Files
mukul975 c47eed6a64 Production hardening: security fixes, code quality, 724 skills complete
- Fix 25 shell=True subprocess calls with list-based commands
- Fix 49 verify=False in defensive skills (env-var override)
- Add timeout to 231 HTTP/subprocess/socket calls
- Fix 6 SQL injection patterns with whitelist validation
- Replace 8 __import__() with standard imports
- Remove 701 unused imports across 442 files
- Add authorized-testing disclaimers to all offensive skills
- Complete 11 incomplete skill directories
- Expand 10 stub SKILL.md files with full content
- Fix 2 YAML parse errors in frontmatter
- Fix 5 pre-existing syntax errors
- Convert 22 hardcoded paths/ports to environment variables
- Back up 21 redundant skill pairs to .bak
- Fix 2 global declaration errors
- 724/724 skills with full folder anatomy (SKILL.md + agent.py + api-reference.md + LICENSE)
- 0 compile errors across all 724 agent.py files
2026-03-19 13:26:49 +01:00

183 lines
6.7 KiB
Python

#!/usr/bin/env python3
# For authorized penetration testing and lab environments only
"""Forced Browsing Authentication Bypass Agent - Tests for unprotected endpoints."""
import json
import logging
import argparse
from datetime import datetime
from urllib.parse import urljoin
import requests
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger(__name__)
DEFAULT_ADMIN_PATHS = [
"/admin", "/administrator", "/admin-panel", "/wp-admin", "/cpanel",
"/phpmyadmin", "/adminer", "/manager", "/console", "/debug",
"/actuator", "/actuator/env", "/actuator/health", "/actuator/beans",
"/swagger-ui", "/swagger-ui.html", "/api-docs", "/graphql", "/graphiql",
"/.env", "/server-status", "/server-info", "/.git/HEAD", "/.git/config",
"/web.config", "/phpinfo.php", "/robots.txt", "/sitemap.xml",
]
SENSITIVE_EXTENSIONS = [
".bak", ".old", ".orig", ".save", ".swp", ".tmp", ".config",
".sql", ".gz", ".tar", ".zip", ".env",
]
def load_wordlist(wordlist_path):
"""Load directory/file wordlist from file."""
with open(wordlist_path, "r") as f:
return [line.strip() for line in f if line.strip() and not line.startswith("#")]
def test_endpoint(base_url, path, session_cookie=None, timeout=10):
"""Test a single endpoint with and without authentication."""
url = urljoin(base_url, path)
unauth_resp = requests.get(url, timeout=timeout, allow_redirects=False, verify=False)
auth_resp = None
if session_cookie:
auth_resp = requests.get(
url, cookies={"session": session_cookie},
timeout=timeout, allow_redirects=False, verify=False,
)
result = {
"path": path,
"url": url,
"unauth_status": unauth_resp.status_code,
"unauth_size": len(unauth_resp.content),
}
if auth_resp:
result["auth_status"] = auth_resp.status_code
result["auth_size"] = len(auth_resp.content)
result["auth_bypass"] = (
unauth_resp.status_code == 200 and auth_resp.status_code == 200
and abs(result["unauth_size"] - result["auth_size"]) < 100
)
return result
def enumerate_directories(base_url, wordlist, session_cookie=None):
"""Enumerate directories and test authentication enforcement."""
findings = []
for word in wordlist:
path = f"/{word}" if not word.startswith("/") else word
try:
result = test_endpoint(base_url, path, session_cookie)
if result["unauth_status"] in (200, 301, 302, 403):
findings.append(result)
logger.info(
"Found: %s (status: %d, size: %d)",
path, result["unauth_status"], result["unauth_size"],
)
except requests.RequestException:
continue
return findings
def test_http_method_bypass(base_url, path):
"""Test HTTP method-based authentication bypass."""
methods = ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"]
results = {}
for method in methods:
try:
resp = requests.request(method, urljoin(base_url, path), timeout=10, verify=False)
results[method] = resp.status_code
except requests.RequestException:
results[method] = None
logger.info("Method bypass test for %s: %s", path, results)
return results
def test_path_traversal_bypass(base_url, path):
"""Test path normalization bypass techniques."""
variants = [
path,
path.upper(),
path.replace("/", "/./"),
f"/public/..{path}",
path.replace("/", "%2f"),
f"/;{path}",
f"/.;{path}",
f"{path}/",
f"{path}.json",
]
results = []
for variant in variants:
try:
resp = requests.get(urljoin(base_url, variant), timeout=10, verify=False)
results.append({"path": variant, "status": resp.status_code, "size": len(resp.content)})
except requests.RequestException:
continue
return results
def check_sensitive_files(base_url):
"""Check for exposed backup and configuration files."""
sensitive_paths = [
".env", ".git/HEAD", ".git/config", "web.config", "wp-config.php.bak",
"config.php.old", ".htpasswd", "database.yml", "phpinfo.php",
]
exposed = []
for path in sensitive_paths:
try:
resp = requests.get(urljoin(base_url, path), timeout=10, verify=False)
if resp.status_code == 200 and len(resp.content) > 0:
exposed.append({"path": path, "status": resp.status_code, "size": len(resp.content)})
logger.warning("EXPOSED: %s (size: %d bytes)", path, len(resp.content))
except requests.RequestException:
continue
return exposed
def generate_report(findings, method_results, sensitive_files):
"""Generate pentest finding report for forced browsing results."""
report = {
"timestamp": datetime.utcnow().isoformat(),
"total_endpoints_found": len(findings),
"auth_bypass_candidates": [f for f in findings if f.get("auth_bypass")],
"accessible_without_auth": [f for f in findings if f["unauth_status"] == 200],
"http_method_bypass": method_results,
"sensitive_files_exposed": sensitive_files,
}
bypasses = len(report["auth_bypass_candidates"])
logger.info("Report: %d endpoints, %d auth bypasses, %d sensitive files",
len(findings), bypasses, len(sensitive_files))
return report
def main():
parser = argparse.ArgumentParser(description="Forced Browsing Authentication Bypass Agent")
parser.add_argument("--target", required=True, help="Target base URL")
parser.add_argument("--wordlist", help="Path to wordlist file")
parser.add_argument("--session-cookie", help="Valid session cookie for auth comparison")
parser.add_argument("--admin-paths", action="store_true", help="Test common admin paths")
parser.add_argument("--output", default="forced_browsing_report.json")
args = parser.parse_args()
wordlist = DEFAULT_ADMIN_PATHS if args.admin_paths else []
if args.wordlist:
wordlist = load_wordlist(args.wordlist)
findings = enumerate_directories(args.target, wordlist, args.session_cookie)
method_results = {}
for f in findings:
if f["unauth_status"] in (401, 403):
method_results[f["path"]] = test_http_method_bypass(args.target, f["path"])
sensitive = check_sensitive_files(args.target)
report = generate_report(findings, method_results, sensitive)
with open(args.output, "w") as f:
json.dump(report, f, indent=2)
logger.info("Report saved to %s", args.output)
if __name__ == "__main__":
main()