欢迎访问Sunbet官网(www.sunbet.us),Allbet欧博官网(www.ALLbetgame.us)!

首页Sunbet_安全防护正文

TP6.0反序列化应用链发掘思绪总结

b9e08c31ae1faa592019-12-1583安全技术漏洞分析

近来CTF中TP反序列化考的比较频仍,从前段时刻的N1CTF到近来的安洵杯都应用了ThinkPHP反序列化,猖獗填坑,审计发掘了下TP5、TP6反序列化中的应用链,本篇主要总结下TP6应用链的发掘思绪。小白文章,大佬们请略过。。。 TP5反序列化进口都是在Windows类的析构要领,经由历程file_exists()函数触发__toString 把戏要领,然后以__toString为中心跳板寻觅代码实行点,形成反序列化恣意敕令实行。有关TP5的剖析能够看发掘潜伏thinkphp中的反序列应用链这篇文章,觉得剖析的思绪比较好,本篇剖析TP6,也是根据文中的思绪来的。 TP6的差别之处就是没有了Windows类,也就没法应用个中的析构要领作为反序列化进口,须要从新发掘其他进口点。

基本知识

1.PHP反序列化

序列化:将php值转换为可存储或传输的字符串,目标是防备丧失其组织和数据类型。 反序列化:序列化的逆历程,将字符串再转化成本来的php变量,以便于运用。 简朴来讲,就是触及php中的serialize与unserialize两个函数。

2.PHP把戏要领

把戏要领:在php中以两个下划线字符(__)开头的要领,要领名都是PHP预先定义好的,之所以称为把戏要领 就是这些要领不须要显现的挪用而是由某种特定的前提触发实行。 经常使用的把戏要领: __constuct: 构建对象的时被挪用 __destruct: 明白烧毁对象或剧本结束时被挪用 __wakeup: 当运用unserialize时被挪用,可用于做些对象的初始化操纵 __sleep: 当运用serialize时被挪用,当你不须要保留大对象的一切数据时很有效 __call: 挪用不可接见或不存在的要领时被挪用 __callStatic: 挪用不可接见或不存在的静态要领时被挪用 __set: 当给不可接见或不存在属性赋值时被挪用 __get: 读取不可接见或不存在属性时被挪用 __isset: 对不可接见或不存在的属性挪用isset()或empty()时被挪用 __unset: 对不可接见或不存在的属性举行unset时被挪用 __invoke: 当以函数体式格局挪用对象时被挪用 __toString: 当一个类被转换成字符串时被挪用 __clone: 举行对象clone时被挪用,用来调解对象的克隆行动 __debuginfo: 当挪用var_dump()打印对象时被挪用(当你不想打印一切属性)适用于PHP5.6版本 __set_state: 当挪用var_export()导出类时,此静态要领被挪用。用__set_state的返回值做为var_export的返回值

3.反序列化破绽应用历程

反序列化破绽就是经由历程多个类,给予肯定前提,使其自动挪用把戏要领,终究到达代码实行点。历程包括出发点、中心跳板、尽头。

出发点

最经常使用的就是反序列化时触发的把戏要领: __destruct: 明白烧毁对象或剧本结束时被挪用 __wakeup: 当运用unserialize时被挪用,可用于做些对象的初始化操纵 有关字符串操纵能够触发的把戏要领: __toString: 当一个类被转换成字符串时被挪用 触发的状况有:
用到打印有关函数时,如echo/ print等
拼接字符串时
格式化字符串时
与字符串举行==比较时
格式化SQL语句,绑定参数时
数组中有字符串时

中心跳板

__toString: 当一个类被转换成字符串时被挪用 __call: 挪用不可接见或不存在的要领时被挪用 __callStatic: 挪用不可接见或不存在的静态要领时被挪用 __set: 当给不可接见或不存在属性赋值时被挪用 __get: 读取不可接见或不存在属性时被挪用 __isset: 对不可接见或不存在的属性挪用isset()或empty()时被挪用

尽头

__call: 挪用不可接见或不存在的要领时被挪用 call_user_funccall_user_func_array等代码实行点

应用链发掘

