#!/usr/bin/env python
import requests
import argparse
import re
import dns.resolver
import os
from termcolor import colored
from urllib.parse import urlparse

"""
Script to identify any IP V4 address, used by a web app exposed on a domain, that is not referenced in its DNS A public records.

Use Shodan, as source of information, for the list of IP addresses outside the DNS A records

Based on the following sources and works:
    https://github.com/boringthegod/CloudFade


Dependencies:
    pip3 install requests termcolor tabulate dnspython
"""

# Constants
requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning)
PROXIES = {}
USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36"
DEFAULT_CALL_TIMEOUT = 10
LARGE_CALL_TIMEOUT = 120
MARKERS_REGEX = [r'<title.*?>(.*?)<\/title>']


def get_ip_addresses_from_dns(domain):
    ips = []
    resolver = dns.resolver.Resolver(configure=True)
    answer = resolver.resolve(domain, "A")
    for data in answer:
        ips.append(data.to_text())
    ips.sort()
    return ips


def get_ip_addresses_from_shodan(domain, api_key):
    ips = []
    service_url = f"https://api.shodan.io/shodan/host/search?key={api_key}&query=hostname:{domain}&facets=ip&minify=True"
    response = requests.get(service_url, proxies=PROXIES, verify=(len(PROXIES) == 0), timeout=LARGE_CALL_TIMEOUT)
    if response.status_code != 200:
        raise Exception(f"HTTP response code {response.status_code} received by Shodan API!")
    for ip_entry in response.json()["facets"]["ip"]:
        ips.append(ip_entry["value"])
    ips.sort()
    return ips


def get_reference_response_content(target_url):
    return requests.get(target_url, headers={"User-Agent": USER_AGENT}, proxies=PROXIES, verify=(len(PROXIES) == 0), timeout=LARGE_CALL_TIMEOUT).text


def get_call_with_ip_response_content(target_url, domain, ip):
    test_target_url = target_url.replace(domain, ip)
    resp = ""
    try:
        resp = requests.get(test_target_url, headers={"User-Agent": USER_AGENT, "Host": domain}, proxies=PROXIES, verify=False, timeout=DEFAULT_CALL_TIMEOUT).text
    except:
        pass
    return resp


def match_reference_response(reference_content, test_content):
    match_counter = 0
    for marker_regex in MARKERS_REGEX:
        occurrences_ref = re.findall(marker_regex, reference_content, re.IGNORECASE)
        if len(occurrences_ref) > 0:
            occurrences_test = re.findall(marker_regex, test_content, re.IGNORECASE)
            if len(occurrences_test) > 0:
                str_ref = ("#".join(occurrences_ref)).lower()
                str_test = ("#".join(occurrences_test)).lower()
                if str_ref == str_test:
                    match_counter += 1
    return (match_counter == len(MARKERS_REGEX))


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Script to identify any IP V4 address, used by a web app exposed on a domain, that is not referenced in its DNS A public records.")
    required_params = parser.add_argument_group("required named arguments")
    required_params.add_argument("-t", action="store", dest="target_url", help="Target URL (ex: 'https://righettod.eu').", required=True)
    parser.add_argument("-x", action="store", dest="proxy", help="Proxy to use for all probe requests (ex: 'http://127.0.0.1:8080', default to no proxy).", required=False, default=None)
    args = parser.parse_args()
    if args.proxy is not None:
        PROXIES = {"http": args.proxy, "https": args.proxy}
    shodan_api_key = os.environ["SHODAN_API_KEY"]
    if shodan_api_key is None or shodan_api_key == "":
        raise Exception("Environment variable 'SHODAN_API_KEY' not defined!")
    target_url = args.target_url
    domain = urlparse(target_url).hostname
    print(colored(f"[+] Get the list of IP addresses from DNS:", "yellow"))
    ips_from_dns = get_ip_addresses_from_dns(domain)
    print(", ".join(ips_from_dns))
    print(colored(f"[+] Get the list of IP addresses from Shodan:", "yellow"))
    ips_from_shodan = get_ip_addresses_from_shodan(domain, shodan_api_key)
    ips_not_defined_in_dns = set(ips_from_shodan).difference(ips_from_dns)
    if len(ips_not_defined_in_dns) == 0:
        print(colored(f"[!] No IP found!", "red"))
    else:
        print(", ".join(ips_from_shodan))
        print(colored(f"[+] Search for occurrence of the site on Shodan's IP address using the domain in the HOST header:", "yellow"))
        ips_found = []
        reference_content = get_reference_response_content(target_url)
        for ip in ips_not_defined_in_dns:
            print(f"\r=> Checking {ip:<16}", end="", flush=True)
            ip_content = get_call_with_ip_response_content(target_url, domain, ip)
            if match_reference_response(reference_content, ip_content):
                ips_found.append(ip)
        print("\r", end="", flush=True)
        if len(ips_found) == 0:
            print(f"No match found.{' ' * 20}")
        else:
            print(", ".join(ips_found))
