ThinkPHP5代码执行的简单分析
漏洞影响版本:
- ThinkPHP 5.0.5-5.0.22
- ThinkPHP 5.1.0-5.1.30
漏洞复现:
一.mac的debug环境搭建。
一键化环境搭建工具: mamp pro ,调试工具 PHPstorm
打开mamp pro,设置左上角的file->Edit Template, 设置httpd.conf (监听本地)
ServerName 127.0.0.1:8087
Listen 127.0.0.1:8087
打开mamp pro,设置左上角的file->Edit Template,设置PHP.ini 选择你的PHP版本
zend_extension="/Applications/MAMP/bin/php/php7.2.10/lib/php/extensions/no-debug-non-zts-20170718/xdebug.so"
xdebug.idekey=PHPSTORM
xdebug.remote_connect_back =
xdebug.remote_enable=on
xdebug.remote_port =
xdebug.remote_handler = dbgp
xdebug.auto_trace =
xdebug.remote_log = /tmp/xdebug.log
其余的在PHPstorm上设置,设置完不行尝试加上XDEBUG_SESSION_START=xxxx ,xxxx为你debug开启的等待的key


最后,环境搭的头疼。
--------
poc: http://127.0.0.1:8087/tp5/public/index.php?s=index/\think\template\driver\file/read&cacheFile=/etc/passwd
先贴上调用栈
File.php:51, think\template\driver\File->read()
Container.php:395, ReflectionMethod->invokeArgs() //反射调用
Container.php:395, think\App->invokeReflectMethod()
Module.php:135, think\route\dispatch\Module->think\route\dispatch\{closure}()
Middleware.php:186, call_user_func_array:{/thinkphp/library/think/Middleware.php:186}()
Middleware.php:186, think\Middleware->think\{closure}()
Middleware.php:130, call_user_func:{/thinkphp/library/think/Middleware.php:130}()
Middleware.php:130, think\Middleware->dispatch()
Module.php:140, think\route\dispatch\Module->exec()
Dispatch.php:168, think\route\dispatch\Module->run()
App.php:432, think\App->think\{closure}()
Middleware.php:186, call_user_func_array:{/thinkphp/library/think/Middleware.php:186}()
Middleware.php:186, think\Middleware->think\{closure}()
Middleware.php:130, call_user_func:{/thinkphp/library/think/Middleware.php:130}()
Middleware.php:130, think\Middleware->dispatch()
App.php:435, think\App->run()
index.php:21, {main}()
最开始进入/tp5/public/index.php
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006-2018 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +---------------------------------------------------------------------- // [ 应用入口文件 ]
namespace think; // 加载基础文件
require __DIR__ . '/../thinkphp/base.php'; // 支持事先使用静态方法设置Request对象和Config对象 // 执行应用并响应
Container::get('app')->run()->send();
加载基础文件,调用app应用,调用run()方法,
App.php:375, think\App->run(),在run方法中会对路由进行检测
App.php:402, $dispatch = $this->routeCheck()->init();
routeCheck()中会去执行pathinfo()方法,取$_GET['s']里面的值
public function pathinfo()
{
if (is_null($this->pathinfo)) {
if (isset($_GET[$this->config['var_pathinfo']])) {
// 判断URL里面是否有兼容模式参数
$pathinfo = $_GET[$this->config['var_pathinfo']];
unset($_GET[$this->config['var_pathinfo']]);
} elseif ($this->isCli()) {
// CLI模式下 index.php module/controller/action/params/...
$pathinfo = isset($_SERVER['argv'][1]) ? $_SERVER['argv'][1] : '';
} elseif ('cli-server' == PHP_SAPI) {
$pathinfo = strpos($this->server('REQUEST_URI'), '?') ? strstr($this->server('REQUEST_URI'), '?', true) : $this->server('REQUEST_URI');
} elseif ($this->server('PATH_INFO')) {
$pathinfo = $this->server('PATH_INFO');
} // 分析PATHINFO信息
if (!isset($pathinfo)) {
foreach ($this->config['pathinfo_fetch'] as $type) {
if ($this->server($type)) {
$pathinfo = (0 === strpos($this->server($type), $this->server('SCRIPT_NAME'))) ?
substr($this->server($type), strlen($this->server('SCRIPT_NAME'))) : $this->server($type);
break;
}
}
} $this->pathinfo = empty($pathinfo) || '/' == $pathinfo ? '' : ltrim($pathinfo, '/');
} return $this->pathinfo;
}
从$_GET['s']中取参
App.php:583, think\App->routeCheck() $dispatch = $this->route->check($path, $must); // 返回一个Url对象, index|\think\template\driver\file|read return new UrlDispatch($this->request, $this->group, $url, [ 'auto_search' => $this->autoSearchController, ]);