主要剖析三篇应用链的发掘思绪,网上也有许多剖析,然则发明许多POC都不能用,因而本身剖析组织下POC。

1.环境搭建

TP6.0装置参照Thinkphp6手册,从5.2版本入手下手不能应用下载的要领,须要应用composer。
composer create-project topthink/think TP-6.0 6.0.*-dev

2.应用出发点

出发点的发掘能够应用直接搜刮经常使用进口魔法函数的要领。 TP6.0反序列化应用链发掘思绪总结  安全技术 漏洞分析 第1张 第一条应用链挑选从Model类剖析: vendor/topthink/think-orm/src/Model.php TP6.0反序列化应用链发掘思绪总结  安全技术 漏洞分析 第2张 满足$this->lazySave 为true 就可以够进入save 要领,跟进下save要领: 发明满足前提就可以够进入updateData 要领:
if ($this->isEmpty() || false === $this->trigger('BeforeWrite')) {
    return false;
}
$result = $this->exists ? $this->updateData() : $this->insertData($sequence);
须要满足:
$this->isEmpty()为false
$this->trigger('BeforeWrite') 为true
$this->exists 为true
跟进下: TP6.0反序列化应用链发掘思绪总结  安全技术 漏洞分析 第3张 TP6.0反序列化应用链发掘思绪总结  安全技术 漏洞分析 第4张
$this->isEmpty()为false 须要$this->data不为空
$this->trigger('BeforeWrite') 为true 须要$this->withEvent 为false
$this->exists 为true
进入updateData函数:
protected function updateData(): bool
{
    // 事宜回调
    if (false === $this->trigger('BeforeUpdate')) {
        return false;
    }
    $this->checkData();
    // 猎取有更新的数据
    $data = $this->getChangedData();
    if (empty($data)) {
        // 关联更新
        if (!empty($this->relationWrite)) {
            $this->autoRelationUpdate();
        }
        return true;
    }
    if ($this->autoWriteTimestamp && $this->updateTime && !isset($data[$this->updateTime])) {
        // 自动写入更新时刻
        $data[$this->updateTime]       = $this->autoWriteTimestamp($this->updateTime);
        $this->data[$this->updateTime] = $data[$this->updateTime];
    }
    // 搜检许可字段
    $allowFields = $this->checkAllowFields();

    foreach ($this->relationWrite as $name => $val) {
        if (!is_array($val)) {
            continue;
        }

        foreach ($val as $key) {
            if (isset($data[$key])) {
                unset($data[$key]);
            }
        }
    }

    // 模子更新
    $db = $this->db();
    $db->startTrans();

    try {
        $this->key = null;
        $where     = $this->getWhere();

        $result = $db->where($where)
            ->strict(false)
            ->cache(true)
            ->setOption('key', $this->key)
            ->field($allowFields)
            ->update($data);

        $this->checkResult($result);

        // 关联更新
        if (!empty($this->relationWrite)) {
            $this->autoRelationUpdate();
        }

        $db->commit();

        // 更新回调
        $this->trigger('AfterUpdate');

        return true;
    } catch (\Exception $e) {
        $db->rollback();
        throw $e;
    }
}
然后跟进函数剖析,发明checkAllowFields函数的db 函数存在提到的拼接字符串操纵,因而能够触发__toString TP6.0反序列化应用链发掘思绪总结  安全技术 漏洞分析 第5张 然后再剖析updateData函数和checkAllowFields函数 看下进入db函数的前提: 起首是updateData函数: TP6.0反序列化应用链发掘思绪总结  安全技术 漏洞分析 第6张 跟进getChangedData()函数: TP6.0反序列化应用链发掘思绪总结  安全技术 漏洞分析 第7张 满足$this->force为true即可,如许进入到checkAllowFields 函数: TP6.0反序列化应用链发掘思绪总结  安全技术 漏洞分析 第8张 $this->field为空,$this->schema 为空即可进入db函数,看一下拼接字符串须要满足的前提: TP6.0反序列化应用链发掘思绪总结  安全技术 漏洞分析 第9张 跟进发明只需$this->connectionmysql 即可。 梳理下思绪:
//寻觅一个进口把戏要领
//能够应用 vendor/topthink/think-orm/src/Model.php
public function __destruct()
{
    if ($this->lazySave) { //须要满足$this->lazySave为true
        $this->save();
    }
}


