漏洞影响版本:

  • 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版本 

  1. zend_extension="/Applications/MAMP/bin/php/php7.2.10/lib/php/extensions/no-debug-non-zts-20170718/xdebug.so"
  2. xdebug.idekey=PHPSTORM
  3. xdebug.remote_connect_back =
  4. xdebug.remote_enable=on
  5. xdebug.remote_port =
  6. xdebug.remote_handler = dbgp
  7. xdebug.auto_trace =
  8. 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

先贴上调用栈

  1. File.php:51, think\template\driver\File->read()
  2. Container.php:395, ReflectionMethod->invokeArgs() //反射调用
  3. Container.php:395, think\App->invokeReflectMethod()
  4. Module.php:135, think\route\dispatch\Module->think\route\dispatch\{closure}()
  5. Middleware.php:186, call_user_func_array:{/thinkphp/library/think/Middleware.php:186}()
  6. Middleware.php:186, think\Middleware->think\{closure}()
  7. Middleware.php:130, call_user_func:{/thinkphp/library/think/Middleware.php:130}()
  8. Middleware.php:130, think\Middleware->dispatch()
  9. Module.php:140, think\route\dispatch\Module->exec()
  10. Dispatch.php:168, think\route\dispatch\Module->run()
  11. App.php:432, think\App->think\{closure}()
  12. Middleware.php:186, call_user_func_array:{/thinkphp/library/think/Middleware.php:186}()
  13. Middleware.php:186, think\Middleware->think\{closure}()
  14. Middleware.php:130, call_user_func:{/thinkphp/library/think/Middleware.php:130}()
  15. Middleware.php:130, think\Middleware->dispatch()
  16. App.php:435, think\App->run()
  17. index.php:21, {main}()

  

最开始进入/tp5/public/index.php

  1. <?php
  2. // +----------------------------------------------------------------------
  3. // | ThinkPHP [ WE CAN DO IT JUST THINK ]
  4. // +----------------------------------------------------------------------
  5. // | Copyright (c) 2006-2018 http://thinkphp.cn All rights reserved.
  6. // +----------------------------------------------------------------------
  7. // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
  8. // +----------------------------------------------------------------------
  9. // | Author: liu21st <liu21st@gmail.com>
  10. // +----------------------------------------------------------------------
  11.  
  12. // [ 应用入口文件 ]
  13. namespace think;
  14.  
  15. // 加载基础文件
  16. require __DIR__ . '/../thinkphp/base.php';
  17.  
  18. // 支持事先使用静态方法设置Request对象和Config对象
  19.  
  20. // 执行应用并响应
  21. Container::get('app')->run()->send();

  

加载基础文件,调用app应用,调用run()方法,

  1. App.php:375, think\App->run(),在run方法中会对路由进行检测
  1. App.php:402 $dispatch = $this->routeCheck()->init();
  1. routeCheck()中会去执行pathinfo()方法,取$_GET['s']里面的值
  1. public function pathinfo()
  2. {
  3. if (is_null($this->pathinfo)) {
  4. if (isset($_GET[$this->config['var_pathinfo']])) {
  5. // 判断URL里面是否有兼容模式参数
  6. $pathinfo = $_GET[$this->config['var_pathinfo']];
  7. unset($_GET[$this->config['var_pathinfo']]);
  8. } elseif ($this->isCli()) {
  9. // CLI模式下 index.php module/controller/action/params/...
  10. $pathinfo = isset($_SERVER['argv'][1]) ? $_SERVER['argv'][1] : '';
  11. } elseif ('cli-server' == PHP_SAPI) {
  12. $pathinfo = strpos($this->server('REQUEST_URI'), '?') ? strstr($this->server('REQUEST_URI'), '?', true) : $this->server('REQUEST_URI');
  13. } elseif ($this->server('PATH_INFO')) {
  14. $pathinfo = $this->server('PATH_INFO');
  15. }
  16.  
  17. // 分析PATHINFO信息
  18. if (!isset($pathinfo)) {
  19. foreach ($this->config['pathinfo_fetch'] as $type) {
  20. if ($this->server($type)) {
  21. $pathinfo = (0 === strpos($this->server($type), $this->server('SCRIPT_NAME'))) ?
  22. substr($this->server($type), strlen($this->server('SCRIPT_NAME'))) : $this->server($type);
  23. break;
  24. }
  25. }
  26. }
  27.  
  28. $this->pathinfo = empty($pathinfo) || '/' == $pathinfo ? '' : ltrim($pathinfo, '/');
  29. }
  30.  
  31. return $this->pathinfo;
  32. }

  从$_GET['s']中取参

  1. App.php:583, think\App->routeCheck() $dispatch = $this->route->check($path, $must); // 返回一个Url对象, index|\think\template\driver\file|read
  2.  
  3. return new UrlDispatch($this->request, $this->group, $url, [ 'auto_search' => $this->autoSearchController, ]);

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

  1.  

  1. 接着再初始化Url对象的init();方法。

