Lucene search
+L

404 bypass tester

🗓️ 11 Dec 2025 15:52:31Reported by KittySploitType 
kittysploit
 kittysploit
🔗 kittysploit.com👁 2 Views

404 bypass tester script to identify web resources that bypass 404 status codes.

Refs
Code
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from kittysploit import *
from lib.protocols.http.http_client import Http_client
from lib.scanner.http.bypass_confirm import is_confirmed_bypass
import os


class Module(Auxiliary, Http_client):

    __info__ = {
        'name': '404 bypass tester',
        'description': (
            "Try to bypass 404 on a web resource. Candidates that merely "
            "collapse to the homepage (e.g. /admin/../) are discarded."
        ),
        'author': 'KittySploit Team',
        'tags': ['web', 'scanner', 'bypass', '404'],
        'agent': {
            'risk': 'active',
            'effects': ['network_probe'],
            'expected_requests': 12,
            'reversible': True,
            'approval_required': False,
            'produces': ['risk_signals', 'endpoints'],
            'cost': 1.0,
            'noise': 0.4,
            'value': 0.8,
            'requires': {
                'min_endpoints': 0,
                'min_params': 0,
                'tech_hints_any': [],
                'tech_hints_all': [],
                'specializations_any': [],
                'risk_signals_any': [],
                'auth_session': False,
                'capabilities_any': [],
                'capabilities_all': [],
                'confidence_min': {},
                'confidence_min_any': {},
                'endpoint_pattern_any': [],
                'param_any': [],
                'api_surface_ready': False,
            },
            'chain': {
                'produces_capabilities': [{'capability': 'endpoints', 'from_detail': ''}],
                'consumes_capabilities': [],
                'option_bindings': {},
                'suggested_followups': [],
            },
        },
    }

    path = OptString("/admin", "Chemin ou ressource à tester", required=True)
    extra_wordlist = OptFile("", "File with additional paths (one per line)", required=False, advanced=True)
    compare_soft404 = OptBool(True, "Compare the body with the reference 404 response", required=False, advanced=True)

    def check(self):
        """Vérifie que la cible est joignable."""
        try:
            resp = self.http_request(method="GET", path="/", allow_redirects=False)
            if resp and resp.status_code in [200, 301, 302, 401, 403, 404]:
                return True
            print_error("The target is not reachable.")
            return False
        except Exception as e:
            print_error(f"Error during check: {e}")
            return False

    def _normalize_path(self, path_str):
        path_str = path_str.strip()
        if not path_str.startswith("/"):
            path_str = f"/{path_str}"
        return path_str

    def _load_extra_paths(self):
        if not self.extra_wordlist:
            return []
        if not os.path.isfile(self.extra_wordlist):
            print_warning(f"Wordlist not found: {self.extra_wordlist}")
            return []
        extra = []
        try:
            with open(self.extra_wordlist, "r", encoding="utf-8", errors="ignore") as f:
                for line in f:
                    line = line.strip()
                    if line:
                        extra.append(self._normalize_path(line))
        except Exception as e:
            print_warning(f"Unable to read wordlist: {e}")
        return extra

    def _base_variations(self, base_path):
        clean = self._normalize_path(base_path.rstrip("/")) if base_path != "/" else "/"
        encoded = clean.replace("/", "%2f")

        suffixes = [".", ".json", ".html", ".php", ".bak", ".old", "~"]
        injections = [
            "",
            "/",
            "/.",
            "/%2e/",
            "%2e",
            "%2f",
            "%2f/",
            "%20",
            "%09",
            "%00",
            ";;/",
            "/..;/",
            "/.;/",
            "/./",
            "/../",
            "//",
            "/?",
            "/?bypass=1",
            "/#",
        ]

        variants = []
        for inj in injections:
            variants.append((f"path:{inj or 'base'}", f"{clean}{inj}", None))

        for suffix in suffixes:
            variants.append((f"suffix:{suffix}", f"{clean}{suffix}", None))

        variants.append(("double_slash_prefix", f"//{clean.lstrip('/')}", None))
        variants.append(("dot_prefix", f"/./{clean.lstrip('/')}", None))
        variants.append(("encoded_path", encoded, None))
        variants.append(("upper_case", clean.upper(), None))
        variants.append(("lower_case", clean.lower(), None))

        # Header tricks: request "/" with rewrite headers pointing at the target.
        header_variants = [
            ("x-original-url", "/", {"X-Original-URL": clean}),
            ("x-rewrite-url", "/", {"X-Rewrite-URL": clean}),
            ("x-original-forwarded-for", "/", {"X-Original-URL": clean, "X-Forwarded-For": "127.0.0.1"}),
            ("x-forwarded-for", clean, {"X-Forwarded-For": "127.0.0.1"}),
            ("x-forwarded-host", clean, {"X-Forwarded-Host": "127.0.0.1"}),
            ("x-client-ip", clean, {"X-Client-IP": "127.0.0.1"}),
            ("referer-root", clean, {"Referer": "/"}),
        ]
        variants.extend(header_variants)

        return variants

    def _response_signature(self, response):
        if response is None:
            return None
        body = response.text or ""
        return {
            "status": response.status_code,
            "length": len(body),
            "content_type": response.headers.get("Content-Type", ""),
        }

    def _fetch(self, path, headers=None):
        try:
            return self.http_request(
                method="GET",
                path=path,
                headers=headers or {},
                allow_redirects=False,
            )
        except Exception as e:
            print_debug(f"Request failed for {path}: {e}")
            return None

    def run(self):
        print_status(f"Target: {self.target}:{self.port} | Test the path {self.path}")
        target_path = self._normalize_path(str(self.path or "/admin"))

        home_sig = self._response_signature(self._fetch("/"))
        # Soft-404 baseline: random child under the target path.
        baseline_resp = self._fetch(f"{target_path.rstrip('/')}/{self.random_text(12)}")
        baseline_sig = self._response_signature(baseline_resp)
        # Also sample the exact path (often identical soft-404).
        path_sig = self._response_signature(self._fetch(target_path))
        if path_sig and (not baseline_sig or path_sig.get("status") == 404):
            baseline_sig = path_sig

        if baseline_sig:
            print_info(
                f"Reference response: status={baseline_sig['status']} len={baseline_sig['length']}"
            )
        else:
            print_warning("Unable to get a reference response, the differences will be less reliable.")
        if home_sig:
            print_info(f"Homepage control: status={home_sig['status']} len={home_sig['length']}")

        variants = self._base_variations(target_path)
        for extra in self._load_extra_paths():
            variants.append((f"extra:{extra}", extra, None))

        seen = set()
        filtered = []
        for name, path, headers in variants:
            key = (path, frozenset((headers or {}).items()))
            if key in seen:
                continue
            seen.add(key)
            filtered.append((name, path, headers))

        findings = []
        confirmed = 0
        for name, path, headers in filtered:
            resp = self._fetch(path, headers=headers)
            sig = self._response_signature(resp)
            interesting = False
            if sig and self.compare_soft404:
                interesting = is_confirmed_bypass(
                    sig,
                    blocked_sig=baseline_sig,
                    home_sig=home_sig,
                    name=name,
                    path=path,
                    target_path=target_path,
                    blocked_status=404,
                )
            elif sig and not self.compare_soft404:
                interesting = sig["status"] not in {404, 400} and not (
                    home_sig and sig["status"] == home_sig.get("status")
                    and abs(sig["length"] - int(home_sig.get("length") or 0)) < 80
                )
            if sig:
                note = "yes" if interesting else "no"
                findings.append([name, path, sig['status'], sig['length'], note])
                if interesting:
                    confirmed += 1
                    print_success(
                        f"[{name}] Confirmed bypass candidate: {path} -> "
                        f"{sig['status']} (len={sig['length']})"
                    )
            else:
                findings.append([name, path, "err", 0, "no"])

        print_table(['Test', 'Path', 'Code', 'Size', 'Diff?'], findings)
        if confirmed:
            print_success(f"Confirmed bypass candidates: {confirmed}")
        else:
            print_info("No confirmed 404 bypass (homepage collapses / errors filtered out).")
        return True

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation

28 Jul 2026 14:48Current
5.9Medium risk
Vulners AI Score5.9
2