public function save(array $data = [], string $sequence = null): bool
{
    if ($this->isEmpty() || false === $this->trigger('BeforeWrite')) {
        return false;
    }//须要满足 $this->data不为空 $this->withEvent为false
    $result = $this->exists ? $this->updateData() : $this->insertData($sequence);
}//$this->exists为true


protected function updateData(): bool
{
    if (false === $this->trigger('BeforeUpdate')) {
        return false;//$this->withEvent为false已满足
    }
    $data = $this->getChangedData();//$this->force 为true
    $allowFields = $this->checkAllowFields();
}


protected function checkAllowFields(): array
{
    if (empty($this->field)) {//$this->field 为空
        if (!empty($this->schema)) {//$this->schema 为空
            $this->field = array_keys(array_merge($this->schema, $this->jsonType));
        } else {
            $query = $this->db();
        }
    }
}


public function db($scope = []): Query
{
    $query = self::$db->connect($this->connection)//$this->connection为mysql
        ->name($this->name . $this->suffix)
        ->pk($this->pk);
    return $query;
}

3.中心跳板

前边组织前提已触发__toString函数,如今须要寻觅可应用类的__toString。 经由历程审计发明后续应用思绪和TP5.2版本应用动态代码实行是一样的,这里只做简朴剖析。 经由历程搜刮不难发明熟习的Conversion 类,直接应用TP5.2的应用链: TP6.0反序列化应用链发掘思绪总结  安全技术 漏洞分析 第10张 跟进函数: TP6.0反序列化应用链发掘思绪总结  安全技术 漏洞分析 第11张
public function toArray(): array
{
    $item       = [];
    $hasVisible = false;

    foreach ($this->visible as $key => $val) {
        if (is_string($val)) {
            if (strpos($val, '.')) {
                [$relation, $name]          = explode('.', $val);
                $this->visible[$relation][] = $name;
            } else {
                $this->visible[$val] = true;
                $hasVisible          = true;
            }
            unset($this->visible[$key]);
        }
    }

    foreach ($this->hidden as $key => $val) {
        if (is_string($val)) {
            if (strpos($val, '.')) {
                [$relation, $name]         = explode('.', $val);
                $this->hidden[$relation][] = $name;
            } else {
                $this->hidden[$val] = true;
            }
            unset($this->hidden[$key]);
        }
    }

    // 兼并关联数据
    $data = array_merge($this->data, $this->relation);

    foreach ($data as $key => $val) {
        if ($val instanceof Model || $val instanceof ModelCollection) {
            // 关联模子对象
            if (isset($this->visible[$key]) && is_array($this->visible[$key])) {
                $val->visible($this->visible[$key]);
            } elseif (isset($this->hidden[$key]) && is_array($this->hidden[$key])) {
                $val->hidden($this->hidden[$key]);
            }
            // 关联模子对象
            if (!isset($this->hidden[$key]) || true !== $this->hidden[$key]) {
                $item[$key] = $val->toArray();
            }
        } elseif (isset($this->visible[$key])) {
            $item[$key] = $this->getAttr($key);
        } elseif (!isset($this->hidden[$key]) && !$hasVisible) {
            $item[$key] = $this->getAttr($key);
        }
    }

    // 追加属性(必需定义猎取器)
    foreach ($this->append as $key => $name) {
        $this->appendAttrToArray($item, $key, $name);
    }

    return $item;
}
进入toArray 函数后,TP5.2有两个思绪,一个是应用getAttrgetValue 函数,然后$value = $closure($value, $this->data);动态挪用;另一个思绪是进入appendAttrToArray 函数,应用$relation->visible($name); 触发__call要领,TP6.0中第二种要领不能用了,第一种是能够的。接下来就是寻觅终究的代码实行点。

4.代码实行

