Author: p0wd3r (知道創宇404安全實驗室)

Date: 2016-12-21

0x00 漏洞概述

1.漏洞簡介

Joomla 于12月13日發布了3.6.5的升級公告,此次升級修復了三個安全漏洞,其中 CVE-2016-9838 被官方定為高危。根據官方的描述,這是一個權限提升漏洞,利用該漏洞攻擊者可以更改已存在用戶的用戶信息,包括用戶名、密碼、郵箱和權限組 。經過分析測試,成功實現了水平用戶權限突破,但沒有實現垂直權限提升為管理員。

2.漏洞影響

觸發漏洞前提條件:

  1. 網站開啟注冊功能
  2. 攻擊者知道想要攻擊的用戶的 id (不是用戶名)

成功攻擊后攻擊者可以更改已存在用戶的用戶信息,包括用戶名、密碼、郵箱和權限組 。

3.影響版本

1.6.0 - 3.6.4

0x01 漏洞復現

1. 環境搭建

docker-compose.yml:

version: '2'

services:
  db:
    image: mysql
    environment:
      - MYSQL_ROOT_PASSWORD=hellojm
      - MYSQL_DATABASE=jm

  app:
    image: joomla:3.6.3
    depends_on:
      - db
    links:
      - db
    ports:
      - "127.0.0.1:8080:80"

然后在 docker-compose.yml 所在目錄執行docker-compose up,訪問后臺開啟注冊再配置SMTP即可。

2.漏洞分析

官方沒有給出具體的分析,只給了描述:

Alt text

翻譯過來就是:

對表單驗證失敗時存儲到 session 中的未過濾數據的不正確使用會導致對現有用戶帳戶的修改,包括重置其用戶名,密碼和用戶組分配。

因為沒有具體細節,所以我們先從補丁下手,其中這個文件的更改引起了我的注意:

https://github.com/joomla/joomla-cms/commit/435a2226118a4e83ecaf33431ec05f39c640c744

Alt text

可以看到這里的$temp是 session 數據,而該文件又與用戶相關,所以很有可能就是漏洞點。

我們下面通過這樣兩個步驟來分析:

  1. 尋找輸入點
  2. 梳理處理邏輯

1.尋找輸入點

我們找一下這個 session 是從哪里來的:

Alt text

components/com_users/controllers/registration.php中設置,在components/com_users/models/registration.php中獲取。我們看components/com_users/controllers/registration.php中第108-204行的register函數:

public function register()
{
    ...

    $data = $model->validate($form, $requestData);

    // Check for validation errors.
    if ($data === false)
    {
        ...

        // Save the data in the session.
        $app->setUserState('com_users.registration.data', $requestData);

        ...
    }

    // Attempt to save the data.
    $return = $model->register($data);

    // Check for errors.
    if ($return === false)
    {
        // Save the data in the session.
        $app->setUserState('com_users.registration.data', $data);

        ...     
    }

    ...
}

這兩處設置 session 均在產生錯誤后進行,和漏洞描述相符,并且$requestData是我們原始的請求數據,并沒有被過濾,所以基本可以把這里當作我們的輸入點。

我們來驗證一下,首先隨便注冊一個用戶,然后再注冊同樣的用戶并開啟動態調試:

Alt text

由于這個用戶之前注冊過,所以驗證出錯,從而將請求數據寫入了 session 中。

取 session 的地方在components/com_users/models/registration.phpgetData函數,該函數在訪問注冊頁面時就會被調用一次,我們在這時就可以看到 session 的值:

Alt text

由于存儲的是請求數據,所以我們還可以通過構造請求來向 session 中寫入一些額外的變量。

2.梳理處理邏輯

輸入點找到了,下面來看我們輸入的數據在哪里被用到。我們看components/com_users/models/registration.phpregister函數:

public function register($temp)
{
    $params = JComponentHelper::getParams('com_users');

    // Initialise the table with JUser.
    $user = new JUser;
    $data = (array) $this->getData();

    // Merge in the registration data.
    foreach ($temp as $k => $v)
    {
        $data[$k] = $v;
    }

    // Prepare the data for the user object.
    $data['email'] = JStringPunycode::emailToPunycode($data['email1']);
    $data['password'] = $data['password1'];
    $useractivation = $params->get('useractivation');
    $sendpassword = $params->get('sendpassword', 1);

    ...

    // Bind the data.
    if (!$user->bind($data))
    {
        $this->setError(JText::sprintf('COM_USERS_REGISTRATION_BIND_FAILED', $user->getError()));

        return false;
    }

    // Load the users plugin group.
    JPluginHelper::importPlugin('user');

    // Store the data.
    if (!$user->save())
    {
        $this->setError(JText::sprintf('COM_USERS_REGISTRATION_SAVE_FAILED', $user->getError()));

        return false;
    }

    ...
}

