Содержание статьи
2019 год подходит к концу, все начинают усиленно готовиться к праздникам. Безопасники добивают свои последние аудиты, которые из года в год наваливаются в эту пору. Неудивительно — ведь фискальный год тоже подходит к концу, а бюджеты еще не до конца потрачены!
После таких плодотворных недель кругом начинается затишье, и это касается в том числе информационной безопасности. Не так много уязвимостей и активностей приходится на конец декабря и начало января. Поэтому сейчас самое время вспомнить баги, которые, возможно, остались незамеченными в течение года.
Они просты по своей сути, однако это не мешает им иметь критический статус. Одни дают возможность выполнить произвольный код, другие — получить доступ к чувствительным данным или вовсе захватить полный доступ к системе. В этом году сильно досталось коммерческому форумному движку vBulletin: сразу несколько опасных багов было найдено в последних его версиях во второй половине года. С них и начнем.
RCE через загрузку аватара в vBulletin
Автор: Эджидио Романо (Egidio Romano aka EgiX)
Дата релиза: 4.10.2019
CVE: CVE-2019-17132
Уязвимые версии: vBulletin <= 5.5.4
Чтобы более предметно разговаривать о найденной проблеме, нужно поднять стенд и посмотреть на нее поближе. Так как vBulletin — коммерческое приложение, я предлагаю тебе самостоятельно решить, каким образом его найти.
В качестве базы данных будем использовать MySQL, а в качестве веб-сервера — докер-контейнер на основе Debian.
docker run -d -e MYSQL_USER="vb" -e MYSQL_PASSWORD="EAQhaTXieg" -e MYSQL_DATABASE="vb" --rm --name=mysql --hostname=mysql mysql/mysql-server:5.7
docker run --rm -ti --link=mysql --name=websrv --hostname=websrv -p80:80 debian /bin/bash
Устанавливаем стандартный набор из Apache2 и PHP.
apt update && apt install -y apache2 php nano php-mysqli php-xml php-gd
После этого можно запускать веб-сервер.
service apache2 start
Теперь устанавливаем vBulletin, я буду использовать версию 5.4.3.
По дефолту загруженные файлы хранятся в базе данных, такое поведение совместимо с эксплуатацией уязвимости. Поэтому сначала нужно зайти в настройки и поменять место хранения аватаров.
Загружается аватар через отправку запроса POST на /profile/upload-profilepicture
.
POST /profile/upload-profilepicture HTTP/1.1
Host: web.fh
Connection: keep-alive
Accept: application/json, text/javascript, */*; q=0.01
X-Requested-With: XMLHttpRequest
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary2V36WCZNIGxAuYwu
Cookie: сессионные_куки_
---WebKitFormBoundary2V36WCZNIGxAuYwu
Content-Disposition: form-data; name="profilePhotoFile"; filename="orange_box.png"
Content-Type: image/png
содержимое_файла
---WebKitFormBoundary2V36WCZNIGxAuYwu
Content-Disposition: form-data; name="securitytoken"
CSRF-токен
---WebKitFormBoundary2V36WCZNIGxAuYwu--
Если здесь просто попытаться загрузить PHP-файл, то ничего не выйдет. Расширение файла определяется библиотекой, которая работает с картинками. Если переданный документ не будет картинкой, то скрипт просто прекратит свою работу с ошибкой not_an_image
.
core/vb/library/user.php
1335: public function uploadAvatar($filename, $crop = array(), $userid = false, $adminoverride = false)
1336: {
...
1339: $isImage = $imageHandler->fileLocationIsImage($filename);
1340: if ($isImage)
1341: {
...
1359: $fileInfo = $imageHandler->fetchImageInfo($filename);
...
1361: else
1362: {
1363: // throw something useful here.
1364: throw new vB_Exception_Api('not_an_image');
1365: }
...
1435: $ext = strtolower($fileInfo[2]);
1436:
1437: $dimensions['extension'] = empty($ext) ? $pathinfo['extension'] : $ext;
...
1485: 'extension' => $dimensions['extension'],
...
1511: $result = $api->updateAvatar($userid, false, $filearray, true);
После всех манипуляций вызывается updateAvatar
с параметрами аватара в $filearray
.
Однако существует возможность напрямую вызвать этот метод API. Чтобы это сделать, нужно отправить запрос на эндпойнт ajax/api/user/updateAvatar
. Если заглянуть в тело метода updateAvatar
, то можно обнаружить любопытный участок кода.
core/vb/api/user.php
4111: public function updateAvatar($userid, $avatarid, $data = array(), $cropped = false)
4112: {
...
4149: if ($useavatar)
4150: {
4151: if (!$avatarid)
4152: {
...
4166: if (empty($data['extension']))
4167: {
4168: $filebits = explode('.', $data['filename']);
4169: $data['extension'] = end($filebits);
4170: }
4171:
4172: $userpic->set('extension', $data['extension']);
...
4182: $avatarfilename = "avatar{$userid}_{$avatarrevision}.{$data['extension']}";
...
4186: $avatarres = @fopen("$avatarpath/$avatarfilename", 'wb');
...
4187: $userpic->set('filename', $avatarfilename);
4188: fwrite($avatarres, $data['filedata']);
4189: @fclose($avatarres);
Здесь расширение берется из массива $data
, который можно просто передать в теле запроса. Оно будет иметь следующий вид:
userid=0&avatarid=0&data[extension]=<расширение_файла>&data[filedata]=<содержимое_файла>&securitytoken=<токен>
Когда userid
установлен в ноль, скрипт выбирает текущего авторизованного пользователя, а avatarid
, равный нулю, говорит, что нужно загружать аватар, а не удалять.
Вот мы и подобрались к самой сути уязвимости. vBulletin не проверят должным образом параметры data[extension]
и data[fildeata]
, и это позволяет творить чудесные вещи. Например, установим расширение php
, а в data[filedata]
передадим простой PHP-код.
Продолжение доступно только участникам
Вариант 1. Присоединись к сообществу «Xakep.ru», чтобы читать все материалы на сайте
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее
Вариант 2. Открой один материал
Заинтересовала статья, но нет возможности стать членом клуба «Xakep.ru»? Тогда этот вариант для тебя! Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.
Я уже участник «Xakep.ru»