toArray 要领中$data = array_merge($this->data, $this->relation);是可控的,所以$item[$key] = $this->getAttr($key); 中的$key 也是可控的,进入getAttr 函数: TP6.0反序列化应用链发掘思绪总结  安全技术 漏洞分析 第12张 起首进入getData 函数看下$value值的处置惩罚:
PIE庇护详解和经常使用bypass手段 什么是PIE呢? PIE全称是position-independent executable,中文解释为地址无关可执行文件,该技术是一个针对代码段(.text)、数据段(.data)、未初始化全局变量段(.bss)等固定地址的一个防护技术,如果程序开启了PIE保护的话,在每次加载程序时都变换加载地址,从而不能通过ROPgadget等一些工具来帮助解题 下面通过一个例子来具体看一下PIE的效果 程序源码 #include int main() { printf("%s","hello world!"); return 0; } 编译命令 gcc -fno-stack-protector -no-pie -s test.c -o test #不开启PIE保护 不开启PIE保护的时候每次运行时加载地址不变 开启PIE保护的时候每次运行时加载地址是随机变化的 可以看出,如果一个程序开启了PIE保护的话,对于ROP造成很大影响,下面来讲解一下绕过PIE开启的方
TP6.0反序列化应用链发掘思绪总结  安全技术 漏洞分析 第13张 TP6.0反序列化应用链发掘思绪总结  安全技术 漏洞分析 第14张 能够发明$value为可控的,由此getValue 函数的参数都是可控,进入到getValue函数: TP6.0反序列化应用链发掘思绪总结  安全技术 漏洞分析 第15张 $this->withAttr可控,$this->data也可控: TP6.0反序列化应用链发掘思绪总结  安全技术 漏洞分析 第16张 如许就可以够实行恣意代码。 梳理下思绪:
// vendor/topthink/think-orm/src/model/concern/Conversion.php

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

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

public function toArray(): array
{
    $data = array_merge($this->data, $this->relation);
    foreach ($data as $key => $val)
    $item[$key] = $this->getAttr($key);
}

// vendor/topthink/think-orm/src/model/concern/Attribute.php

public function getAttr(string $name)
{
    return $this->getValue($name, $value, $relation);
}


protected function getValue(string $name, $value, bool $relation = false)
{
    $closure = $this->withAttr[$fieldName];
    $value = $closure($value, $this->data);
}
如许一跳完整的应用链就出来。

5.另一条应用出发点

