作者:綠盟科技
來源:http://blog.nsfocus.net/cve-2018-7600-analysis/?from=timeline&isappinstalled=0
近日,流行的開源內容管理框架Drupal曝出一個遠程代碼執行漏洞,漏洞威脅等級為高危,攻擊者可以利用該漏洞執行惡意代碼,導致網站完全被控制。漏洞對應的CVE編號為CVE-2018-7600。
本篇文章對Drupal 8 – CVE-2017-7600漏洞進行了詳細分析。這個漏洞看起來是一個漏洞,其實我認為,它是由兩個小的雞肋問題組成的。具體是什么呢?
漏洞分析
這個漏洞的根本原因出在drupal對表單的渲染上:

可見,在drupal中,我們不需要直接寫html表單,而是先創建一個數組,表單呈現引擎通過位于\drupal\core\lib\Drupal\Core\Form\FormBuilder.php文件中的buildForm方法構造出一個名為$form表單,然后成對應的html表單進行呈現。
通過下圖buildform的定義,可以看出它是用來構造一個表單的

最終的$form是如下圖這個樣子:

這個漏洞,恰恰就出在了這里。
但是對于一個drupal框架的應用程序來說,后臺表單數組都是開發者寫好的,像這個樣子
public function form(array $form, FormStateInterface $form_state) {
$user = $this->currentUser();
/** @var \Drupal\user\UserInterface $account */
$account = $this->entity;
$admin = $user->hasPermission('administer users');
// Pass access information to the submit handler. Running an access check
// inside the submit function interferes with form processing and breaks
// hook_form_alter().
$form['administer_users'] = [
'#type' => 'value',
'#value' => $admin,
];
$form['#attached']['library'][] = 'core/drupal.form';
// For non-admin users, populate the form fields using data from the
// browser.
if (!$admin) {
$form['#attributes']['data-user-info-from-browser'] = TRUE;
}
// Because the user status has security implications, users are blocked by
// default when created programmatically and need to be actively activated
// if needed. When administrators create users from the user interface,
// however, we assume that they should be created as activated by default.
if ($admin) {
$account->activate();
}
// Start with the default user account fields.
$form = parent::form($form, $form_state, $account);
return $form;
}
攻擊者是無法改變表單數組元素的key值的。
很多應用都提供了如下的一個便利的方法:
比如要注冊一個用戶,用戶名、密碼、郵箱、電話,這些東西都填好了。當點擊提交的時候,網站告訴你,用戶名已存在。
這時候,你會發現,密碼、郵箱、電話這些元素不需要你再次填寫了,頁面已經將保存下來了。
drupal系統同樣有這樣的功能,具體如何實現的呢?下面我們做個試驗:
我們先提交個正常的表單
先在buildform函數返回處下斷后

填寫表單并提交

頁面跳轉到注冊成功頁面,

我們在buildform函數返回處下的斷點根本沒有斷下來。
接著我們再按著上面的表單一模一樣的注冊一個看看:

但這次呢,在斷點處成功斷下了:

在這處斷點,我們把name的值改為“kingsguard_test_1”試試

這次的返回頁面如下:

整個流程是:
- 用戶填寫表單->表單沒有問題->返回注冊成功頁面
- 用戶填寫表單->表單內容有問題(例如用戶名已被注冊)->調用buildform方法,把用戶傳入的內容一同構造為表單數組->渲染表單數組為html頁面返回
這就是剛剛在buildform斷點處把name值由kingsguard改為kingsguard_test_1,返回的頁面里username值也變成kingsguard_test_1的原因。
到這里,攻擊鏈已經很明確了,攻擊者傳入的值,可以通過buildform(方法構造表單數組,并且這個表單數組接下來還會被drupal表單呈現引擎解析為html頁面。
當我們在這個注冊表單頁面里,如果想上傳一張圖片

這時候發送的請求如下

當上傳成功后,往往有一個縮略圖顯示在那,如下圖菊花處:

這個縮略圖,是通過drupal\core\modules\file\src\Element\ManagedFile.php文件中的uploadAjaxCallback方法來解析。
注意,還記的上文buildform方法嗎?buildform生成form數組傳遞給uploadAjaxCallback方法來解析,目的是在返回頁面上顯示那個縮率的菊花。
既然流程已經捋順了,我們通過構造poc來動態調試下,發送如下圖post包:

首先會進入buildform函數來構造表單數組,接下來這個表單數組($form)會進入uploadAjaxCallback方法。
看下這個uploadAjaxCallback方法:

傳入uploadAjaxCallback方法中的$form變量,就是buildform方法生成的表單數組:

$form數組傳入uploadAjaxCallback方法中后,可以看到有這么一行(下圖紅框處):

$form_parents變量竟然可以從get中傳入,意味著這個變量可控,其實就是我們poc中的element_parents=account/mail/%23value。
通過poc,此處的$form_parents變量如下圖

$form_parents變量和form
新的form變量如下:

接下來看這里的renderRoot方法:

此處傳入的$form變量為:

繼續看renderRoot方法:
public function renderRoot(&$elements) {
// Disallow calling ::renderRoot() from within another ::renderRoot() call.
if ($this->isRenderingRoot) {
$this->isRenderingRoot = FALSE;
throw new \LogicException('A stray renderRoot() invocation is causing bubbling of attached assets to break.');
}
// Render in its own render context.
$this->isRenderingRoot = TRUE;
$output = $this->executeInRenderContext(new RenderContext(), function () use (&$elements) {
return $this->render($elements, TRUE);
});
$this->isRenderingRoot = FALSE;
return $output;
}
里面調用了render方法
繼續看render方法:
public function render(&$elements, $is_root_call = FALSE) {
// Since #pre_render, #post_render, #lazy_builder callbacks and theme
// functions or templates may be used for generating a render array's
// content, and we might be rendering the main content for the page, it is
// possible that any of them throw an exception that will cause a different
// page to be rendered (e.g. throwing
// \Symfony\Component\HttpKernel\Exception\NotFoundHttpException will cause
// the 404 page to be rendered). That page might also use
// Renderer::renderRoot() but if exceptions aren't caught here, it will be
// impossible to call Renderer::renderRoot() again.
// Hence, catch all exceptions, reset the isRenderingRoot property and
// re-throw exceptions.
try {
return $this->doRender($elements, $is_root_call);
}
catch (\Exception $e) {
// Mark the ::rootRender() call finished due to this exception & re-throw.
$this->isRenderingRoot = FALSE;
throw $e;
}
}
里面調用了doRender方法
繼續看doRender方法:
在這個方法的505行

調用call_user_func方法
此處的參數如下:

可見,這里的
$callable=”exec”
$elements[‘#children’]=”kingsguard_text”(這里我們傳入的惡意代碼,這里我就不演示了)
總結:
這個漏洞看起來是一個漏洞,其實我認為,它是由兩個小的雞肋的問題組成的,第一次就是在buildform處,用戶傳入的變量沒有受到限制,導致可以傳入mail[#post_render]、mail[#type]這樣的變量,但是單單這個問題,還不嚴重,因為對于最終渲染的html頁面來說,傳入的數組仍然是數組,不能被當成元素來解析。但是偏偏uploadAjaxCallback方法中的$form_parents變量是直接通過get(‘element_parents’)得來的,這下兩個一結合,$form_parents把之前傳入的數值當成元素了,這下就造成了一個大洞。
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/571/
暫無評論