第一步解析默认的URL规则。调用parseUrl($this->dispatch) ,返回URL规则

  1. public function init()
  2. {
  3. // 解析默认的URL规则
  4. $result = $this->parseUrl($this->dispatch);
  5.  
  6. return (new Module($this->request, $this->rule, $result))->init();
  7. }

  

  1. protected function parseUrl($url)
  2. {
  3. $depr = $this->rule->getConfig('pathinfo_depr');
  4. $bind = $this->rule->getRouter()->getBind();
  5.  
  6. if (!empty($bind) && preg_match('/^[a-z]/is', $bind)) {
  7. $bind = str_replace('/', $depr, $bind);
  8. // 如果有模块/控制器绑定
  9. $url = $bind . ('.' != substr($bind, -1) ? $depr : '') . ltrim($url, $depr);
  10. }
  11.  
  12. list($path, $var) = $this->rule->parseUrlPath($url);
  13. if (empty($path)) {

  使用"/"进行分割,拿到 [模块/控制器/操作]

  1. public function parseUrlPath($url)
  2. {
  3. ....
  4. ....
  5. } elseif (strpos($url, '/')) {
  6. // [模块/控制器/操作]
  7. $path = explode('/', $url);
  8. } elseif (false !== strpos($url, '=')) {
  9. // 参数1=值1&参数2=值2...
  10. $path = [];
  11. parse_str($url, $var);
  12. } else {
  13. $path = [$url];
  14. }
  15.  
  16. return [$path, $var];
  17. }

  

从$result = $this->parseUrl($this->dispatch); 拿到封装好的路由规则。

接着往回看return (new Module($this->request, $this->rule, $result))->init();

  1. public function init()
  2. {
  3. // 解析默认的URL规则
  4. $result = $this->parseUrl($this->dispatch);
  5.  
  6. return (new Module($this->request, $this->rule, $result))->init();
  7. }

在Module类中没有构造函数,在调用Dispatch父类的构造函数,在调用Module类中init()函数。将转换控制器和操作名赋值给$this,也转换控制器和操作名封装到request里面,返回当前类

  1. public function init()
  2. {
  3. parent::init();
  4. $result = $this->dispatch;
  5. if ($this->rule->getConfig('app_multi_module')) {
  6. // 多模块部署
  7. $module = strip_tags(strtolower($result[0] ?: $this->rule->getConfig('default_module')));
  8. ...
  9. ...
  10. } elseif (!in_array($module, $this->rule->getConfig('deny_module_list')) && is_dir($this->app->getAppPath() . $module)) {
  11. $available = true;
  12. }
  13. ...
  14. ...
  15. // 模块初始化
  16. if ($module && $available) {
  17. // 初始化模块
  18. $this->request->setModule($module);
  19. $this->app->init($module);
  20. } else {
  21. throw new HttpException(404, 'module not exists:' . $module);
  22. }
  23. }
  24. // 获取控制器名
  25. $controller = strip_tags($result[1] ?: $this->rule->getConfig('default_controller'));
  26. $this->controller = $convert ? strtolower($controller) : $controller;
  27. // 获取操作名
  28. $this->actionName = strip_tags($result[2] ?: $this->rule->getConfig('default_action'));
  29. // 设置当前请求的控制器、操作
  30. $this->request
  31. ->setController(Loader::parseName($this->controller, 1))
  32. ->setAction($this->actionName);
  33. return $this;
  34. }

  

引用启明的分析:

  1. 这里存在第一个对$module的判断,需要让$available等于true,这就需要is_dir($this->app->getAppPath() . $module)成立。官方demo给出的模块是index,而实际开发程序不一定存在该模块名,
    所以构造payload时这里是一个注意点。 

在回到最开始的app模块

  1. public function run(){
    .....
  2. $this->middleware->add(function (Request $request, $next) use ($dispatch, $data) {
  3. return is_null($data) ? $dispatch->run() : $data;
  4. });
  5.  
  6. $response = $this->middleware->dispatch($this->request);
  7. .....
  8. }

  

创建一个闭包函数,然后执行$this->middleware->dispatch($this->request);

  1. public function dispatch(Request $request, $type = 'route')
  2. {
  3. return call_user_func($this->resolve($type), $request);
  4. }

  

