#!/usr/bin/env python
import requests
import argparse
import re
from termcolor import colored
from tabulate import tabulate

"""
Script to detect the different proxies in place by abusing the "Max-Forwards" HTTP request header.

Based on the following sources:
    https://twitter.com/irsdl/status/1337299267652825088
    https://www.agarri.fr/blog/archives/2011/11/12/traceroute-like_http_scanner/index.html
    https://github.com/righettod/toolbox-pentest-web/issues/47

Documentation about this header:
    http://webconcepts.info/concepts/http-header/Max-Forwards
    https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Max-Forwards

"The 'Max-Forwards' header field provides a mechanism with the TRACE and OPTIONS request methods 
to limit the number of times that the request is forwarded by proxies."

Dependencies:
    pip3 install requests termcolor tabulate
"""

# Config
# Disable TLS warning when validation is disabled when requests is used
requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning)

# Constants
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"
REFERENCE_HEADER_NAMES = ["server", "content-type", "via", "x-forwarded-for"]
REFERENCE_HTML_TAG_NAMES = {"title": r'<title.*?>(.*?)<\/title>'}
METHODS = ["GET", "TRACE", "OPTIONS"]


class ResponseData:
    def __init__(self, method, response, max_forwards_value):
        self.method = method
        self.return_code = response.status_code
        self.reference_headers_values = {}
        self.reference_html_values = {}
        self.max_forwards_value = max_forwards_value
        content = response.text
        for header in response.headers:
            if header.lower() in REFERENCE_HEADER_NAMES:
                self.reference_headers_values[header] = response.headers[header]
        for tag_name, tag_name_regex in REFERENCE_HTML_TAG_NAMES.items():
            tag_name_occurences = re.findall(tag_name_regex, content, re.IGNORECASE)
            if len(tag_name_occurences) > 0:
                self.reference_html_values[tag_name] = " ".join(tag_name_occurences)


def main(target_url, forwards_count):
    print(colored(f"[+] Execution context:", "yellow"))
    print(f"Full URL       : {target_url}")
    print(f"Forwards count : {forwards_count}")
    print(f"Proxy          : {PROXIES}")
    print(colored("[+] Send crafted requests trying the different forward counts...", "yellow"))
    results = []
    req_headers = {"Max-Forwards": 0, "User-Agent": USER_AGENT}
    for method in METHODS:
        for t in range(forwards_count):
            req_max_forwards_value = str((t+1))  # Range is 0 indexed
            req_headers["Max-Forwards"] = req_max_forwards_value
            print(f"\rTesting '{method}' with 'Max-Forwards' to {req_max_forwards_value}", end="", flush=True)
            try:
                response = requests.request(method, target_url, headers=req_headers, verify=False, proxies=PROXIES, allow_redirects=False, timeout=10)
                results.append(ResponseData(method, response, req_max_forwards_value))
            except:
                pass
    print(f"\rDone{' ' * 60}")
    print(colored("[+] Results, if a methods does not appear then requests using it were rejected:", "yellow"))
    data = []
    data.append(["Method", "Max-Forwards", "HTTP Code", "Reference response headers values", "References response body HTML values"])
    for result in results:
        data.append([result.method, result.max_forwards_value, result.return_code, result.reference_headers_values, result.reference_html_values])
    print(tabulate(data, headers="firstrow", tablefmt="github", numalign="center", stralign="left"))


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Script to detect the different proxies in place by abusing the 'Max-Forwards' HTTP request header.")
    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("-f", action="store", dest="forwards_count", type=int, help="Total count of forwards to test (ex: 5).", required=False, default="5")
    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}
    main(args.target_url, args.forwards_count)