在這里調用了之前的getData函數,然后使用請求數據對$data賦值,再用$data對用戶數據做更改。

首先跟進$user->bind($data),在libraries/joomla/user/user.php中第595-693行:

public function bind(&$array)
{
    ...

    // Bind the array
    if (!$this->setProperties($array))
    {
        $this->setError(JText::_('JLIB_USER_ERROR_BIND_ARRAY'));

        return false;
    }

    // Make sure its an integer
    $this->id = (int) $this->id;

    return true;
}

這里根據我們傳入的數據對對象的屬性進行賦值,setProperties并沒有對賦值進行限制。

接下來我們看$user->save($data),在libraries/joomla/user/user.php中第706-818行:

public function save($updateOnly = false)
{
    // Create the user table object
    $table = $this->getTable();
    $this->params = (string) $this->_params;
    $table->bind($this->getProperties());

    ... 

    if (!$table->check())
    {
        $this->setError($table->getError());

        return false;
    }   

    ...

    // Store the user data in the database
    $result = $table->store();

    ...
}

具體內容就是將$user的屬性綁定到$table中,然后對$table進行檢查,這里僅僅是過濾特殊符號和重復的用戶名和郵箱,如果檢查通過,將數據存入到數據庫中,存儲數據的函數在libraries/joomla/table/user.php中:

/**
 * Method to store a row in the database from the JTable instance properties.
 *
 * If a primary key value is set the row with that primary key value will be updated with the instance property values.
 * If no primary key value is set a new row will be inserted into the database with the properties from the JTable instance.
 *
 * @param   boolean  $updateNulls  True to update fields even if they are null.
 *
 * @return  boolean  True on success.
 *
 * @since   11.1
 */
public function store($updateNulls = false)

如果主鍵存在則更新,主鍵不存在則插入。

整個的流程看下來我發現這樣一個問題:

如果$data中有id這個屬性并且其值是一個已存在的用戶的 id ,由于在bindsave中并沒有對這個屬性進行過濾,那么最終保存的數據就會帶有 id 這個主鍵,從而變成了更新操作,也就是用我們請求的數據更新了一個已存在的用戶。

實際操作一下,我們之前注冊了一個名字為 victim 的用戶,數據庫中的 id 是57:

Alt text

然后我們以相同的用戶名再發起一次請求,然后截包,添加一個值為57名為jform[id]的屬性:

Alt text

放行后由于重復注冊從而發生錯誤,程序隨后將請求數據記錄到了 session 中:

Alt text

接下來我們發送一個新的注冊請求,用戶名郵箱均為之前未注冊過的,在save函數處下斷點:

Alt text

id 被寫進了$user中。然后放行請求,即可在數據庫中看到結果:

Alt text

之前的 victim 已被新用戶 attacker 取代。

整個攻擊流程總結如下:

  1. 注冊用戶A
  2. 重復注冊用戶A,請求包中加上想要攻擊的用戶C的 id
  3. 注冊用戶B
  4. 用戶B替代了用戶C

(上面的演示中A和C是同一個用戶)

需要注意的是我們不能直接發送一個帶有 id 的請求來更新用戶,這樣的請求會在validate函數中被過濾掉,在components/com_users/controllers/registration.phpregister函數中:

public function register()
{
    ...

    $data = $model->validate($form, $requestData);

    // Check for validation errors.
    if ($data === false)
    {
        ...

        // Save the data in the session.
        $app->setUserState('com_users.registration.data', $requestData);

        ...
    }

    // Attempt to save the data.
    $return = $model->register($data);

    ...
}

所以我們采用的是先通過validate觸發錯誤來將 id 寫到 session 中,然后發送正常請求,在register中讀取 session 來引入 id,這樣就可以繞過validate了。

另外一點,實施攻擊后被攻擊用戶的權限會被改為新注冊用戶的權限(一般是 Registered),這個權限目前我們無法更改,因為在getData函數中對groups做了強制賦值:

$temp = (array) $app->getUserState('com_users.registration.data', array());

...

// Get the groups the user should be added to after registration.
$this->data->groups = array();

// Get the default new user group, Registered if not specified.
$system = $params->get('new_usertype', 2);

$this->data->groups[] = $system;

所以目前只是實現了水平權限的提升,至于是否可以垂直權限提升以及怎么提升還要等官方的說明或者是大家的分析。

由于沒有技術細節,一切都是根據自己的推斷而來,如有錯誤,還望指正 :)

3.補丁分析

Alt text

使用 session 時僅允許使用指定的屬性。

0x02 修復方案

升級至3.6.5 https://www.joomla.org/announcements/release-news/5693-joomla-3-6-5-released.html

0x03 參考


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