使用call_user_func回调函数,将$request作为参数传进resolve,\think\Middleware::resolve

  1. protected function resolve($type = 'route')
  2. {
  3. return function (Request $request) use ($type) {
  4.  
  5. $middleware = array_shift($this->queue[$type]);
  6.  
  7. if (null === $middleware) {
  8. throw new InvalidArgumentException('The queue was exhausted, with no response returned');
  9. }
  10.  
  11. list($call, $param) = $middleware;
  12.  
  13. try {
  14. //TODO此处的参数要在看一下
  15. $response = call_user_func_array($call, [$request, $this->resolve($type), $param]);
  16. } catch (HttpResponseException $exception) {
  17. $response = $exception->getResponse();
  18. }
  19.  
  20. if (!$response instanceof Response) {
  21. throw new LogicException('The middleware must return Response instance');
  22. }
  23.  
  24. return $response;
  25. };
  26. }

  

进入到call_user_func_array() ,继续回调,将[$request, $this->resolve($type), $param]作为参数传进去。

这里的$call参数是个闭包函数,会调用之前app模块的闭包函数。在app.php:431

  1. #app.php:431
    function (Request $request, $next) use ($dispatch, $data) {
  2. return is_null($data) ? $dispatch->run() : $data;
  3. }

Dispatch.php:168, think\route\dispatch\Module->run()
App.php:432, think\App->think\{closure}()

  1. public function run()
  2. {
  3. $option = $this->rule->getOption();
  4.  
  5. // 检测路由after行为
  6. if (!empty($option['after'])) {
  7. $dispatch = $this->checkAfter($option['after']);
  8.  
  9. if ($dispatch instanceof Response) {
  10. return $dispatch;
  11. }
  12. }
  13.  
  14. // 数据自动验证
  15. if (isset($option['validate'])) {
  16. $this->autoValidate($option['validate']);
  17. }
  18.  
  19. $data = $this->exec();
  20.  
  21. return $this->autoResponse($data);
  22. }

  

这时候会执行$data = $this->exec();

  1. public function exec()
  2. {
  3. // 监听module_init
  4. $this->app['hook']->listen('module_init');
  5.  
  6. try {
  7. // 实例化控制器
  8. $instance = $this->app->controller($this->controller,
  9. $this->rule->getConfig('url_controller_layer'),
  10. $this->rule->getConfig('controller_suffix'),
  11. $this->rule->getConfig('empty_controller'));
  12.  
  13. if ($instance instanceof Controller) {
  14. $instance->registerMiddleware();
  15. }
  16. } catch (ClassNotFoundException $e) {
  17. throw new HttpException(, 'controller not exists:' . $e->getClass());
  18. }
    .....
  1. return $this->app['middleware']->dispatch($this->request, 'controller');

这里有看到了熟悉的$this->app['middleware']->dispatch($this->request, 'controller');

只不过这里不再是route,而是controller,这里的controller将会再次调用exec()函数里面的闭包函数controller

  1. $this->app['middleware']->controller(function (Request $request, $next) use ($instance) {
    // 获取当前操作名
    $action = $this->actionName . $this->rule->getConfig('action_suffix');
  2.  
  3. if (is_callable([$instance, $action])) {
    // 执行操作方法
    $call = [$instance, $action];
  4.  
  5. // 严格获取当前操作方法名
    $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);
  6.  
  7. // 自动获取请求变量
    $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 . '()');
    }
  8.  
  9. $this->app['hook']->listen('action_begin', $call);
  10.  
  11. $data = $this->app->invokeReflectMethod($instance, $reflect, $vars);
  12.  
  13. return $this->autoResponse($data);
    });
  1.   

通过闭包函数controller()进行反射,跟进invokeReflectMethod

  1. $data = $this->app->invokeReflectMethod($instance, $reflect, $vars);
  1. public function invokeReflectMethod($instance, $reflect, $vars = [])
    {
    $args = $this->bindParams($reflect, $vars);
  2.  
  3. return $reflect->invokeArgs($instance, $args);
    }

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

至此,简单分析完了,学习到了使用简单闭包的方法。

官方修复方式:连接

添加如下正则,对控制器进行判断。只允许a-zA-Z.这样的字符通过

  1. if (!preg_match('/^[A-Za-z](\w)*$/', $controller)) {
  2. throw new HttpException(404, 'controller not exists:' . $controller);
  3. }

