0x00 ThinkPHP安裝

composer下載鏈接

https://getcomposer.org/doc/00-intro.md

安裝ThinkPHP6.0.13,需要本地PHP>7.2

composer create-project topthink/think tp

使用PHPStudy搭建安裝完成

0x01 反序列化漏洞分析

漏洞演示

斷點分析

首先反序列化第一步走的就是__destruct()魔術方法,通過全局搜索這個魔術方法,找到這里最有可能是反序列化的入口點。這個AbstractCache類是Psr6Cache的父類

public function \_\_destruct()
{
    if ($this->lazySave) {
        $this->save();
    }
}

接著$this->save就會跳轉到Psr6Cache類的save方法

public function save()
{
    $item = $this->pool->getItem($this->key);
    $item->set($this->getForStorage());
    $item->expiresAfter($this->expire);
    $this->pool->save($item);
}

這里在初始化的時候傳入與了一個$pool變量

$b = new think\log\Channel();

$a = new League\Flysystem\Cached\Storage\Psr6Cache($b);

第一步實例化了Psr6Cache對象,在初始化的時候傳入第一個參數,$this->pool

public function \_\_construct(CacheItemPoolInterface $pool, $key = 'flysystem', $expire = null)
{
    $this->pool = $pool;
    $this->key = $key;
    $this->expire = $expire;
}

在Psr6Cache類中,$this->pool->getItem調用時,出發了魔術方法_call,因為Channel對象中沒有getItem方法。此時也會執行構造方法

public function save()
{
    $item = $this->pool->getItem($this->key);
    $item->set($this->getForStorage());
    $item->expiresAfter($this->expire);
    $this->pool->save($item);
}

在Channel類中的_call方法,__call方法中又調用了$this->log方法

public function log($level, $message, array $context = \[\])
{
    $this->record($message, $level, $context);
}
public function \_\_call($method, $parameters)
{
    $this->log($method, ...$parameters);
}

接著跟蹤$this->record方法,這個方法是用來記錄日志信息的。這里最終會調用到$this->save方法

# 記錄日志信息

public function record($msg, string $type = 'info', array $context = [], bool $lazy = true)

{

if ($this->close || (!empty($this->allow) && !in_array($type, $this->allow))) {

return $this;

}

    if (is\_string($msg) && !empty($context)) {
        $replace = \[\];
        foreach ($context as $key => $val) {
            $replace\['{' . $key . '}'\] = $val;
        }
        $msg = strtr($msg, $replace);
    }
    if (!empty($msg) || 0 === $msg) {
        $this->log\[$type\]\[\] = $msg;
        if ($this->event) {
            $this->event->trigger(new LogRecord($type, $msg));
        }
    }
    if (!$this->lazy || !$lazy) {
        $this->save();
    }
    return $this;

來到save方法,save方法中又調用到了$this->logger->save()

這里在初始化的時候定義了$this->logger = new think\log\driver\Socket() ,所以在調用的時候會去往Socket類

/\*\*
 \* 保存日志
 \* @return bool
 \*/
public function save(): bool
{
    $log = $this->log;
    if ($this->event) {
        $event = new LogWrite($this->name, $log);
        $this->event->trigger($event);
        $log = $event->log;
    }
    if ($this->logger->save($log)) {
        $this->clear();
        return true;
    }
    return false;
}

來到think\log\driver\Socket()下的save方法

此時$this->config['format_head'] = [new \think\view\driver\Php,'display']

這里的$this->app->invoke是調用反射執行callable 支持參數綁定,進行動態反射調用

/\*\*
 \* 調試輸出接口
 \* @access public
 \* @param array $log 日志信息
 \* @return bool
 \*/
public function save(array $log = \[\]): bool
{
    if (!$this->check()) {
        return false;
    }
    $trace = \[\];
    if ($this->config\['debug'\]) {
        if ($this->app->exists('request')) {
            $currentUri = $this->app->request->url(true);
        } else {
            $currentUri = 'cmd:' . implode(' ', $\_SERVER\['argv'\] ?? \[\]);
        }
        if (!empty($this->config\['format\_head'\])) {
            try {
                $currentUri = $this->app->invoke($this->config\['format\_head'\], \[$currentUri\]);
            } catch (NotFoundExceptionInterface $notFoundException) {
                // Ignore exception
            }
        }
            ......

跟蹤$this->app->invoke

public function invoke($callable, array $vars = \[\], bool $accessible = false)
{
    if ($callable instanceof Closure) {
        return $this->invokeFunction($callable, $vars);
    } elseif (is\_string($callable) && false === strpos($callable, '::')) {
        return $this->invokeFunction($callable, $vars);
    } else {
        return $this->invokeMethod($callable, $vars, $accessible);
    }
}

這里的判斷循環最終會進入到return $reflect->invokeArgs(is_object($class) ? $class : null, $args);

通過這里的反射方法來到Php類下的display方法,$calss是一個對象類,$args就是對象類下的方法

public function invokeMethod($method, array $vars = \[\], bool $accessible = false)
{
    //$method = think\\view\\driver\\Php
    if (is\_array($method)) {
        \[$class, $method\] = $method;
        $class = is\_object($class) ? $class : $this->invokeClass($class);
    } else {
        // 靜態方法
        \[$class, $method\] = explode('::', $method);
    }
    try {
        //ReflectionMethod回調方法
        $reflect = new ReflectionMethod($class, $method);
    } catch (ReflectionException $e) {
        $class = is\_object($class) ? get\_class($class) : $class;
        throw new FuncNotFoundException('method not exists: ' . $class . '::' . $method . '()', "{$class}::{$method}", $e);
    }
    $args = $this->bindParams($reflect, $vars);
    if ($accessible) {
        $reflect->setAccessible($accessible);
    }
    return $reflect->invokeArgs(is\_object($class) ? $class : null, $args);
}

官方文檔

此時調用方法的參數

輸出一個回調方法,$calss是類名,$method是方法

$reflect = newReflectionMethod($class, $method);

通過$this->invokeArgs方法將參數傳遞給類下的方法

$reflect->invokeArgs(is_object($class) ? $class : null, $args);

通過圖可以看出值傳遞的過程

來到display方法中,$content就是傳遞的值,然后拼接到了eval去執行命令

public function display(string $content, array $data = \[\]): void
{
    $this->content = $content;
    extract($data, EXTR\_OVERWRITE);
    eval('?>' . $this->content);
}