Пред­ставь: ста­рый поч­товый веб‑кли­ент, дав­но забытый и оставлен­ный пылить­ся в зако­улках интерне­та, но по‑преж­нему таящий в себе кла­дезь... Это исто­рия о том, как глу­бокое пог­ружение в RainLoop при­вело к тому, что я нашел RCE и спо­соб получить дос­туп к дан­ным поль­зовате­лей круп­ной ком­пании, которая не пожале­ла воз­награж­дения.

Это иссле­дова­ние получи­ло пер­вое мес­то на Pentest Award 2025 в катего­рии «Про­бив WEB». Сорев­нование еже­год­но про­водит­ся ком­пани­ей Awillix.

RainLoop — это про­ект на PHP с откры­тым исходным кодом и четырь­мя тысяча­ми звезд на GitHub. Мы совер­шим увле­катель­ное путешес­твие по лабирин­там его кода, заг­лянем в механиз­мы крип­тогра­фии и про­дела­ем пару трю­ков с хос­тами, которые, казалось бы, в ско­уп не вхо­дят, но поз­волят сор­вать джек­пот.

warning

Статья име­ет озна­коми­тель­ный харак­тер и пред­назна­чена для спе­циалис­тов по безопас­ности, про­водя­щих тес­тирова­ние в рам­ках кон­трак­та. Автор и редак­ция не несут ответс­твен­ности за любой вред, при­чинен­ный с при­мене­нием изло­жен­ной информа­ции. Рас­простра­нение вре­донос­ных прог­рамм, наруше­ние работы сис­тем и наруше­ние тай­ны перепис­ки прес­леду­ются по закону.

 

Исходная позиция

Все началось обыч­ным вечером, ког­да я, воору­жив­шись чаш­кой кофе, про­водил пер­вичную раз­ведку баг‑баун­ти‑ско­упа хос­тера beget.com. В SSL-сер­тифика­те одно­го из сотен доменов мель­кнул любопыт­ный хост. Это был поч­товый кли­ент, который ком­пания пре­дос­тавля­ла сво­им зарегис­три­рован­ным поль­зовате­лям, на под­домене вида fancy.beget.email.

Там был уста­нов­лен RainLoop, что показа­лось мне доволь­но стран­ным, так как основным веб‑мей­лером выс­тупал Roundcube. И тут я прос­то не смог усто­ять перед соб­лазном покопать­ся в исходни­ках.

Это при­ложе­ние я видел впер­вые, к тому же вер­сия ока­залась не самой све­жей — 1.12.1. Все говорит о том, что покопать­ся в исходни­ках будет отличной иде­ей, к тому же это моя страсть! Мотиви­рует одна толь­ко воз­можность най­ти что‑то инте­рес­ное.

Про­ект ока­зал­ся нишевым, но не сов­сем — поис­ковик FOFA находит 21 433 IP-адре­са, по которым отве­чает RainLoop.

Что ж, отличное ком­бо! Прис­тупа­ем к иссле­дова­нию.

 

Опасная десериализация

Ска­чав ис­ходный код RainLoop, я начал искать потен­циаль­ные точ­ки вхо­да для ата­ки. Моей целью было най­ти уяз­вимость, которая поз­волила бы выпол­нить про­изволь­ный код или коман­ды на сер­вере.

Од­ной из пер­вых находок стал метод RainLoop\Utils::DecodeKeyValuesQ(). Он обра­баты­вает дан­ные, которые затем попада­ют в фун­кцию unserialize, клас­сичес­кий такой век­тор для RCE.

./rainloop/v/1.12.1/app/libraries/RainLoop/Utils.php
static public function DecodeKeyValuesQ($sEncodedValues, $sCustomKey = '')
{
$aResult = @\unserialize(
\RainLoop\Utils::DecryptStringQ(
\MailSo\Base\Utils::UrlSafeBase64Decode($sEncodedValues), \md5(APP_SALT.$sCustomKey)));
return \is_array($aResult) ? $aResult : array();
}

На сто­роне кли­ента в куках хра­нят­ся дан­ные, которые затем обра­баты­вает этот метод. Но есть заг­воз­дка: дан­ные шиф­руют­ся с исполь­зовани­ем длин­ного слу­чай­ного клю­ча APP_SALT. Подоб­рать его нере­аль­но, и эта защита дела­ет под­мену дан­ных про­изволь­ными невоз­можной, надеж­но бло­кируя подоб­ный век­тор ата­ки.

 

Читалка секретов

Сле­дующим эта­пом стал поиск дру­гих уяз­вимос­тей, которые помог­ли бы рас­крыть этот ключ. Бла­го при­ложе­ние име­ет боль­шой пласт самых раз­ных фич.