参考来源:

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代码执行的简单分析的更多相关文章

  1. thinkphp 2.1代码执行及路由分析

    Dispatcher.class.php这个文件中是url路由,由于第一次正式看路由那块,所以就从头开始一行一行看把. 首先是dispatch函数 是37行到140行 这个函数是做映射用,把url映射 ...

  2. ECShop全系列版本远程代码执行高危漏洞分析+实战提权

    漏洞概述 ECShop的user.php文件中的display函数的模版变量可控,导致注入,配合注入可达到远程代码执行.攻击者无需登录站点等操作,可以直接远程写入webshell,危害严重. 漏洞评级 ...

  3. Apache Log4j 反序列化代码执行(CVE-2019-17571) 漏洞分析

    Apache Log4j 漏洞分析 仅用于研究漏洞原理,禁止用于非法用途,后果自负!!! CVE-2019-17571 漏洞描述 Log4j是美国阿帕奇(Apache)软件基金会的一款基于Java的开 ...

  4. Shiro RememberMe 1.2.4远程代码执行漏洞-详细分析

    本文首发于先知: https://xz.aliyun.com/t/6493 0x01.漏洞复现 环境配置 https://github.com/Medicean/VulApps/tree/master ...

  5. hive执行计划简单分析

    原始SQL: select a2.ISSUE_CODE as ISSUE_CODE, a2.FZQDM as FZQDM, a2.FZQLB as FZQLB, a2.FJJDM as FJJDM, ...

  6. thinkphp5.0.22远程代码执行漏洞分析及复现

    虽然网上已经有几篇公开的漏洞分析文章,但都是针对5.1版本的,而且看起来都比较抽象:我没有深入分析5.1版本,但看了下网上分析5.1版本漏洞的文章,发现虽然POC都是一样的,但它们的漏洞触发原因是不同 ...

  7. Ecshop 2.x_3.x SQL注入和代码执行漏洞复现和分析

    0x00 前言 问题发生在user.php的的显示函数,模版变量可控,导致注入,配合注入可达到远程代码执行 0x01 漏洞分析 1.SQL注入 先看user.php的$ back_act变量来源于HT ...

  8. 干货|CVE-2019-11043: PHP-FPM在Nginx特定配置下任意代码执行漏洞分析

    近期,国外安全研究员Andrew Danau,在参加夺旗赛(CTF: Capture the Flag)期间,偶然发现php-fpm组件处理特定请求时存在缺陷:在特定Nginx配置下,特定构造的请求会 ...

  9. Spring框架的反序列化远程代码执行漏洞分析(转)

    欢迎和大家交流技术相关问题: 邮箱: jiangxinnju@163.com 博客园地址: http://www.cnblogs.com/jiangxinnju GitHub地址: https://g ...

随机推荐

  1. vue实现左侧滑动删除

    不是很完美,无法做到第一个左滑其他的隐藏删除: 代码来源于 https://segmentfault.com/a/1190000011062124 自己做了写改动,添加父组件点击触发子组件 引入组件 ...

  2. floor函数

    C++中 可以用floor函数来截断小数部分 floor(x)返回一个不大于x的整数,有点像取整函数

  3. jquery选择树:CheckTree 插件

    在做权限等提交的时候,需要用到选择树.比较了很多插件如:easyUI 树,ztree等等.本来决定自己写,但是到最后还是发现了checkTree这个插件.省了很多事情. 但是在引用过程中还是出现了一些 ...

  4. BUG YII2.0 $ is not defined

    来源:https://www.cnblogs.com/attitudeY/p/6279985.html BUG描述:$ is not defined 没有加载jquery成功 原因:Yii2.0将JS ...

  5. Le Chapitre IX

    Je crois qu'il profita, pour son évasion[evazjɔ̃]逃跑, d'une migration d'oiseaux sauvages[sovaʒ]未驯化的. ...

  6. #pragma warning(disable 4786)

    #pragma warning(disable 4786) 此warning产生的原因是因为标识符过长,超过了最大限定255个字符类名超过了255个字符,使用时就会报4786的waring. 在使用S ...

  7. mui框架如何实现页面间传值

    mui框架如何实现页面间传值 我的传值 listDetail = '<li class="mui-table-view-cell mui-media>">< ...

  8. How to fix "FAILURE DURING CONVERSION TO COFF: FILE INVALID OR CORRUPT"

    Error LINK : fatal error LNK1123: failure during conversion to COFF: file invalid or corrupt appear ...

  9. java实现把两张图片合并(Graphics2D)

    package com.yin.text; import java.awt.Graphics2D; import java.awt.image.BufferedImage; import java.i ...

  10. Codeforces Round #540 (Div. 3)--1118D1 - Coffee and Coursework (Easy version)

    https://codeforces.com/contest/1118/problem/D1 能做完的天数最大不超过n,因为假如每天一杯咖啡,每杯咖啡容量大于1 首先对容量进行从大到小的排序, sor ...