0x00 前言
前期第一次遇到反序列化這方面題目的時(shí)候,也看了不少資料,都是前輩們寫(xiě)的總結(jié),但是都是直接從在ctf中的運(yùn)用開(kāi)始的,自己在這段時(shí)間整理的過(guò)程中,發(fā)現(xiàn)對(duì)于php類與對(duì)象了解不是很多,導(dǎo)致在看一些題目、或值前輩的總結(jié)時(shí)都比較困難,下面參考php文檔,結(jié)合自己對(duì)php類與對(duì)象的理解先把反序列化的基礎(chǔ)知識(shí)做一下整理。
0x01 php類與對(duì)象
class
在php手冊(cè)中這樣介紹:
每個(gè)類的定義都以關(guān)鍵字
class開(kāi)頭,后面跟著類名,后面跟著一對(duì)花括號(hào),里面包含有類的屬性與方法的定義。
類名可以是任何非 PHP保留字 的合法標(biāo)簽。在這一點(diǎn)中與變量定義相同,避免歧義。一個(gè)合法類名以字母或下劃線開(kāi)頭,后面跟著若干字母,數(shù)字或下劃線。以正則表達(dá)式表示為:^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*$。一個(gè)類可以包含有屬于自己的常量,變量(稱為“屬性”)以及函數(shù)(稱為“方法”)。
<?php
class SimpleClass
{
// 聲明屬性
public $var = 'a default value';
// 聲明方法
public function displayVar() {
echo $this->var;
}
}
?>
當(dāng)一個(gè)方法在類定義內(nèi)部被調(diào)用時(shí),有一個(gè)可用的偽變量 $this。$this 是一個(gè)到當(dāng)前對(duì)象的引用。此時(shí)所有類內(nèi)的$this都表示當(dāng)前這個(gè)類,訪問(wèn)的對(duì)象與方法也是當(dāng)前類的。例如 $this->var;
new
對(duì)定義的類進(jìn)行創(chuàng)建對(duì)應(yīng)的實(shí)例,必須使用new方法,new方法在創(chuàng)建一個(gè)實(shí)例時(shí),不管以構(gòu)造函數(shù)還是直接傳入的方式,其必須為類內(nèi)的屬性賦初始值。當(dāng)沒(méi)有參數(shù)要傳遞給類的構(gòu)造函數(shù),類名后的括號(hào)則可以省略掉。
例如對(duì)于上面的類:
<?php
$instance = new SimpleClass();
// 也可以這樣做:
$className = 'SimpleClass';
$instance = new $className(); // new SimpleClass()
//或不加括號(hào)
$instance = new $className;
?>
變量與方法
在php中類的屬性和方法存在于不同的“命名空間”中,也就是說(shuō)一個(gè)類的屬性和方法可以使用同樣的名字。 在類中訪問(wèn)屬性和調(diào)用方法使用同樣的操作符,具體是訪問(wèn)一個(gè)屬性還是調(diào)用一個(gè)方法,取決于你的上下文,即用法是變量訪問(wèn)還是函數(shù)調(diào)用。
例如:
<?php
class Foo
{
public $bar = 'property';
public function bar() {
return 'method';
}
}
$obj = new Foo();
echo $obj->bar, PHP_EOL, $obj->bar(), PHP_EOL;//第一個(gè)訪問(wèn)類屬性成員bar,第二個(gè)則訪問(wèn)類方法bar()。
屬性
類的變量成員叫做“屬性”,屬性聲明是由關(guān)鍵字 public,protected 或者private開(kāi)頭,然后跟一個(gè)普通的變量聲明來(lái)組成。屬性中的變量可以初始化,但是初始化的值必須是常數(shù),這里的常數(shù)是指 PHP 腳本在編譯階段時(shí)就可以得到其值,而不依賴于運(yùn)行時(shí)的信息才能求值。
- 被定義為公有的類成員可以在任何地方被訪問(wèn)。
- 被定義為受保護(hù)的類成員則可以被其自身以及其子類和父類訪問(wèn)。
- 被定義為私有的類成員則只能被其定義所在的類訪問(wèn)。
而在類的成員方法里面,可以用 ->(對(duì)象運(yùn)算符):$this->property(其中 property 是該屬性名)這種方式來(lái)訪問(wèn)非靜態(tài)屬性。靜態(tài)屬性則是用 ::(雙冒號(hào)):self::$property 來(lái)訪問(wèn)。這里的靜態(tài),一定是被static聲明過(guò)的屬性。且::后的$符不可省略。
當(dāng)調(diào)用的方法不是靜態(tài)時(shí)會(huì)拋出錯(cuò)誤:
Non-static method Foo::bar() should not be called statically
<?php
class Foo
{
public static $bar = 'property';
}
$obj = new Foo();
echo $obj::$bar;
類常量
可以把在類中始終保持不變的值定義為常量。在定義和使用常量的時(shí)候不需要使用 $ 符號(hào)。且常量的值必須是一個(gè)定值,不能是變量,類屬性,數(shù)學(xué)運(yùn)算的結(jié)果或函數(shù)調(diào)用。
<?php
class MyClass
{
const constant = 'constant value';
function showConstant() {
echo self::constant . "\n";
}
}
echo MyClass::constant . "\n";//注意這里沒(méi)有$,與上面做對(duì)比。
當(dāng)使用定義的變量$boj->constant時(shí),會(huì)返回 Undefined property: MyClass::$constant,可見(jiàn)對(duì)象運(yùn)算符->中不需要$是因?yàn)樵谠L問(wèn)時(shí)自動(dòng)認(rèn)為屬性為變量。
0x02 構(gòu)造函數(shù)與析構(gòu)函數(shù)
具有構(gòu)造函數(shù)的類會(huì)在每次創(chuàng)建新對(duì)象時(shí)先調(diào)用此方法,非常適合在使用對(duì)象之前做一些初始化工作。其基本格式為:
__construct(mixed ...$values = ""): void
而析構(gòu)函數(shù)會(huì)在到某個(gè)對(duì)象的所有引用都被刪除或者當(dāng)對(duì)象被顯式銷毀時(shí)執(zhí)行。他的基本格式為:
__destruct(): void
例如:
<?php
class MyDestructableClass
{
function __construct() {
echo "In constructor\n","<br/>";
}
function __destruct() {
print "Destroying " . __CLASS__ . "\n";
}
}
$obj = new MyDestructableClass();
運(yùn)行結(jié)果為:
In constructor
Destroying MyDestructableClass
可見(jiàn)在整個(gè)腳本結(jié)束后析構(gòu)函數(shù)會(huì)被自動(dòng)調(diào)用,需要注意的是在使用exit()終止腳本運(yùn)行時(shí)也會(huì)被調(diào)用。但在析構(gòu)函數(shù)中調(diào)用 exit()會(huì)中止其余關(guān)閉操作的運(yùn)行。
0x03 PHP魔術(shù)方法
PHP將所有以 __(兩個(gè)下劃線)開(kāi)頭的類方法保留為魔術(shù)方法。所以在定義類方法時(shí),除了上述魔術(shù)方法,建議不要以__為前綴。且所有的魔術(shù)方法 必須 聲明為 public。其中,魔術(shù)方法有:
__construct()
__destruct()
__call()
__callStatic()
__get()
__set()
__isset()
__unset()
__sleep()
__wakeup()
__serialize()
__unserialize()
__toString()
__invoke()
__set_state()
__clone()
__debugInfo()等方法
-
__construct()和__destruct()為構(gòu)造與析構(gòu)函數(shù),在對(duì)象被創(chuàng)建和銷毀時(shí)調(diào)用,上面也已經(jīng)介紹過(guò)了,不再多加介紹。 -
__call()和__callStatic()為重載方法時(shí)調(diào)用,也就是說(shuō)當(dāng)調(diào)用一個(gè)當(dāng)前類沒(méi)有的方法時(shí),將會(huì)調(diào)用他們(靜態(tài)為callStatic)。
public __call(string $name, array $arguments): mixed·在對(duì)象中調(diào)用一個(gè)不可訪問(wèn)方法時(shí),__call()會(huì)被調(diào)用。
public static __callStatic(string $name, array $arguments): mixed在靜態(tài)上下文中調(diào)用一個(gè)不可訪問(wèn)方法時(shí),__callStatic() 會(huì)被調(diào)用。
-
__set()、__get()、__isset()和__unset()為重載屬性時(shí)調(diào)用,也就是說(shuō)在訪問(wèn)、賦值一個(gè)不可訪問(wèn)、不存在的屬性時(shí),將會(huì)調(diào)用,這些魔術(shù)方法的調(diào)用時(shí)機(jī)就是我們需要掌握的內(nèi)容。
public __set(string $name, mixed $value): void,在給不可訪問(wèn)(protected 或 private)或不存在的屬性賦值時(shí),__set()會(huì)被調(diào)用。
public __get(string `$name`): mixed,讀取不可訪問(wèn)(protected 或 private)或不存在的屬性的值時(shí),__get()會(huì)被調(diào)用。
public __isset(string $name): bool,當(dāng)對(duì)不可訪問(wèn)(protected 或 private)或不存在的屬性調(diào)用 isset()或 empty() 時(shí),__isset()會(huì)被調(diào)用。
public __unset(string $name): void,當(dāng)對(duì)不可訪問(wèn)(protected 或 private)或不存在的屬性調(diào)用 unset() 時(shí),__unset()會(huì)被調(diào)用。
-
__sleep()、__wakeup()為序列化與反序列化之前調(diào)用的函數(shù):
serialize()函數(shù)會(huì)檢查類中是否存在一個(gè)魔術(shù)方法__sleep()。如果存在,該方法會(huì)先被調(diào)用,然后才執(zhí)行序列化操作。此功能可以用于清理對(duì)象,并返回一個(gè)包含對(duì)象中所有應(yīng)被序列化的變量名稱的數(shù)組。
unserialize()會(huì)檢查是否存在一個(gè)__wakeup()方法。如果存在,則會(huì)先調(diào)用__wakeup方法,預(yù)先準(zhǔn)備對(duì)象需要的資源。__wakeup()經(jīng)常用在反序列化操作中,例如重新建立數(shù)據(jù)庫(kù)連接,或執(zhí)行其它初始化操作。
-
__serialize()、__unserialize()為序列化與反序列化之前調(diào)用的函數(shù),與__sleep()、__wakeup()不同,他們的優(yōu)先級(jí)最高,即如果同時(shí)存在__serialize()、__sleep()函數(shù),只會(huì)調(diào)用__serialize()而__sleep()會(huì)被忽略。__unserialize()同理。
public __serialize(): array,serialize()函數(shù)會(huì)檢查類中是否存在一個(gè)魔術(shù)方法__serialize()。如果存在,該方法將在任何序列化之前優(yōu)先執(zhí)行。它必須以一個(gè)代表對(duì)象序列化形式的 鍵/值 成對(duì)的關(guān)聯(lián)數(shù)組形式來(lái)返回,如果沒(méi)有返回?cái)?shù)組,將會(huì)拋出一個(gè) TypeError 錯(cuò)誤。
public __unserialize(array $data): void,相反,unserialize()檢查是否存在具有__unserialize()魔術(shù)方法。如果存在,該函數(shù)將被傳遞給__serialize()返回的恢復(fù)數(shù)組。然后,它可以根據(jù)需要從該數(shù)組中恢復(fù)對(duì)象的屬性。
這兩句話好像比上面所有的話都難以理解,簡(jiǎn)單說(shuō)就是__serialize()會(huì)把要序列化的鍵+值變成對(duì)應(yīng)的數(shù)組返回,這樣其實(shí)對(duì)于最后序列化的字符串來(lái)說(shuō)一目了然,其實(shí)就是換了一種表示方式,那么相反的__unserialize就是把需要反序列化的字串值傳遞給剛剛序列化之前的數(shù)組,是一個(gè)逆過(guò)程。
-
__toString()為一個(gè)類被當(dāng)成字符串時(shí)調(diào)用,例如echo、print,此方法必須返回一個(gè)字符串,否則將發(fā)出一條 E_RECOVERABLE_ERROR 級(jí)別的致命錯(cuò)誤。 -
__invoke()當(dāng)嘗試以調(diào)用函數(shù)的方式調(diào)用一個(gè)對(duì)象時(shí)自動(dòng)調(diào)用。(只在 PHP 5.3.0 及以上版本有效。)
0x04 序列化與反序列化
serialize(mixed $value): string,serialize()返回字符串,此字符串包含了表示 value 的字節(jié)流,可以存儲(chǔ)于任何地方。
在下面的代碼中:
<?php
class MyDestructableClass
{
public $a = "123";
public $b = "456";
}
$obj = new MyDestructableClass();
echo serialize($obj);
返回的字符串格式為:
O:19:"MyDestructableClass":2:{s:1:"a";s:3:"123";s:1:"b";s:3:"456";}
從左至右的字串一次表示為:
序列化對(duì)象O、名稱長(zhǎng)度19、名稱內(nèi)容MyDestructableClass、屬性個(gè)數(shù)2、第一個(gè)屬性類型s長(zhǎng)度1、第一個(gè)屬性名稱a、第一個(gè)屬性a的值類型s長(zhǎng)度3、第一個(gè)屬性a的值內(nèi)容123,第二個(gè)屬性同理。
反序列化就是將上面那段話的意思反過(guò)來(lái),生成一個(gè)有屬性的對(duì)象。使用 unserialize()函數(shù)。
0x05 小結(jié)
對(duì)php類與對(duì)象學(xué)習(xí)之后,再對(duì)之前遇到的題目進(jìn)行學(xué)習(xí)就會(huì)知其然也知其所以然,了解各類型魔術(shù)方法的調(diào)用時(shí)機(jī),就能很好的利用好他們,才會(huì)有自己的思路,下面將對(duì)典型的幾道題目進(jìn)行練習(xí),學(xué)習(xí)CTF中的這類題目解題套路。