RCE3
影响范围:
- thinkphp 6.0.1x
使用
phpggc ThinkPHP/RCE3 system id -b -u
分析
thinkphp 6 版本与此前的版本存在较大差异,搜索 __destruct
时可发现入口点都在 vendor 目录下,也就是 Composer 类库目录。
vendor/league/flysystem-cached-adapter/src/Storage
AbstractCache.php
public function __destruct()
vendor/league/flysystem/src
SafeStorage.php
public function __destruct()
vendor/league/flysystem/src/Adapter
AbstractFtpAdapter.php
public function __destruct()
vendor/topthink/think-orm/src
Model.php
public function __destruct()
vendor/topthink/think-orm/src/db
Connection.php
public function __destruct()
RCE3 整个调用链如下所示:
League\Flysystem\Cached\Storage\Psr6Cache::__destruct()
| $this->save();
| $this->pool->getItem()
League\Flysystem\Directory::__call()<---+
| call_user_func_array |
| $this->getItem() ----------------+
think\Validate::__call()
| call_user_func_array
| $this->is()
| call_user_func_array * <----
总体看这条链还是比较短的,入口点为 Psr6Cache,这个类为 AbstractCache 的实现类。sink 点为 think\Validate
的 is 方法,在 is 方法中可以找到 call_user_func_array 的调用,$this->type
是可控的,$rule
和 $value
都是传入的参数。
default:
if (isset($this->type[$rule])) {
// 注册的验证规则
$result = call_user_func_array($this->type[$rule], [$value]);
think\Validate
类正好有一个 __call
方法,且使用 call_user_func_array 调用自身的 is 方法,传入参数为 args,这样看来还是比较好控制的。
public function __call($method, $args)
{
if ('is' == strtolower(substr($method, 0, 2))) {
$method = substr($method, 2);
}
array_push($args, lcfirst($method));
return call_user_func_array([$this, 'is'], $args);
}
这条链比较巧妙的点在与 Directory 的构造,在think\Validate::__call
中,call_user_func_array
调用 is 方法时需要传入三个参数,其中第三个参数为数组。
public function is($value, string $rule, array $data = [])
由于是从 League\Flysystem\Directory::__call()
中使用 call_user_func_array
调用 getItem 函数过来的,因此 $method
的值为 ‘getItem’, 但 array_push($args, lcfirst($method));
仅会往 $args
末尾添加一个元素 ‘getItem’,因此在传入think\Validate::call
时 $args
的第三个元素需要为一个数组。
调试时就可以发现,如果仅仅构造一层的 Directory,$args
的第三个元素始终都无法为一个数组,因此在构造 payload 时对 League\Flysystem\Directory
进行了嵌套。
array_unshift($arguments, $this->path);
也就是说,第一次调用到 League\Flysystem\Directory::__call
中的 call_user_func_array
时,再次调用 League\Flysystem\Directory::getItem
,由于这方法不存在,会再次进入 __call
,从而两次执行 array_unshift($arguments, $this->path);
进而构造一个满足条件的 $arguments
.
public function __call($method, array $arguments)
{
array_unshift($arguments, $this->path);
$callback = [$this->filesystem, $method];
try {
return call_user_func_array($callback, $arguments); <---
} catch (BadMethodCallException $e) {
throw new BadMethodCallException(
'Call to undefined method '
. get_called_class()
. '::' . $method
);
}
}