在URL类中没找到含4个参数的构造函数,调用父类Dispatch的构造函数。


接着再初始化Url对象的init();方法。
第一步解析默认的URL规则。调用parseUrl($this->dispatch) ,返回URL规则
public function init()
{
// 解析默认的URL规则
$result = $this->parseUrl($this->dispatch); return (new Module($this->request, $this->rule, $result))->init();
}
protected function parseUrl($url)
{
$depr = $this->rule->getConfig('pathinfo_depr');
$bind = $this->rule->getRouter()->getBind(); if (!empty($bind) && preg_match('/^[a-z]/is', $bind)) {
$bind = str_replace('/', $depr, $bind);
// 如果有模块/控制器绑定
$url = $bind . ('.' != substr($bind, -1) ? $depr : '') . ltrim($url, $depr);
} list($path, $var) = $this->rule->parseUrlPath($url);
if (empty($path)) {
使用"/"进行分割,拿到 [模块/控制器/操作]
public function parseUrlPath($url)
{
....
....
} elseif (strpos($url, '/')) {
// [模块/控制器/操作]
$path = explode('/', $url);
} elseif (false !== strpos($url, '=')) {
// 参数1=值1&参数2=值2...
$path = [];
parse_str($url, $var);
} else {
$path = [$url];
} return [$path, $var];
}
从$result = $this->parseUrl($this->dispatch); 拿到封装好的路由规则。
接着往回看return (new Module($this->request, $this->rule, $result))->init();
public function init()
{
// 解析默认的URL规则
$result = $this->parseUrl($this->dispatch); return (new Module($this->request, $this->rule, $result))->init();
}
在Module类中没有构造函数,在调用Dispatch父类的构造函数,在调用Module类中init()函数。将转换控制器和操作名赋值给$this,也转换控制器和操作名封装到request里面,返回当前类
public function init()
{
parent::init();
$result = $this->dispatch;
if ($this->rule->getConfig('app_multi_module')) {
// 多模块部署
$module = strip_tags(strtolower($result[0] ?: $this->rule->getConfig('default_module')));
...
...
} elseif (!in_array($module, $this->rule->getConfig('deny_module_list')) && is_dir($this->app->getAppPath() . $module)) {
$available = true;
}
...
...
// 模块初始化
if ($module && $available) {
// 初始化模块
$this->request->setModule($module);
$this->app->init($module);
} else {
throw new HttpException(404, 'module not exists:' . $module);
}
}
// 获取控制器名
$controller = strip_tags($result[1] ?: $this->rule->getConfig('default_controller'));
$this->controller = $convert ? strtolower($controller) : $controller;
// 获取操作名
$this->actionName = strip_tags($result[2] ?: $this->rule->getConfig('default_action'));
// 设置当前请求的控制器、操作
$this->request
->setController(Loader::parseName($this->controller, 1))
->setAction($this->actionName);
return $this;
}
引用启明的分析:
这里存在第一个对$module的判断,需要让$available等于true,这就需要is_dir($this->app->getAppPath() . $module)成立。官方demo给出的模块是index,而实际开发程序不一定存在该模块名,
所以构造payload时这里是一个注意点。
在回到最开始的app模块
public function run(){
.....
$this->middleware->add(function (Request $request, $next) use ($dispatch, $data) {
return is_null($data) ? $dispatch->run() : $data;
});
$response = $this->middleware->dispatch($this->request);
.....
}
创建一个闭包函数,然后执行$this->middleware->dispatch($this->request);
public function dispatch(Request $request, $type = 'route')
{
return call_user_func($this->resolve($type), $request);
}
使用call_user_func回调函数,将$request作为参数传进resolve,\think\Middleware::resolve
protected function resolve($type = 'route')
{
return function (Request $request) use ($type) { $middleware = array_shift($this->queue[$type]); if (null === $middleware) {
throw new InvalidArgumentException('The queue was exhausted, with no response returned');
} list($call, $param) = $middleware; try {
//TODO此处的参数要在看一下
$response = call_user_func_array($call, [$request, $this->resolve($type), $param]);
} catch (HttpResponseException $exception) {
$response = $exception->getResponse();
} if (!$response instanceof Response) {
throw new LogicException('The middleware must return Response instance');
} return $response;
};
}
进入到call_user_func_array() ,继续回调,将[$request, $this->resolve($type), $param]作为参数传进去。
这里的$call参数是个闭包函数,会调用之前app模块的闭包函数。在app.php:431
#app.php:431
function (Request $request, $next) use ($dispatch, $data) {
return is_null($data) ? $dispatch->run() : $data;
}
Dispatch.php:168, think\route\dispatch\Module->run()
App.php:432, think\App->think\{closure}()
public function run()
{
$option = $this->rule->getOption(); // 检测路由after行为
if (!empty($option['after'])) {
$dispatch = $this->checkAfter($option['after']); if ($dispatch instanceof Response) {
return $dispatch;
}
} // 数据自动验证
if (isset($option['validate'])) {
$this->autoValidate($option['validate']);
} $data = $this->exec(); return $this->autoResponse($data);
}
这时候会执行$data = $this->exec();
public function exec()
{
// 监听module_init
$this->app['hook']->listen('module_init'); try {
// 实例化控制器
$instance = $this->app->controller($this->controller,
$this->rule->getConfig('url_controller_layer'),
$this->rule->getConfig('controller_suffix'),
$this->rule->getConfig('empty_controller')); if ($instance instanceof Controller) {
$instance->registerMiddleware();
}
} catch (ClassNotFoundException $e) {
throw new HttpException(, 'controller not exists:' . $e->getClass());
}
.....
return $this->app['middleware']->dispatch($this->request, 'controller');
这里有看到了熟悉的$this->app['middleware']->dispatch($this->request, 'controller');
只不过这里不再是route,而是controller,这里的controller将会再次调用exec()函数里面的闭包函数controller
$this->app['middleware']->controller(function (Request $request, $next) use ($instance) {
// 获取当前操作名
$action = $this->actionName . $this->rule->getConfig('action_suffix');
if (is_callable([$instance, $action])) {
// 执行操作方法
$call = [$instance, $action];
// 严格获取当前操作方法名
$reflect = new ReflectionMethod($instance, $action);
$methodName = $reflect->getName();
$suffix = $this->rule->getConfig('action_suffix');
$actionName = $suffix ? substr($methodName, 0, -strlen($suffix)) : $methodName;
$this->request->setAction($actionName);
// 自动获取请求变量
$vars = $this->rule->getConfig('url_param_type')
? $this->request->route()
: $this->request->param();
$vars = array_merge($vars, $this->param);
} elseif (is_callable([$instance, '_empty'])) {
// 空操作
$call = [$instance, '_empty'];
$vars = [$this->actionName];
$reflect = new ReflectionMethod($instance, '_empty');
} else {
// 操作不存在
throw new HttpException(404, 'method not exists:' . get_class($instance) . '->' . $action . '()');
}
$this->app['hook']->listen('action_begin', $call);
$data = $this->app->invokeReflectMethod($instance, $reflect, $vars);
return $this->autoResponse($data);
});
通过闭包函数controller()进行反射,跟进invokeReflectMethod
$data = $this->app->invokeReflectMethod($instance, $reflect, $vars);
public function invokeReflectMethod($instance, $reflect, $vars = [])
{
$args = $this->bindParams($reflect, $vars); return $reflect->invokeArgs($instance, $args);
}

最后就调用传入的方法和参数,进行反射。


至此,简单分析完了,学习到了使用简单闭包的方法。
官方修复方式:连接
添加如下正则,对控制器进行判断。只允许a-zA-Z.这样的字符通过
if (!preg_match('/^[A-Za-z](\w)*$/', $controller)) {
throw new HttpException(404, 'controller not exists:' . $controller);
}
参考来源:
https://paper.seebug.org/760/
https://laravel-china.org/articles/5388/closures-and-anonymous-functions-of-php-new-features
https://github.com/top-think/framework/commit/adde39c236cfeda454fe725d999d89abf67b8caf
ThinkPHP5代码执行的简单分析的更多相关文章
- thinkphp 2.1代码执行及路由分析
Dispatcher.class.php这个文件中是url路由,由于第一次正式看路由那块,所以就从头开始一行一行看把. 首先是dispatch函数 是37行到140行 这个函数是做映射用,把url映射 ...
- ECShop全系列版本远程代码执行高危漏洞分析+实战提权
漏洞概述 ECShop的user.php文件中的display函数的模版变量可控,导致注入,配合注入可达到远程代码执行.攻击者无需登录站点等操作,可以直接远程写入webshell,危害严重. 漏洞评级 ...
- Apache Log4j 反序列化代码执行(CVE-2019-17571) 漏洞分析
Apache Log4j 漏洞分析 仅用于研究漏洞原理,禁止用于非法用途,后果自负!!! CVE-2019-17571 漏洞描述 Log4j是美国阿帕奇(Apache)软件基金会的一款基于Java的开 ...
- Shiro RememberMe 1.2.4远程代码执行漏洞-详细分析
本文首发于先知: https://xz.aliyun.com/t/6493 0x01.漏洞复现 环境配置 https://github.com/Medicean/VulApps/tree/master ...
- hive执行计划简单分析
原始SQL: select a2.ISSUE_CODE as ISSUE_CODE, a2.FZQDM as FZQDM, a2.FZQLB as FZQLB, a2.FJJDM as FJJDM, ...
- thinkphp5.0.22远程代码执行漏洞分析及复现
虽然网上已经有几篇公开的漏洞分析文章,但都是针对5.1版本的,而且看起来都比较抽象:我没有深入分析5.1版本,但看了下网上分析5.1版本漏洞的文章,发现虽然POC都是一样的,但它们的漏洞触发原因是不同 ...
- Ecshop 2.x_3.x SQL注入和代码执行漏洞复现和分析
0x00 前言 问题发生在user.php的的显示函数,模版变量可控,导致注入,配合注入可达到远程代码执行 0x01 漏洞分析 1.SQL注入 先看user.php的$ back_act变量来源于HT ...
- 干货|CVE-2019-11043: PHP-FPM在Nginx特定配置下任意代码执行漏洞分析
近期,国外安全研究员Andrew Danau,在参加夺旗赛(CTF: Capture the Flag)期间,偶然发现php-fpm组件处理特定请求时存在缺陷:在特定Nginx配置下,特定构造的请求会 ...
- Spring框架的反序列化远程代码执行漏洞分析(转)
欢迎和大家交流技术相关问题: 邮箱: jiangxinnju@163.com 博客园地址: http://www.cnblogs.com/jiangxinnju GitHub地址: https://g ...
随机推荐
- php-fpm超时时间设置request_terminate_timeout分析
之前发现一个php配置之后关于返回500和502的问题,今天看到一个兄弟写的非常不错,记录一下. php日志中有一条超时的日志,但是我request_terminate_timeout中设置的是0 ...
- 使用docker 安装 GITLIB
在安装 gitlib 社区版时,配置老不成功,改成使用docker安装 比较顺利,省事. 1外部卷配置 docker 需要配置一些卷在外部,创建一下git的目录 我们创建一个在home下 创建一个gi ...
- hdu6365 2018 Multi-University Training Contest 6 1004 Shoot Game
http://acm.hdu.edu.cn/showproblem.php?pid=6365 细节处理 unique返回的是最后一位的后一位,因此从1开始的数组要减去(p+1) 结构体可以用unqiu ...
- 【转载】Impala和Hive的区别
Impala和Hive的关系 Impala是基于Hive的大数据实时分析查询引擎,直接使用Hive的元数据库Metadata,意味着impala元数据都存储在Hive的metastore中.并且im ...
- Mysql中Left Join Right Join Inner Join where条件的比较
建立一对多的表 company 和 employee company表 id name address 1baidu北京 2huawei深圳 3jingdong北京 4tengxu ...
- embeded_2_separate_sync
//如果是8位的话,只选择低8位传输 //因为同步码也是可以自己设置,所以把同步码设置成parameter最好 module embeded_2_separate_sync( input clk, : ...
- [转]两表join的multi update语句在MySQL中的执行流程分析
出自:http://hedengcheng.com/?p=209 两表join的multi update语句,执行结果与预计不一致的分析过程 — multi update结论在实际应用中,不要轻易使用 ...
- java混淆代码的使用
前言:为了保护我们的劳动成果,我们来学习java混淆代码工具的使用. 1.下载retroguard.jar 进入http://www.retrologic.com/retroguard-downloa ...
- Redis基于eval的多字段原子增量计算
目录 目录 1 1. 前言 1 2. 优点 1 3. 方法一:使用struct 2 3.1. 设置初始值(覆盖原有的,如果存在) 2 3.2. 查询k1的值 2 3.3. 设置初始值(覆盖原有的,如果 ...
- UIKit-UIBezierPath
UIBezierPath精讲 http://www.henishuo.com/uibezierpath-draw/ iOS UIBezierPath类 介绍 http://justsee.iteye. ...