diff --git a/requirements.txt b/requirements.txt index 77bbe78..5c1bb9e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,3 +15,4 @@ setuptools>=75.6.0 SQLAlchemy>=2.0.32 tldextract>=5.1.2 weasyprint>=65.0 +playwright diff --git a/subdominator/modules/cli/cli.py b/subdominator/modules/cli/cli.py index dc3dac3..38e4bd1 100644 --- a/subdominator/modules/cli/cli.py +++ b/subdominator/modules/cli/cli.py @@ -25,6 +25,10 @@ def cli(): parser.add_argument("-V", "--verbose", action="store_true") parser.add_argument("-sup", "--show-updates", action="store_true") parser.add_argument("-fw", "--filter-wildcards", action="store_true") + + # New Feature: Sourcemap Leakage Flag + parser.add_argument("-sm", "--sourcemap", action="store_true") + parser.add_argument("-json", "--json", action="store_true") parser.add_argument("-s", "--silent", action="store_true") parser.add_argument("-ir", "--include-resources", type=str,default=None) @@ -42,4 +46,4 @@ def cli(): Exit(1) except Exception as e: logger(f"Unahandled Exception occured in the CLI module due to: {e}", "warn") - Exit(1) \ No newline at end of file + Exit(1) diff --git a/subdominator/modules/handler.py b/subdominator/modules/handler.py index f74f6dd..3263b3d 100644 --- a/subdominator/modules/handler.py +++ b/subdominator/modules/handler.py @@ -32,6 +32,8 @@ from .version.version import version from .logger.logger import logger from .utils.utils import filters,reader,split_to_list, check_directory_permission,check_file_permission, Exit + # Import the new scanner module + from .scanner.sourcemap import check_sourcemap_leakage from .subscraper.abuseipdb.abuseipdb import abuseipdb from .subscraper.alienvault.alientvault import alienvault from .subscraper.anubis.anubis import anubis @@ -270,7 +272,28 @@ async def _domain_handler_(domain): file(output, domain, args) elif args.output_directory: dir(output, domain, args) - + + # START NEW FEATURE: Sourcemap Leakage Check + if args.sourcemap: + if not args.silent: + logger(f"Checking for Sourcemap Leakage on {len(final)} subdomains...", "info", args.no_color) + + # Use Semaphore to limit concurrent web requests during scanning + sem = asyncio.Semaphore(10) + + async def limited_check(sub): + async with sem: + return await check_sourcemap_leakage(sub, timeout=args.timeout) + + scan_tasks = [limited_check(sub) for sub in final] + scan_results = await asyncio.gather(*scan_tasks) + + for res in scan_results: + if res and res.get("vulnerable"): + msg = f"VULNERABLE: Sourcemap Leakage at {res['subdomain']} ({res['count']} maps found)" + logger(msg, "info", args.no_color) # You can customize this logger level + # END NEW FEATURE + async with AsyncSessionLocal() as db: await add_or_update_domain(db, domain, final) diff --git a/subdominator/modules/help/help.py b/subdominator/modules/help/help.py index 135b442..c30899c 100644 --- a/subdominator/modules/help/help.py +++ b/subdominator/modules/help/help.py @@ -31,7 +31,9 @@ def help(path, dbpath): {bold}{white}[{reset}{bold}{blue}OPTIMIZATION{reset}{bold}{white}]{reset}: {bold}{white}-t, --timeout : Set timeout value for API requests (default: 30s). - -fw, --filter-wildcards : Filter out wildcard subdomains.{reset} + -fw, --filter-wildcards : Filter out wildcard subdomains. + -sm, --sourcemap : Check for sourcemap leakage on discovered subdomains.{reset} + {bold}{white}[{reset}{bold}{blue}CONFIGURATION{reset}{bold}{white}]{reset}: @@ -64,4 +66,4 @@ def help(path, dbpath): -ls, --list-source : List available subdomain enumeration sources. -V, --verbose : Enable verbose output.{reset} """) - Exit() \ No newline at end of file + Exit() diff --git a/subdominator/modules/scanner/__init__.py b/subdominator/modules/scanner/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/subdominator/modules/scanner/__init__.py @@ -0,0 +1 @@ + diff --git a/subdominator/modules/scanner/sourcemap.py b/subdominator/modules/scanner/sourcemap.py new file mode 100644 index 0000000..e81fcd0 --- /dev/null +++ b/subdominator/modules/scanner/sourcemap.py @@ -0,0 +1,57 @@ +import asyncio +import warnings +from urllib.parse import urljoin +from bs4 import XMLParsedAsHTMLWarning +from playwright.async_api import async_playwright +from subdominator.modules.logger.logger import logger + +warnings.filterwarnings("ignore", category=XMLParsedAsHTMLWarning) + +async def check_sourcemap_leakage(subdomain, timeout=15): + """ + Uses Playwright to load a site and listen for all JS requests. + Checks for .map files for every JS resource discovered. + """ + found_maps = [] + url = f"http://{subdomain}" + + try: + async with async_playwright() as p: + browser = await p.chromium.launch(headless=True) + context = await browser.new_context(ignore_https_errors=True) + page = await context.new_page() + + js_urls = set() + + page.on("request", lambda request: js_urls.add(request.url) + if request.resource_type == "script" else None) + + try: + await page.goto(url, timeout=timeout * 1000, wait_until="networkidle") + except Exception: + await page.goto(f"https://{subdomain}", timeout=timeout * 1000, wait_until="networkidle") + + await browser.close() + + import httpx + async with httpx.AsyncClient(verify=False, timeout=5.0) as client: + for js_url in js_urls: + map_url = f"{js_url}.map" + try: + res = await client.get(map_url) + if res.status_code == 200 and "application/json" in res.headers.get("Content-Type", ""): + found_maps.append(map_url) + except Exception: + continue + + if len(found_maps) > 3: + return { + "subdomain": subdomain, + "vulnerable": True, + "count": len(found_maps), + "files": found_maps + } + except Exception: + pass + + return None