Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/modules/auxiliary/gather/cloud_lookup.rb
Views: 11777
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##45require 'public_suffix'67class MetasploitModule < Msf::Auxiliary8include Msf::Exploit::Remote::DNS::Enumeration9include Msf::Auxiliary::Report1011def initialize(info = {})12super(13update_info(14info,15'Name' => 'Cloud Lookup (and Bypass)',16'Description' => %q{17This module can be useful if you need to test the security of your server and your18website behind a solution Cloud based. By discovering the origin IP address of the19targeted host.2021More precisely, this module uses multiple data sources (in order ViewDNS.info, DNS enumeration22and Censys) to collect assigned (or have been assigned) IP addresses from the targeted site or domain23that uses the following:24* Cloudflare, Amazon CloudFront, ArvanCloud, Envoy Proxy, Fastly, Stackpath Fireblade,25Stackpath MaxCDN, Imperva Incapsula, InGen Security (BinarySec EasyWAF), KeyCDN, Microsoft AzureCDN,26Netlify and Sucuri.27},28'Author' => [29'mekhalleh (RAMELLA Sébastien)', # https://www.pirates.re/30'Yvain'31],32'References' => [33['URL', 'https://citadelo.com/en/blog/cloudflare-how-to-do-it-right-and-do-not-reveal-your-real-ip/']34],35'DefaultOptions' => { 'DnsNote' => false },36'License' => MSF_LICENSE,37'Actions' => [38['Automatic', {}],39[40'CloudFlare', {41'Description' => 'Cloudflare provides SaaS based CDN, WAF, DNS and DDoS mitigation services.',42'Signatures' => ['server: cloudflare']43}44],45[46'Amazon CloudFront', {47'Description' => 'Content Delivery Network services of Amazon',48'Signatures' => ['x-amz-cf-id:']49}50],51[52'ArvanCloud CDN', {53'Description' => 'ArvanCloud CDN comprises tens of PoP sites in important locations all around the world to deliver online content to the users',54'Signatures' => ['server: ArvanCloud']55}56],57[58'AzureCDN', {59'Description' => 'Microsoft Azure Content Delivery Network (CDN) is a global content distribution network solution for delivering high bandwidth content',60'Signatures' => []61}62],63[64'Envoy Proxy', {65'Description' => 'An open source edge and service proxy, designed for Cloud-Native applications',66'Signatures' => ['server: envoy']67}68],69[70'Fastly', {71'Description' => 'Another widely used CDN/WAF solution',72'Signatures' => ['Fastly-SSL']73}74],75[76'Imperva Incapsula', {77'Description' => 'Cloud based Web application firewall of Imperva',78'Signatures' => ['X-CDN: Incapsula', '_incap_']79}80],81[82'InGen Security (BinarySec EasyWAF)', { # Reunion island powa!83'Description' => 'Cloud based Web application firewall of InGen Security and BinarySec',84'Signatures' => ['binarysec', 'server: gatejs']85}86],87[88'KeyCDN', {89'Description' => 'KeyCDN is a high performance content delivery network that has been built for the future', # lol90'Signatures' => ['Server: keycdn-engine']91}92],93[94'Netlifi', {95'Description' => 'One workflow, from local development to global deployment',96'Signatures' => ['x-nf-request-id:']97}98],99[100'NoWAFBypass', {101'Description' => 'Do NOT check any bypass method',102'Signatures' => []103}104],105[106'Stackpath Fireblade', {107'Description' => 'Enterprise Website Security & DDoS Protection',108'Signatures' => ['Server: fbs']109}110],111[112'Stackpath MaxCDN', {113'Description' => 'Speed Up your Content Delivery',114'Signatures' => ['Server: NetDNA-cache']115}116],117[118'Sucuri', {119'Description' => 'Cloud based Web application firewall of Sucuri',120'Signatures' => ['x-sucuri-id:']121}122],123],124'DefaultAction' => 'Automatic',125'Notes' => {126'Stability' => [],127'Reliability' => [],128'SideEffects' => [IOC_IN_LOGS]129}130)131)132133register_options([134OptString.new('CENSYS_SECRET', [false, 'The Censys API SECRET']),135OptString.new('CENSYS_UID', [false, 'The Censys API UID']),136OptString.new('COMPSTR', [false, 'You can use a custom string to perform the comparison (read documentation)']),137OptString.new('HOSTNAME', [true, 'The hostname or domain name where we want to find the real IP address']),138OptPath.new('IPBLACKLIST_FILE', [false, 'Files containing IP addresses to blacklist during the analysis process, one per line', nil]),139OptString.new('Proxies', [false, 'A proxy chain of format type:host:port[,type:host:port][...]']),140OptInt.new('RPORT', [true, 'The target TCP port on which the protected website responds', 443]),141OptBool.new('SSL', [true, 'Negotiate SSL/TLS for outgoing connections', true]),142OptInt.new('THREADS', [true, 'Threads for DNS enumeration', 8]),143OptString.new('URIPATH', [true, 'The URI path on which to perform the page comparison', '/']),144OptPath.new('WORDLIST', [false, 'Wordlist of subdomains', ::File.join(Msf::Config.data_directory, 'wordlists', 'namelist.txt')])145])146147register_advanced_options([148OptBool.new('ALLOW_NOWAF', [true, 'Automatically switch to NoWAFBypass when detection fails with the Automatic action', false]),149OptBool.new('ENUM_BRT', [true, 'Set DNS bruteforce as optional', true]),150OptBool.new('REPORT_LEAKS', [true, 'Set to write leaked ip addresses in notes', false]),151OptString.new('USERAGENT', [true, 'Specify a personalized User-Agent header in HTTP requests', Rex::UserAgent.session_agent]),152OptEnum.new('TAG', [true, 'Specify the HTML tag in which you want to find the fingerprint', 'title', ['title', 'html']]),153OptInt.new('HTTP_TIMEOUT', [true, 'HTTP(s) request timeout', 8]),154])155end156157def setup_resolver158dns_resolver = super159# set the DNS port explicitly so it does not conflict with the datastore160# RPORT value which is for HTTP161dns_resolver.port = 53162@dns_resolver = dns_resolver163end164165# ------------------------------------------------------------------------- #166167# auxiliary/gather/censys_search.rb168def censys_search(keyword, uid, secret)169begin170cli = Rex::Proto::Http::Client.new('search.censys.io', 443, {}, true, nil, datastore['Proxies'])171cli.connect172173response = cli.request_cgi(174'method' => 'GET',175'uri' => "/api/v2/hosts/search?q=#{keyword}",176'agent' => datastore['USERAGENT'],177'headers' => {178'Authorization' => "Basic #{Rex::Text.encode_base64("#{uid}:#{secret}")}"179}180)181results = cli.send_recv(response)182rescue ::Rex::ConnectionError, Errno::ECONNREFUSED, Errno::ETIMEDOUT183print_error('HTTP connection failed to Censys.IO website.')184end185186unless results187print_error('Unable to retrieve any data from Censys.IO website.')188return []189end190191records = ActiveSupport::JSON.decode(results.body)192193results = records['result']194parse_ipv4(results)195end196197def check_tcp_port(ip, port)198begin199sock = Rex::Socket::Tcp.create(200'PeerHost' => ip,201'PeerPort' => port,202'Proxies' => datastore['Proxies']203)204rescue ::Rex::ConnectionRefused, Rex::ConnectionError205vprint_status(" * Closed: tcp://#{ip}:#{port}/")206return false207end208209sock.close210return true211end212213def wrap_mx(records)214ar_ips = []215records.each.map do |r|216a = /(\d*\.\d*\.\d*\.\d*)/.match(dns_get_a(r.exchange.to_s).to_s)217ar_ips.push(a) if a218end219ar_ips220end221222def grab_domain_ip_history(domain)223begin224cli = Rex::Proto::Http::Client.new('viewdns.info', 443, {}, true, nil, datastore['Proxies'])225cli.connect226227request = cli.request_cgi({228'method' => 'GET',229'uri' => "/iphistory/?domain=#{domain}",230'agent' => datastore['USERAGENT']231})232response = cli.send_recv(request)233cli.close234rescue ::Rex::ConnectionError, Errno::ECONNREFUSED, Errno::ETIMEDOUT235print_error('HTTP connection failed to ViewDNS.info website.')236return []237end238239unless response240print_error('Unable to retrieve any data from ViewDNS.info website.')241return []242end243244html = response.get_html_document245table = html.css('table')[3]246247unless table.nil?248rows = table.css('tr')249250ar_ips = []251rows.each.map do |row|252row = /(\d*\.\d*\.\d*\.\d*)/.match(row.css('td').map(&:text).to_s)253unless row.nil?254ar_ips.push(row)255end256end257end258259if ar_ips.nil?260print_bad('No domain IP(s) history founds.')261return []262end263264ar_ips265end266267def http_get_request_raw(host, port, ssl, uri, vhost = nil)268begin269http = Rex::Proto::Http::Client.new(host, port, {}, ssl, nil, datastore['Proxies'])270http.connect(datastore['HTTP_TIMEOUT'])271272unless vhost.nil?273http.set_config({ 'vhost' => vhost })274end275276request = http.request_raw({277'method' => 'GET',278'uri' => uri,279'agent' => datastore['USERAGENT']280})281response = http.send_recv(request)282http.close283rescue ::Rex::ConnectionError, Errno::ECONNREFUSED, Errno::ETIMEDOUT284# noop285rescue ::StandardError => e286print_error(e.message)287end288return false if response.nil?289290response291end292293# auxiliary/gather/censys_search.rb294def parse_ipv4(records)295ip_list = []296records['hits'].each do |ipv4|297ip_list.push(ipv4['ip'])298end299ip_list300end301302def save_note(hostname, ip, port, proto, ebypass)303data = { 'vhost' => hostname, 'detected_ip' => ip, 'action' => @my_action.name, 'effective_bypass' => ebypass }304report_note(305host: hostname,306service: proto,307port: port,308type: 'auxiliary.gather.cloud_lookup',309data: data,310update: :unique_data311)312end313314# ------------------------------------------------------------------------- #315316def check_bypass(fingerprint, ip)317# Check for "misconfigured" web server on TCP/80.318if check_tcp_port(ip, 80)319ret_value ||= check_request(fingerprint, ip, 80, false)320end321322# Check for "misconfigured" web server on TCP/443.323if check_tcp_port(ip, 443)324ret_value ||= check_request(fingerprint, ip, 443, true)325end326327ret_value328end329330def check_request(fingerprint, ip, port, ssl)331proto = (ssl ? 'https' : 'http')332333vprint_status(" * Trying: #{proto}://#{ip}:#{port}/")334response = http_get_request_raw(ip, port, ssl, datastore['URIPATH'], datastore['HOSTNAME'])335336if response337return false if detect_signature(response)338339if response.code == 200340found = false341342html = response.get_html_document343begin344# Searches for the chain to compare in the defined tag.345found = true if html.at(datastore['TAG']).to_s.include? fingerprint.to_s.encode('utf-8')346rescue NoMethodError, ::Encoding::CompatibilityError347return false348end349350if found351print_good("A direct-connect IP address was found: #{proto}://#{ip}:#{port}/")352if @my_action.name == 'NoWAFBypass'353save_note(datastore['HOSTNAME'], ip, port, proto, 'manual check claimed')354else355save_note(datastore['HOSTNAME'], ip, port, proto, true)356end357return true358end359360elsif response.redirect?361found = false362363vprint_line(" --> responded with HTTP status code: #{response.code} to #{response.headers['location']}")364begin365found = true if response.headers['location'].include?(datastore['hostname'])366rescue NoMethodError, ::Encoding::CompatibilityError367return false368end369370if found371print_warning("A leaked IP address was found: #{proto}://#{ip}:#{port}/")372save_note(datastore['HOSTNAME'], ip, port, proto, false) if datastore['REPORT_LEAKS']373end374375else376vprint_line(" --> responded with an unhandled HTTP status code: #{response.code}")377end378end379380return false381end382383def detect_signature(data)384@my_action['Signatures'].each do |signature|385return true if data.headers.to_s.downcase.include?(signature.downcase)386end387return false388end389390def arvancloud_ips391response = http_get_request_raw(392'www.arvancloud.com',393443,394true,395'/en/ips.txt'396)397return [] if response.nil?398399response.get_html_document.text.split("\n")400end401402# https://docs.microsoft.com/fr-fr/azure/cdn/cdn-pop-list-api403def azurecdn_ips404regions = {405'region' => [406'asiaeast', 'asiasoutheast', 'australiaeast', 'australiasoutheast', 'canadacentral',407'canadaeast', 'chinaeast', 'chinanorth', 'europenorth', 'europewest',408'germanycentral', 'germanyn', 'germanynortheast', 'indiacentral', 'indiasouth',409'indiawest', 'japaneast', 'japanwest', 'brazilsouth', 'koreasouth',410'koreacentral', 'ukwest', 'uksouth', 'uscentral', 'useast',411'useast2', 'usnorth', 'ussouth', 'uswestcentral', 'uswest',412'uswest2'413]414}415params = regions.merge({416'complement' => 'on',417'outputformat' => 'list-cidr'418})419420begin421cli = Rex::Proto::Http::Client.new('azurerange.azurewebsites.net', 443, {}, true, nil, datastore['Proxies'])422cli.connect423424response = cli.request_cgi(425'method' => 'GET',426'uri' => '/Download/',427'agent' => datastore['USERAGENT'],428'vars_get' => params429)430results = cli.send_recv(response)431rescue ::Rex::ConnectionError, Errno::ECONNREFUSED, Errno::ETIMEDOUT432print_error('HTTP connection failed to Azurerange website.')433end434435unless results436print_error('Unable to retrieve any data from Azurerange website.')437return []438end439440results.get_html_document.css('p').text.split("\r\n")441end442443def cloudflare_ips444response = http_get_request_raw(445'www.cloudflare.com',446443,447true,448'/ips-v4'449)450return [] if response.nil?451452response.get_html_document.css('p').text.split("\n")453end454455def cloudfront_ips456response = http_get_request_raw(457'd7uri8nf7uskq.cloudfront.net',458443,459true,460'/tools/list-cloudfront-ips'461)462return [] if response.nil?463464ip_list = response.get_json_document['CLOUDFRONT_GLOBAL_IP_LIST']465ip_list += response.get_json_document['CLOUDFRONT_REGIONAL_EDGE_IP_LIST']466467ip_list.map { |ip| ip.gsub('"', '') }468end469470def fastly_ips471response = http_get_request_raw(472'api.fastly.com',473443,474true,475'/public-ip-list'476)477return [] if response.nil?478479response.get_json_document['addresses'].map { |ip| ip.gsub('"', '') }480end481482def incapsula_ips483begin484cli = Rex::Proto::Http::Client.new('my.incapsula.com', 443, {}, true, nil, datastore['Proxies'])485cli.connect486487response = cli.request_cgi(488'method' => 'POST',489'uri' => '/api/integration/v1/ips',490'agent' => datastore['USERAGENT'],491'vars_post' => { 'resp_format' => 'json' }492)493results = cli.send_recv(response)494rescue ::Rex::ConnectionError, Errno::ECONNREFUSED, Errno::ETIMEDOUT495print_error('HTTP connection failed to Incapsula website.')496end497498unless results499print_error('Unable to retrieve any data from Incapsula website.')500return []501end502503results.get_json_document['ipRanges'].map { |ip| ip.gsub('"', '') }504end505506def pick_action507return action if action.name != 'Automatic'508509response = http_get_request_raw(510datastore['HOSTNAME'],511datastore['RPORT'],512datastore['SSL'],513datastore['URIPATH']514)515return nil unless response516517actions.each do |my_action|518next if my_action.name == 'Automatic'519520my_action['Signatures'].each do |signature|521return my_action if response.headers.to_s.downcase.include?(signature.downcase)522end523end524525nil526end527528# ------------------------------------------------------------------------- #529530def run531# If the action can be detected automatically. (Action: Automatic)532@my_action = pick_action533if @my_action.nil?534# If the automatic search fails, bye bye.535unless datastore['ALLOW_NOWAF']536print_error('Couldn\'t determine the action automatically because no target signatures matched')537return538end539# If allowed, and the automatic action fails, searches for all website occurrences without regard to filtering systems.540actions.each do |my_action|541@my_action = my_action if my_action.name == 'NoWAFBypass'542end543end544vprint_status("Selected action: #{@my_action.name}")545546print_status('Passive gathering information...')547548domain_name = PublicSuffix.parse(datastore['HOSTNAME']).domain549ip_list = []550551# Start collecting information for grabbing all IP address(es).552553# ViewDNS.info554ip_records = grab_domain_ip_history(domain_name)555if ip_records && !ip_records.empty?556ip_list |= ip_records557end558print_status(" * ViewDNS.info: #{ip_records.count} IP address(es) found.")559560# Mail eXchanger561ip_records = dns_get_mx(domain_name)562if ip_records && !ip_records.empty?563ip_records = wrap_mx(ip_records)564ip_list |= ip_records565print_status(" * Received MX records: #{ip_records.count} IP address(es) found.")566end567568# DNS Enumeration569if datastore['ENUM_BRT'] && !dns_wildcard_enabled?(domain_name)570ip_records = dns_bruteforce(domain_name, datastore['WORDLIST'], datastore['THREADS'])571if ip_records && !ip_records.empty?572ip_list |= ip_records573end574print_status(" * DNS Enumeration: #{ip_records.count} IP address(es) found.")575end576577# Censys search578if [datastore['CENSYS_UID'], datastore['CENSYS_SECRET']].none?(&:nil?)579ip_records = censys_search(domain_name, datastore['CENSYS_UID'], datastore['CENSYS_SECRET'])580if ip_records && !ip_records.empty?581ip_list |= ip_records582end583print_status(" * Censys IPv4: #{ip_records.count} IP address(es) found.")584end585print_status586587# Exit if no IP address(es) has been found.588if ip_list.empty?589print_bad('No IP address found :-(')590return591end592593# Comparison to remove address(es) that match the security solution to be tested.594# except:595# - the selected action is set to NoWAFBypass (except if blacklist ip file is set)596# - addresses are not provided597598ip_blacklist = []599# Cleaning IP addresses if necessary.600case @my_action.name601when /ArvanCloud/602ip_blacklist = arvancloud_ips603when /AzureCDN/604ip_blacklist = azurecdn_ips605when /CloudFlare/606ip_blacklist = cloudflare_ips607when /CloudFront/608ip_blacklist = cloudfront_ips609when /Fastly/610ip_blacklist = fastly_ips611when /Incapsula/612ip_blacklist = incapsula_ips613when /InGen Security/614# Public address(es) not available, check for known provider DNS responses match :-)615ip_list.uniq.each do |ip|616a = dns_get_a(ip.to_s)617['binarysec', 'easywaf', 'ingensec', '127.0.0.1'].each do |signature|618ip_blacklist << ip.to_s if a.to_s.downcase.include? signature.downcase619end620end621end622623# Add. Blacklisted IP address(es) from file.624unless datastore['IPBLACKLIST_FILE'].nil?625if File.readable? datastore['IPBLACKLIST_FILE']626ips = File.readlines(datastore['IPBLACKLIST_FILE'], chomp: true)627ips.each do |ip|628ip_blacklist << ip629end630else631raise ArgumentError, "Cannot read file #{datastore['IPBLACKLIST_FILE']}"632end633end634635# Time to clean, removing bad address(es).636records = []637if !ip_blacklist.empty?638print_status("Clean #{@my_action.name} server(s)...")639ip_list.uniq.each do |ip|640is_listed = false641642ip_blacklist.each do |ip_range|643if IPAddr.new(ip_range).include? ip.to_s644is_listed = true645break646end647end648649records << ip.to_s unless is_listed650end651else652records.concat(ip_list.uniq.map(&:to_s))653end654655# Exit if no IP address(es) has been found after cleaning.656if records.empty?657print_bad('No IP address found after cleaning.')658return659end660661print_status(" * Total: #{records.uniq.count} IP address(es) found after cleaning.")662print_status663664# Processing bypass steps.665666print_status("Bypass #{action.name} is in progress...")667if datastore['COMPSTR'].nil?668# If no customized comparison string is entered by the user, search automatically into the user defined TAG (default: <title>).669print_status(" * Initial request to the original server for <#{datastore['TAG']}> comparison")670response = http_get_request_raw(671datastore['HOSTNAME'],672datastore['RPORT'],673datastore['SSL'],674datastore['URIPATH']675)676html = response.get_html_document677begin678fingerprint = html.at(datastore['TAG'])679unless fingerprint680print_bad('Auto-fingerprinting value is empty. Please consider the COMPSTR option')681return682end683rescue NoMethodError684print_bad('Please consider the COMPSTR option')685return686end687688vprint_status(" * Fingerprint: #{fingerprint.to_s.gsub("\n", '')}")689vprint_status690else691# The user-defined comparison string does not require a request to initiate a connection to the target server.692# The comparison is made by the check_bypass function in the user-defined TAG (default: <title>).693fingerprint = datastore['COMPSTR']694end695696# Loop for each unique IP:PORT candidate to check bypass.697ret_value = false698records.uniq.each do |ip|699found = check_bypass(700fingerprint,701ip702)703ret_value = true if found704end705706# message indicating that nothing was found.707unless ret_value708print_bad('No direct-connect IP address found :-(')709end710end711712end713714715