關注這個專欄的其他相關筆記:[Web 安全] 反序列化漏洞 - 學習筆記-CSDN博客
0x01:PHP 序列化 — Serialize
序列化就是將對象的狀態信息轉化為可以存儲或傳輸的形式的過程,在 PHP 中,通常使用 serialize()
函數來完成序列化的操作。
下面筆者直接簡要點列出各個數據類型序列化之后的結果,不信的崽崽可以自己跑一下:
?echo serialize(null); ? ? ? // N;echo serialize(123); ? ? ? ?// i:123; ? => int 類型的值為 123echo serialize(123.3); ? ? ?// d:123.3; => double 類型的值為 123.3echo serialize(true); ? ? ? // b:1; ? ? => Boolean 類型的值為 Trueecho serialize("Blue17"); ? // s:6:"Blue17"; => String 類型的值為 Blue17echo serialize(array("Blue17", 17, null)); // a:3:{i:0;s:6:"Blue17";i:1;i:17;i:2;N;}
0x0101:序列化結果解析 — Array 類型
這部分筆者就簡單分析一下上面 Array 類型序列化后的結果:
?echo serialize(array("Blue17", 17, null)); // a:3:{i:0;s:6:"Blue17";i:1;i:17;i:2;N;}?/*a:3 ? ? ? ? ? ? ? => array 中有三個元素i:0;s:6:"Blue17" => index 為 0 的地方是一個長度為 6 的 String 類型的元素,值為 Blue17i:1;i:17; ? ? ? ? => index 為 1 的地方是一個 int 類型的元素,值為 17i:2;N; ? ? ? ? ? => index 為 2 的地方是一個 Null 類型的元素。*/
0x0102:序列化結果解析 — 類
類的序列化結果基本大差不差,但是不同 “訪問類型” 的變量序列化的結果有很大差異,這部分筆者就按照 “訪問類型” 進行分類,并對每個單獨進行講解。
1. 類序列化結果解析 —— Public 型
這里我們開始進入正規,講講最常見的類的序列化結果,先來一個最常見的試試水:
?<?php?class demo {public $var1; ? ? ? ? ? ? ? // 這個變量沒有賦值public $var2 = "Blue17"; ? ?// 這個變量賦予了字符串類型的值var $var3 = 17; ? ? ? ? ?// 雖然修飾符是 var 但其實還是 public 類型的?function printVar($var) {$localVar = $var; ? ?// 類方法中的局部變量echo $localVar;}}?// O:4:"demo":3:{s:4:"var1";N;s:4:"var2";s:6:"Blue17";s:4:"var3";i:17;}echo serialize(new demo(array(123)));
下面我們仔細分析一下結果,可以看到,它序列化的結果基本全是變量,類中方法其實是沒有被序列化的,類中的局部變量也沒有被序列化:
?O:4:"demo":3:{s:4:"var1";N;s:4:"var2";s:6:"Blue17";s:4:"var3";i:17;}?// O:4:"demo":3 => Object 對象是一個 4 字的叫 demo,其中有 3 個屬性(變量)// s:4:"var1";N; => 屬性名稱占 4 字節,叫 var1 其值是 Null 類型// s:4:"var2";s:6:"Blue17" => 屬性名稱占 4 字節,叫 var2,其值是 String 類型,長度為 6 內容是 Blue17// s:4:"var3";i:17; => 屬性名稱占 4 字節,叫 var3,其值是 int 類型,值為 17。
2. 類序列化結果解析 —— Protected 型
下面我們來看看如果類的屬性中混入了 Protect 型的變量它序列化的結果長啥樣吧:
?<?php?class demo {protected $var1; ? ? ? ? ? ? ? // 這個變量沒有賦值protected $var2 = "Blue17"; ? ?// 這個變量賦予了字符串類型的值protected $var3 = 17; ? ? ? ? ?// 雖然修飾符是 var 但其實還是 public 類型的}?echo serialize(new demo()) . "\n";// O:4:"demo":3:{s:7:"*var1";N;s:7:"*var2";s:6:"Blue17";s:7:"*var3";i:17;}?echo urlencode(serialize(new demo()));// O%3A4%3A%22demo%22%3A3%3A%7Bs%3A7%3A%22%00%2A%00var1%22%3BN%3Bs%3A7%3A%22%00%2A%00var2%22%3Bs%3A6%3A%22Blue17%22%3Bs%3A7%3A%22%00%2A%00var3%22%3Bi%3A17%3B%7D
下面我們仔細分析一下結果,這里筆者特意對它序列化后的結果做了一個編碼,因為里面其實有一些不可見的字符,不編碼是看不出來的,大部分內容其實與 Public 一致,但是被 protected 修飾的變量序列化后的內容就有很大不同了:
?O:4:"demo":3:{s:7:"*var1";N;s:7:"*var2";s:6:"Blue17";s:7:"*var3";i:17;}?// s:7:"*var1";N; => 屬性名稱占 7 個字節?怎么數著只有 5 個??// 這個是 URL 編碼后的內容:s%3A7%3A%22%00%2A%00var1%22%3BN%3B// 這個是簡單解碼后的樣子:s:7:"%00*%00var1";N;
如上,可以發現,Protected 屬性序列化后明面看只有 *var1
這樣,但其實 *
兩邊其實還藏了兩個 ASCII 碼值為 0 的字符 (這也引出后面一個 Bug,在嘗試構造反序列化值得時候,不建議通過直接復制就篡改被 Protected 或者 Private 修飾的值,因為你復制得內容一般都不全,會丟掉這個特殊的 %00
)。
3. 類序列化結果解析 —— Private 型
下面我們來看看如果類的屬性中混入了 Private 型的變量它序列化的結果長啥樣吧:
?<?php?class demo {private $var1; ? ? ? ? ? ? ? // 這個變量沒有賦值private $var2 = "Blue17"; ? ?// 這個變量賦予了字符串類型的值}?echo serialize(new demo()) . "\n";// O:4:"demo":2:{s:10:"demovar1";N;s:10:"demovar2";s:6:"Blue17";}?echo urlencode(serialize(new demo()));// O%3A4%3A%22demo%22%3A2%3A%7Bs%3A10%3A%22%00demo%00var1%22%3BN%3Bs%3A10%3A%22%00demo%00var2%22%3Bs%3A6%3A%22Blue17%22%3B%7D
下面我們仔細分析一下結果,這里筆者特意對它序列化后的結果做了一個編碼,因為里面其實有一些不可見的字符,不編碼是看不出來的,大部分內容其實與 Public 一致,但是被 Private 修飾的變量序列化后的內容就有很大不同了:
?O:4:"demo":2:{s:10:"demovar1";N;s:10:"demovar2";s:6:"Blue17";}?// s:10:"demovar1";N; => 屬性名稱占 10 個字節?怎么數著只有 8 個??// 這個是 URL 編碼后的內容:s%3A10%3A%22%00demo%00var1%22%3BN%3B// 這個是簡單解碼后的樣子:s:10:"%00demo%00var1";N; => %00 算一位,數一數,剛好 10 位
如上,可以發現,Private 屬性序列化后明面看只有 demovar1
這樣,但其實類名兩邊其實還藏了兩個 ASCII 碼值為 0 的字符,所以其真實格式為(URL 編碼后的哈,不編碼的話 ASCII 值為 0 的其實是不可見字符) %00類名%00變量名
。
0x02:PHP 反序列化 — Unserialize
以下是反序列化相關的幾個特性:
-
反序列化就是將序列化得到的字符串轉化為一個對象的過程。
-
反序列化生成的對象成員屬性值由被反序列化的字符串決定,與原來類預定義的值無關。
-
PHP 中通過
unserialize()
函數進行反序列化,序列化使用serialize()
函數。 -
反序列化不觸發類的成員方法,需要被調用方法后才會被觸發。(不一定,這個后面講)
下面筆者就以 Public 型的類為例,講解一下反序列化的用處(另外兩種類型,流程一致,但是要特別注意 %00
到底有沒有被復制過去,如果報錯了,一般就是這個的問題)。
0x0201:反序列化 —— 正常流程
首先是比較簡單的 Public 型類的反序列化,我們先創建一個類,假設叫 Note
(筆記)類吧,然后我們寫筆記就要實例化這個類,然后往這個類的對象里寫東西,代碼如下:
<?phpclass Note {public $title; // 筆記標題public $content; // 筆記內容// 記錄標題 & 內容function write($title, $content) {$this -> title = $title;$this -> content = $content;}// 讀取標題 & 內容function read() {echo "Title: " . $this -> title . "\n";echo "Content: " . $this -> content . "\n";}
}// 實例化筆記類
$note = new Note();
// 往筆記里寫東西
$note -> write("Hello, World!!", "Today Is a Nice Day!!");
如上,我們已經往筆記里寫東西了,寫了你要保存吧,可是你是個對象你咋保存?這時就可以使用序列化,把 $note
這個對象里的內容序列化然后存儲在一個文件里:
// 保存 $note 筆記里的東西
echo serialize($note); // O:4:"Note":2:{s:5:"title";s:14:"Hello, World!!";s:7:"content";s:21:"Today Is a Nice Day!!";}
OK,保存了你過段時間得看吧,給你看下面這個東西你又看不懂是不是:
O:4:"Note":2:{s:5:"title";s:14:"Hello, World!!";s:7:"content";s:21:"Today Is a Nice Day!!";}
這個時候就需要用我們軟件進行反序列化然后再調用 read
方法了是吧:
// 保存 $note 筆記里的東西
$save = serialize($note); // O:4:"Note":2:{s:5:"title";s:14:"Hello, World!!";s:7:"content";s:21:"Today Is a Nice Day!!";}// 查看保存的內容
$raw = unserialize($save); // 對序列化的內容進行反序列化
$raw -> read();
如上,可以看到,通過反序列化被保存的值,我們成功還原了用戶筆記里寫的東西。下面是整個測試的源碼(這里筆者特別提醒,反序列化的環境中要有序列化的那個類,不然即使反序列化了也是無法執行類的方法的):
<?php// 創建筆記類
class Note {public $title; // 筆記標題public $content; // 筆記內容// 記錄標題 & 內容function write($title, $content) {$this -> title = $title;$this -> content = $content;}// 讀取標題 & 內容function read() {echo "Title: " . $this -> title . "\n";echo "Content: " . $this -> content . "\n";}
}// 實例化筆記類
$note = new Note();
// 往筆記里寫東西
$note -> write("Hello, World!!", "Today Is a Nice Day!!");// 保存 $note 筆記里的東西
$save = serialize($note); // O:4:"Note":2:{s:5:"title";s:14:"Hello, World!!";s:7:"content";s:21:"Today Is a Nice Day!!";}// 查看保存的內容
$raw = unserialize($save); // 對序列化的內容進行反序列化
$raw -> read(); // 執行類的成員方法
0x0202:反序列化 —— 異常流程
我們繼續假設,我們剛剛是本地的,假設你寫了筆記,然后你要上傳,那你上傳服務端的序列化的內容就是下面這個:
O:4:"Note":2:{s:5:"title";s:14:"Hello, World!!";s:7:"content";s:21:"Today Is a Nice Day!!";}
假設,攻擊者截獲了這個內容,按照 PHP 序列化的格式自己改了一下(主要是改內容和長度):
O:4:"Note":2:{s:5:"title";s:14:"Hello, World!!";s:7:"content";s:25:"Today Is NOT a Nice Day!!";}
如上,我們添加了一個單詞 Not ,然后修改了長度,然后我們 “發送” 到服務端,讓他讀一下,代碼如下:
<?php// 創建筆記類
class Note {public $title; // 筆記標題public $content; // 筆記內容// 記錄標題 & 內容function write($title, $content) {$this -> title = $title;$this -> content = $content;}// 讀取標題 & 內容function read() {echo "Title: " . $this -> title . "\n";echo "Content: " . $this -> content . "\n";}
}
// 接收的信息
$receive = 'O:4:"Note":2:{s:5:"title";s:14:"Hello, World!!";s:7:"content";s:25:"Today Is NOT a Nice Day!!";}';$raw = unserialize($receive); // 對序列化的內容進行反序列化
$raw -> read(); // 調用讀方法
如上,可以看到,結果就這樣被修改了。這就是前面介紹的反序列化的一個特性 “反序列化生成的對象成員屬性值由被反序列化的字符串決定,與原來類預定義的值無關。”,也是我們后面 “反序列化漏洞的依據”。