Один из десят­ка методов, RainLoop\Actions\DoComposeUploadExternals(), ока­зал­ся нас­тоящим подар­ком для ата­кующе­го. Дан­ные, пос­тупа­ющие от поль­зовате­ля, без какой‑либо пос­тобра­бот­ки попада­ют нап­рямую в CURLOPT_URL.

/rainloop/v/1.12.1/app/libraries/RainLoop/Actions.php
public function DoComposeUploadExternals()
{
...
$aExternals = $this->GetActionParam('Externals', array());
if (\is_array($aExternals) && 0 < \count($aExternals))
{
...
foreach ($aExternals as $sUrl)
{
if ($rFile && $oHttp->SaveUrlToFile($sUrl, $rFile, '', $sContentType, $iCode, $this->Logger(), 60,
...
}
return $this->DefaultResponse(__FUNCTION__, $mResult);
}
./rainloop/v/1.12.1/app/libraries/MailSo/Base/Http.php
public function SaveUrlToFile($sUrl, $rFile, ...){
...
$aOptions = array(
CURLOPT_URL => $sUrl,
...
$oCurl = \curl_init();
\curl_setopt_array($oCurl, $aOptions);
...
$bResult = \curl_exec($oCurl);
...
return $bResult;
}

И это откры­вает две­ри для мно­жес­тва атак, вклю­чая SSRF через схе­мы вро­де gopher://, так как curl под­держи­вает десят­ки раз­ных про­токо­лов, в том чис­ле чте­ние локаль­ных фай­лов через file://, что было для меня глав­ным!

Ме­тод сам по себе не отда­вал содер­жимое фай­ла сра­зу, поэто­му для успешной экс­плу­ата­ции тре­бова­лась совокуп­ность дей­ствий:

  • Соз­дать новое пись­мо и прик­репить к нему про­изволь­ный аттач, нап­ример 123.txt.
  • Сох­ранить пись­мо в чер­новики и перех­ватить этот зап­рос (1).
  • В зап­росе (1) заменить POST-дан­ные такими:

    XToken=__CSRF_TOKEN__&Action=ComposeUploadExternals&Externals[]=file:///var/www/html/data/SALT.php
  • Отпра­вить и ско­пиро­вать хеш атта­ча из рес­понса (2).

  • Поменять хеш в (1) на (2) и отпра­вить зап­рос.

  • В атта­че нового пись­ма в чер­новиках будет содер­жимое фай­ла SALT.php.

Так я наконец смог про­читать файл, в котором хра­нилась сек­ретная соль.

/var/www/html/data/SALT.php
<?php //a58a35a5c3c08f4f047364531dee2dc3fbd99005c0c7e5abedcc0f531def5b1b329e151c4b801d27248bce1d27996eca3364

Соль, как видишь, име­ет вну­шитель­ный раз­мер и пол­ностью исполь­зует­ся для шиф­рования строк.

./rainloop/v/1.12.1/include.php
$sSalt = @file_get_contents(APP_DATA_FOLDER_PATH.'SALT.php');

Это тот самый ключ, который нужен для обхо­да крип­тогра­фичес­кой защиты. Теперь пазл начал скла­дывать­ся, но не хва­тало глав­ной детали.

 

Извилистая цепочка до RCE

На этом эта­пе я стол­кнул­ся с новой голово­лом­кой. RainLoop ока­зал­ся доволь­но скром­ным в пла­не исполь­зуемых биб­лиотек, да и те, что были, не всег­да под­гру­жались через autoload. Мой арсе­нал для соз­дания цепоч­ки десери­али­зации был, мяг­ко говоря, огра­ничен­ным. На «закус­ку» у меня было все­го нес­коль­ко биб­лиотек:

array(7) {
[0]=>
string(8) "RainLoop"
[1]=>
string(8) "Facebook"
[2]=>
string(8) "PHPThumb"
[3]=>
string(6) "Predis"
[4]=>
string(16) "SabreForRainLoop"
[5]=>
string(7) "Imagine"
[6]=>
string(9) "Detection"
}

PHPGGC, мой вер­ный спут­ник в таких делах, лишь грус­тно вздох­нул и само­устра­нил­ся. Стан­дар­тные гад­жеты здесь неп­римени­мы... Конеч­но же, я сра­зу пом­чался к велико­му «Гро­ку» и не менее умно­му «Кло­ду», ведь они за счи­таные пром­пты соберут мне то, что нуж­но. Но как толь­ко я начал раз­гре­бать их глю­ки, понял, что про­ще раз­бирать­ся самому.

Продолжение доступно только участникам

Материалы из последних выпусков становятся доступны по отдельности только через два месяца после публикации. Чтобы продолжить чтение, необходимо стать участником сообщества «Xakep.ru».

Присоединяйся к сообществу «Xakep.ru»!

Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее

  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    0 комментариев
    Старые
    Новые Популярные
    Межтекстовые Отзывы
    Посмотреть все комментарии