0x00 前言
前期第一次遇到反序列化这方面题目的时候,也看了不少资料,都是前辈们写的总结,但是都是直接从在ctf中的运用开始的,自己在这段时间整理的过程中,发现对于php类与对象了解不是很多,导致在看一些题目、或值前辈的总结时都比较困难,下面参考php文档,结合自己对php类与对象的理解先把反序列化的基础知识做一下整理。
0x01 php类与对象
class
在php手册中这样介绍:
每个类的定义都以关键字
class
开头,后面跟着类名,后面跟着一对花括号,里面包含有类的属性与方法的定义。
类名可以是任何非 PHP保留字 的合法标签。在这一点中与变量定义相同,避免歧义。一个合法类名以字母或下划线开头,后面跟着若干字母,数字或下划线。以正则表达式表示为:^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*$
。一个类可以包含有属于自己的常量,变量(称为“属性”)以及函数(称为“方法”)。
<?php
class SimpleClass
{
// 声明属性
public $var = 'a default value';
// 声明方法
public function displayVar() {
echo $this->var;
}
}
?>
当一个方法在类定义内部被调用时,有一个可用的伪变量 $this
。$this
是一个到当前对象的引用。此时所有类内的$this
都表示当前这个类,访问的对象与方法也是当前类的。例如 $this->var;
new
对定义的类进行创建对应的实例,必须使用new方法,new方法在创建一个实例时,不管以构造函数还是直接传入的方式,其必须为类内的属性赋初始值。当没有参数要传递给类的构造函数,类名后的括号则可以省略掉。
例如对于上面的类:
<?php
$instance = new SimpleClass();
// 也可以这样做:
$className = 'SimpleClass';
$instance = new $className(); // new SimpleClass()
//或不加括号
$instance = new $className;
?>
变量与方法
在php中类的属性和方法存在于不同的“命名空间”中,也就是说一个类的属性和方法可以使用同样的名字。 在类中访问属性和调用方法使用同样的操作符,具体是访问一个属性还是调用一个方法,取决于你的上下文,即用法是变量访问还是函数调用。
例如:
<?php
class Foo
{
public $bar = 'property';
public function bar() {
return 'method';
}
}
$obj = new Foo();
echo $obj->bar, PHP_EOL, $obj->bar(), PHP_EOL;//第一个访问类属性成员bar,第二个则访问类方法bar()。
属性
类的变量成员叫做“属性”,属性声明是由关键字 public
,protected
或者private
开头,然后跟一个普通的变量声明来组成。属性中的变量可以初始化,但是初始化的值必须是常数,这里的常数是指 PHP 脚本在编译阶段时就可以得到其值,而不依赖于运行时的信息才能求值。
- 被定义为公有的类成员可以在任何地方被访问。
- 被定义为受保护的类成员则可以被其自身以及其子类和父类访问。
- 被定义为私有的类成员则只能被其定义所在的类访问。
而在类的成员方法里面,可以用 ->(对象运算符):$this->property
(其中 property 是该属性名)这种方式来访问非静态属性。静态属性则是用 ::(双冒号):self::$property
来访问。这里的静态,一定是被static声明过的属性。且::
后的$
符不可省略。
当调用的方法不是静态时会抛出错误:
Non-static method Foo::bar() should not be called statically
<?php
class Foo
{
public static $bar = 'property';
}
$obj = new Foo();
echo $obj::$bar;
类常量
可以把在类中始终保持不变的值定义为常量。在定义和使用常量的时候不需要使用 $ 符号。且常量的值必须是一个定值,不能是变量,类属性,数学运算的结果或函数调用。
<?php
class MyClass
{
const constant = 'constant value';
function showConstant() {
echo self::constant . "\n";
}
}
echo MyClass::constant . "\n";//注意这里没有$,与上面做对比。
当使用定义的变量$boj->constant
时,会返回 Undefined property: MyClass::$constant
,可见对象运算符->
中不需要$
是因为在访问时自动认为属性为变量。
0x02 构造函数与析构函数
具有构造函数的类会在每次创建新对象时先调用此方法,非常适合在使用对象之前做一些初始化工作。其基本格式为:
__construct(mixed ...$values = ""): void
而析构函数会在到某个对象的所有引用都被删除或者当对象被显式销毁时执行。他的基本格式为:
__destruct(): void
例如:
<?php
class MyDestructableClass
{
function __construct() {
echo "In constructor\n","<br/>";
}
function __destruct() {
print "Destroying " . __CLASS__ . "\n";
}
}
$obj = new MyDestructableClass();
运行结果为:
In constructor
Destroying MyDestructableClass
可见在整个脚本结束后析构函数会被自动调用,需要注意的是在使用exit()
终止脚本运行时也会被调用。但在析构函数中调用 exit()
会中止其余关闭操作的运行。
0x03 PHP魔术方法
PHP将所有以 __
(两个下划线)开头的类方法保留为魔术方法。所以在定义类方法时,除了上述魔术方法,建议不要以__
为前缀。且所有的魔术方法 必须 声明为 public
。其中,魔术方法有:
__construct()
__destruct()
__call()
__callStatic()
__get()
__set()
__isset()
__unset()
__sleep()
__wakeup()
__serialize()
__unserialize()
__toString()
__invoke()
__set_state()
__clone()
__debugInfo()等方法
-
__construct()
和__destruct()
为构造与析构函数,在对象被创建和销毁时调用,上面也已经介绍过了,不再多加介绍。 -
__call()
和__callStatic()
为重载方法时调用,也就是说当调用一个当前类没有的方法时,将会调用他们(静态为callStatic)。
public __call(string $name, array $arguments): mixed·
在对象中调用一个不可访问方法时,__call()会被调用。
public static __callStatic(string $name, array $arguments): mixed
在静态上下文中调用一个不可访问方法时,__callStatic() 会被调用。
-
__set()、__get()、__isset()和__unset()
为重载属性时调用,也就是说在访问、赋值一个不可访问、不存在的属性时,将会调用,这些魔术方法的调用时机就是我们需要掌握的内容。
public __set(string $name, mixed $value): void
,在给不可访问(protected 或 private)或不存在的属性赋值时,__set()会被调用。
public __get(string `$name`): mixed
,读取不可访问(protected 或 private)或不存在的属性的值时,__get()会被调用。
public __isset(string $name): bool
,当对不可访问(protected 或 private)或不存在的属性调用 isset()或 empty() 时,__isset()会被调用。
public __unset(string $name): void
,当对不可访问(protected 或 private)或不存在的属性调用 unset() 时,__unset()会被调用。
-
__sleep()、__wakeup()
为序列化与反序列化之前调用的函数:
serialize()
函数会检查类中是否存在一个魔术方法__sleep()
。如果存在,该方法会先被调用,然后才执行序列化操作。此功能可以用于清理对象,并返回一个包含对象中所有应被序列化的变量名称的数组。
unserialize()
会检查是否存在一个__wakeup()
方法。如果存在,则会先调用__wakeup
方法,预先准备对象需要的资源。__wakeup()
经常用在反序列化操作中,例如重新建立数据库连接,或执行其它初始化操作。
-
__serialize()、__unserialize()
为序列化与反序列化之前调用的函数,与__sleep()、__wakeup()
不同,他们的优先级最高,即如果同时存在__serialize()、__sleep()
函数,只会调用__serialize()
而__sleep()
会被忽略。__unserialize()
同理。
public __serialize(): array
,serialize()
函数会检查类中是否存在一个魔术方法__serialize()。如果存在,该方法将在任何序列化之前优先执行。它必须以一个代表对象序列化形式的 键/值 成对的关联数组形式来返回,如果没有返回数组,将会抛出一个 TypeError 错误。
public __unserialize(array $data): void
,相反,unserialize()检查是否存在具有__unserialize()魔术方法。如果存在,该函数将被传递给__serialize()返回的恢复数组。然后,它可以根据需要从该数组中恢复对象的属性。
这两句话好像比上面所有的话都难以理解,简单说就是__serialize()
会把要序列化的键+值变成对应的数组返回,这样其实对于最后序列化的字符串来说一目了然,其实就是换了一种表示方式,那么相反的__unserialize
就是把需要反序列化的字串值传递给刚刚序列化之前的数组,是一个逆过程。
-
__toString()
为一个类被当成字符串时调用,例如echo、print
,此方法必须返回一个字符串,否则将发出一条 E_RECOVERABLE_ERROR 级别的致命错误。 -
__invoke()
当尝试以调用函数的方式调用一个对象时自动调用。(只在 PHP 5.3.0 及以上版本有效。)
0x04 序列化与反序列化
serialize(mixed $value): string
,serialize()
返回字符串,此字符串包含了表示 value
的字节流,可以存储于任何地方。
在下面的代码中:
<?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";}
从左至右的字串一次表示为:
序列化对象O
、名称长度19
、名称内容MyDestructableClass
、属性个数2
、第一个属性类型s
长度1
、第一个属性名称a
、第一个属性a
的值类型s
长度3
、第一个属性a
的值内容123
,第二个属性同理。
反序列化就是将上面那段话的意思反过来,生成一个有属性的对象。使用 unserialize()
函数。
0x05 小结
对php类与对象学习之后,再对之前遇到的题目进行学习就会知其然也知其所以然,了解各类型魔术方法的调用时机,就能很好的利用好他们,才会有自己的思路,下面将对典型的几道题目进行练习,学习CTF中的这类题目解题套路。