应用出发点发掘的时刻发明还存在其他出发点。 vendor/league/flysystem-cached-adapter/src/Storage/AbstractCache.php 须要满足$this->autosave 为false,进入save函数,发明并没有完成什么功用,参考网上师傅剖析的思绪,AbstractCache类的子类有无完成该函数: TP6.0反序列化应用链发掘思绪总结  安全技术 漏洞分析 第17张 进入到vendor/topthink/framework/src/think/filesystem/CacheStore.php,发明了save 要领: TP6.0反序列化应用链发掘思绪总结  安全技术 漏洞分析 第18张 看到可控的$this->store能够触发恣意类的set要领只需找到恣意类存在风险操纵的set 要领即可应用。$this->key可控,$this->expire可控。 跟进下$this->getForStorageTP6.0反序列化应用链发掘思绪总结  安全技术 漏洞分析 第19张 $this->cache可控,$this->complete 可控,因而$contents可控,只不过经由一次json编码,然则不影响目标。 寻觅一处存在风险行动的set 要领: TP6.0反序列化应用链发掘思绪总结  安全技术 漏洞分析 第20张 vendor/topthink/framework/src/think/cache/driver/File.php
public function set($name, $value, $expire = null): bool
{
    $this->writeTimes++;

    if (is_null($expire)) {
        $expire = $this->options['expire'];
    }

    $expire   = $this->getExpireTime($expire);
    $filename = $this->getCacheKey($name);

    $dir = dirname($filename);

    if (!is_dir($dir)) {
        try {
            mkdir($dir, 0755, true);
        } catch (\Exception $e) {
            // 建立失利
        }
    }

    $data = $this->serialize($value);

    if ($this->options['data_compress'] && function_exists('gzcompress')) {
        //数据压缩
        $data = gzcompress($data, 3);
    }

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

    if ($result) {
        clearstatcache();
        return true;
    }

    return false;
}
剖析set要领,跟踪下几个主要的函数:
$filename = $this->getCacheKey($name);
public function getCacheKey(string $name): string
{
    $name = hash($this->options['hash_type'], $name);

    if ($this->options['cache_subdir']) {
        // 运用子目录
        $name = substr($name, 0, 2) . DIRECTORY_SEPARATOR . substr($name, 2);
    }

    if ($this->options['prefix']) {
        $name = $this->options['prefix'] . DIRECTORY_SEPARATOR . $name;
    }

    return $this->options['path'] . $name . '.php';
}
$this->options 可控,所以getCacheKey 返回的值完整可控。
$data = $this->serialize($value);
protected function serialize($data): string
{
    if (is_numeric($data)) {
        return (string) $data;
    }

    $serialize = $this->options['serialize'][0] ?? "\Opis\Closure\serialize";

    return $serialize($data);
}
$this->options['serialize'][0]可控,$serialize可控,$data 为我们传入set函数的$value,也就是$this->store->set($this->key, $contents, $this->expire); 中的$content,是可控的。只不过此时$data 经由json编码。 不难发明这里我们能够组织动态代码实行,测试下这个历程(当地试验是在windows下所以应用&或许||,linux下直接应用反引号即可。
<?php
$contents = ["test"=>"\"||dir||\""];

$cachedProperties = array_flip([
    'path', 'dirname', 'basename', 'extension', 'filename',
    'size', 'mimetype', 'visibility', 'timestamp', 'type',
]);

foreach ($contents as $path => $object) {
    if (is_array($object)) {
        $contents[$path] = array_intersect_key($object, $cachedProperties);
    }
}

$contents = json_encode($contents);
$options = ["system"];
$data = $contents;
var_dump($data);
$serialize = $options[0];
$serialize($data);
TP6.0反序列化应用链发掘思绪总结  安全技术 漏洞分析 第21张 梳理下思绪:
// vendor/league/flysystem-cached-adapter/src/Storage/AbstractCache.php
// abstract class AbstractCache 抽象类
// protected $cache = [];
// protected $complete = [];
public function __destruct()
{
    if (! $this->autosave) { //$this->autosave=false
        $this->save();
    }
}

// vendor/topthink/framework/src/think/filesystem/CacheStore.php
//use League\Flysystem\Cached\Storage\AbstractCache;
// class CacheStore
// protected $key;
// protected $expire;
public function save()
{
    $contents = $this->getForStorage();
    $this->store->set($this->key, $contents, $this->expire);
}//$this->store = new File();

// vendor/topthink/framework/src/think/cache/driver/File.php
// // use think\cache\Driver;
// class File extends Driver
public function set($name, $value, $expire = null): bool
{
$data = $this->serialize($value);
}
// vendor/topthink/framework/src/think/cache/Driver.php
// abstract class Driver
// protected $options = [];
protected function serialize($data): string
{
    $serialize = $this->options['serialize'][0];
    return $serialize($data);//敕令实行点
    }
还没完 ,继续剖析set 要领:
$data   = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;
$result = file_put_contents($filename, $data);
发明还存在一个恣意文件写入的点,只不过存在一个殒命exit,CTF中常见的一个点,应用p牛的php://filter协定的base64编码很轻松就可以绕过。前面提到过$filename可控,$data 也可控,所以能够getshell。

6.破绽应用

PS:这里只梳理触发的历程,防备不必要的贫苦,不放出POC,详细参数在剖析历程中都提到了。

应用链一

vendor/topthink/think-orm/src/Model.php 进口在Model 类的__destruct要领,然则此类为抽象类没法实例化,找到了它的子类Pivotvendor/topthink/think-orm/src/model/Pivot.php 以实例化Pivot类为出发点 然后给有关参数赋值,满足肯定前提层层触发: $this->save > $this->updateData > $this->checkAllowFields > $this->db()$this->db()中字符串拼接,触发__toString vendor/topthink/think-orm/src/model/concern/Conversion.php 触发了Conversion 类的__toStringConversion 类为Trait类,在Model 类中应用,只需赋值然后触发: $this->toJson > $this->toArray() 然后进入到Attribute类的 getAttr函数 vendor/topthink/think-orm/src/model/concern/Attribute.php 为Trait类,在Model 类中应用 getAttr > $this->getValue
$closure = $this->withAttr[$fieldName];
$value   = $closure($value, $this->data);
动态函数实行。

应用链二

vendor/league/flysystem-cached-adapter/src/Storage/AbstractCache.php 进口为AbstractCache类的__destruct 要领 该类为抽象类找到其子类CacheStore vendor/topthink/framework/src/think/filesystem/CacheStore.php 进入子类的$this->save 挪用恣意类的set函数:
$this->store->set($this->key, $contents, $this->expire);
挪用Filevendor/topthink/framework/src/think/cache/driver/File.php $this->serialize 然后敕令实行: vendor/topthink/framework/src/think/cache/Driver.php Driver类为抽象类,在File类 中有挪用
return $serialize($data);
实行敕令。

应用链三

前部分和应用链二一样,只是在末了getshell的要领差别,应用File类恣意文件写入shell
$data   = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;
$result = file_put_contents($filename, $data);

思绪总结

反序列化应用链基本发掘思绪

先找到进口文件,然后再层层跟进,找到代码实行点等风险操纵。 特别注重魔法函数、恣意类和函数的挪用、以及子类等的综合剖析 组织POC注重复用类和抽象类的问题: 发明类是Trait类,Trait类PHP 5.4.0入手下手引入的一种代码复用手艺,是为处理PHP单继续而预备的一种代码复用机制,没法经由历程 trait 本身来实例化,须要找到复用它的类来应用。 抽象类也不能实例化,须要找到子类一般类来实例化。 再就是ThinkPHP定名空间的问题: 定名空间基本能够参考php文档,参照文档很好明白三种援用体式格局,文档中将定名空间与文件体系作类比:
  1. 非限制称号(不包括前缀的类称号) 如 $a=new foo();foo::staticmethod();。假如当前定名空间是 currentnamespace,foo 将被剖析为 currentnamespace\foo。假如运用 foo 的代码是全局的,不包括在任何定名空间中的代码,则 foo 会被剖析为foo
  2. 限制称号 (包括前缀的称号) 如 $a = new subnamespace\foo();subnamespace\foo::staticmethod();。假如当前的定名空间是 currentnamespace,则 foo 会被剖析为 currentnamespace\subnamespace\foo。假如运用 foo 的代码是全局的,不包括在任何定名空间中的代码,foo 会被剖析为subnamespace\foo
  3. 完整限制称号(包括了全局前缀操纵符的称号) 如$a = new \currentnamespace\foo();\currentnamespace\foo::staticmethod();。在这类状况下,foo 老是被剖析为代码中的笔墨名(literal name)currentnamespace\foo
TinkPHP采纳定名空间,那末我们组织POC的时刻也应应用定名空间的要领挪用差别类和函数,组织POC就是在一个文件中定义多个定名空间,文档中也有申明。有两种体式格局:简朴组合语法和大括号语法 简朴组合语法:
<?php
namespace MyTest1;

class Test {}
function test() {}

namespace MyTest2;

class Test {}
function test() {}
?>
不引荐这类要领。 大括号语法:
<?php
namespace MyTest1{
    class Test {}
    function test() {}
}

namespace MyTest2{
    class Test {}
    function test() {}
}
?>
组织POC的末了还会用到全局非定名空间:
将全局的非定名空间中的代码与定名空间中的代码组合在一起,只能运用大括号情势的语法。全局代码必需用一个不带称号的 namespace 语句加上大括号括起来
<?php
namespace MyTest1{
    class Test {}
    function test() {}
}

namespace MyTest2{
    class Test {}
    function test() {}
}
namespace{
    $v = new MyTest2\Test();
    $s = new MyTest1\Test();
    $this -> xxx = $v;
    echo serialize($s);
}
?>
发掘应用链真好玩,phpstorm真香。

参考

发掘潜伏ThinkPHP中的反序列应用链 ThinkPHP6.X反序列化应用链 ThinkPHP 6.0.x反序列化(二)/) PHP手册-定名空间  

网友评论