342 lines
12 KiB
Python
342 lines
12 KiB
Python
"""
|
|
©AngelaMos | 2025
|
|
SQL injection vulnerability scanner
|
|
|
|
Tests error based, boolean based, and time based blind SQLi
|
|
"""
|
|
|
|
import time
|
|
import statistics
|
|
from typing import Any
|
|
|
|
from ..core.enums import (
|
|
ScanStatus,
|
|
Severity,
|
|
TestType,
|
|
)
|
|
from ..schemas.test_result_schemas import TestResultCreate
|
|
|
|
from .payloads import SQLiPayloads
|
|
from .base_scanner import BaseScanner
|
|
|
|
|
|
class SQLiScanner(BaseScanner):
|
|
"""
|
|
Tests for SQL injection vulnerabilities
|
|
|
|
Detects:
|
|
- Error-based SQLi (database error messages)
|
|
- Boolean-based blind SQLi (response differences)
|
|
- Time-based blind SQLi (response timing analysis)
|
|
|
|
Uses payloads covering MySQL, PostgreSQL, MSSQL, Oracle
|
|
"""
|
|
def scan(self) -> TestResultCreate:
|
|
"""
|
|
Execute SQL injection tests
|
|
|
|
Returns:
|
|
TestResultCreate: Scan result with findings
|
|
"""
|
|
error_based_test = self._test_error_based_sqli()
|
|
if error_based_test["vulnerable"]:
|
|
return self._create_vulnerable_result(
|
|
details =
|
|
f"Error-based SQL injection detected: {error_based_test['database_type']}",
|
|
evidence = error_based_test,
|
|
severity = Severity.CRITICAL,
|
|
recommendations = [
|
|
"Use parameterized queries (prepared statements)",
|
|
"Never concatenate user input into SQL queries",
|
|
"Implement input validation and sanitization",
|
|
"Disable detailed error messages in production",
|
|
"Use ORM frameworks with proper escaping",
|
|
],
|
|
)
|
|
|
|
boolean_based_test = self._test_boolean_based_sqli()
|
|
if boolean_based_test["vulnerable"]:
|
|
return self._create_vulnerable_result(
|
|
details = "Boolean-based blind SQL injection detected",
|
|
evidence = boolean_based_test,
|
|
severity = Severity.CRITICAL,
|
|
recommendations = [
|
|
"Use parameterized queries for all database operations",
|
|
"Implement proper input validation",
|
|
"Avoid exposing different responses for true/false conditions",
|
|
],
|
|
)
|
|
|
|
time_based_test = self._test_time_based_sqli()
|
|
if time_based_test["vulnerable"]:
|
|
return self._create_vulnerable_result(
|
|
details =
|
|
f"Time-based blind SQL injection detected: {time_based_test['database_type']}",
|
|
evidence = time_based_test,
|
|
severity = Severity.CRITICAL,
|
|
recommendations = [
|
|
"Use parameterized queries exclusively",
|
|
"Implement strict input validation",
|
|
"Monitor for unusual response time patterns",
|
|
],
|
|
)
|
|
|
|
return TestResultCreate(
|
|
test_name = TestType.SQLI,
|
|
status = ScanStatus.SAFE,
|
|
severity = Severity.INFO,
|
|
details = "No SQL injection vulnerabilities detected",
|
|
evidence_json = {
|
|
"error_based_test": error_based_test,
|
|
"boolean_based_test": boolean_based_test,
|
|
"time_based_test": time_based_test,
|
|
},
|
|
recommendations_json = [
|
|
"Continue using parameterized queries",
|
|
"Regularly update security testing",
|
|
],
|
|
)
|
|
|
|
def _test_error_based_sqli(self) -> dict[str, Any]:
|
|
"""
|
|
Test for error based SQL injection
|
|
|
|
Detects database errors in responses indicating SQLi vulnerability
|
|
|
|
Returns:
|
|
dict[str, Any]: Error-based SQLi test results
|
|
"""
|
|
error_signatures = SQLiPayloads.get_error_signatures()
|
|
|
|
basic_payloads = SQLiPayloads.BASIC_AUTHENTICATION_BYPASS
|
|
|
|
for payload in basic_payloads:
|
|
try:
|
|
response = self.make_request("GET", f"/?id={payload}")
|
|
|
|
response_text_lower = response.text.lower()
|
|
|
|
for db_type, signatures in error_signatures.items():
|
|
for signature in signatures:
|
|
if signature in response_text_lower:
|
|
return {
|
|
"vulnerable": True,
|
|
"database_type": db_type,
|
|
"payload": payload,
|
|
"status_code": response.status_code,
|
|
"error_signature": signature,
|
|
"response_excerpt":
|
|
response.text[: 500],
|
|
}
|
|
|
|
except Exception:
|
|
continue
|
|
|
|
return {
|
|
"vulnerable": False,
|
|
"payloads_tested": len(basic_payloads),
|
|
"description": "No database errors detected",
|
|
}
|
|
|
|
def _test_boolean_based_sqli(self) -> dict[str, Any]:
|
|
"""
|
|
Test for boolean based blind SQL injection
|
|
|
|
Compares responses from true vs false conditions to detect SQLi
|
|
|
|
Returns:
|
|
dict[str, Any]: Boolean based SQLi test results
|
|
"""
|
|
try:
|
|
baseline_response = self.make_request("GET", "/?id=1")
|
|
baseline_length = len(baseline_response.text)
|
|
baseline_status = baseline_response.status_code
|
|
|
|
if baseline_status != 200:
|
|
return {
|
|
"vulnerable": False,
|
|
"description": "Baseline request failed",
|
|
"baseline_status": baseline_status,
|
|
}
|
|
|
|
boolean_payloads = SQLiPayloads.BOOLEAN_BASED_BLIND
|
|
true_payloads = [
|
|
p for p in boolean_payloads
|
|
if "AND '1'='1" in p or "AND 1=1" in p
|
|
]
|
|
false_payloads = [
|
|
p for p in boolean_payloads if "AND '1'='2" in p
|
|
or "AND 1=2" in p or "AND 1=0" in p
|
|
]
|
|
|
|
true_lengths = []
|
|
for payload in true_payloads:
|
|
response = self.make_request("GET", f"/?id={payload}")
|
|
true_lengths.append(len(response.text))
|
|
|
|
false_lengths = []
|
|
for payload in false_payloads:
|
|
response = self.make_request("GET", f"/?id={payload}")
|
|
false_lengths.append(len(response.text))
|
|
|
|
avg_true = statistics.mean(true_lengths)
|
|
avg_false = statistics.mean(false_lengths)
|
|
|
|
length_diff = abs(avg_true - avg_false)
|
|
|
|
if length_diff > 100 and avg_true != avg_false:
|
|
return {
|
|
"vulnerable":
|
|
True,
|
|
"baseline_length":
|
|
baseline_length,
|
|
"true_condition_avg_length":
|
|
avg_true,
|
|
"false_condition_avg_length":
|
|
avg_false,
|
|
"length_difference":
|
|
length_diff,
|
|
"confidence":
|
|
"HIGH" if length_diff > 500 else "MEDIUM",
|
|
}
|
|
|
|
return {
|
|
"vulnerable": False,
|
|
"description": "No boolean-based SQLi detected",
|
|
"length_difference": length_diff,
|
|
}
|
|
|
|
except Exception as e:
|
|
return {
|
|
"vulnerable": False,
|
|
"error": str(e),
|
|
"description": "Error testing boolean-based SQLi",
|
|
}
|
|
|
|
def _test_time_based_sqli(self,
|
|
delay_seconds: int = 5) -> dict[str,
|
|
Any]:
|
|
"""
|
|
Test for time based blind SQL injection
|
|
|
|
Uses baseline timing comparison with statistical analysis
|
|
for false positive reduction
|
|
|
|
Args:
|
|
delay_seconds: Delay to inject (from settings)
|
|
|
|
Returns:
|
|
dict[str, Any]: Time-based SQLi test results
|
|
"""
|
|
try:
|
|
baseline_mean, baseline_stdev = self.get_baseline_timing("/")
|
|
|
|
threshold = baseline_mean + (3 * baseline_stdev)
|
|
expected_delay_time = baseline_mean + delay_seconds
|
|
|
|
all_time_payloads = SQLiPayloads.TIME_BASED_BLIND
|
|
|
|
delay_payloads = {
|
|
"mysql":
|
|
[p for p in all_time_payloads if "SLEEP" in p],
|
|
"postgres": [
|
|
p for p in all_time_payloads if "pg_sleep" in p
|
|
],
|
|
"mssql": [
|
|
p for p in all_time_payloads if "WAITFOR" in p
|
|
],
|
|
}
|
|
|
|
for db_type, payloads in delay_payloads.items():
|
|
for payload in payloads:
|
|
delay_times = []
|
|
|
|
for _ in range(3):
|
|
try:
|
|
response = self.make_request(
|
|
"GET",
|
|
f"/?id={payload}",
|
|
timeout = delay_seconds + 10,
|
|
)
|
|
elapsed = getattr(
|
|
response,
|
|
"request_time",
|
|
0.0
|
|
)
|
|
delay_times.append(elapsed)
|
|
|
|
except Exception:
|
|
delay_times.append(delay_seconds + 10)
|
|
|
|
time.sleep(1)
|
|
|
|
avg_delay = statistics.mean(delay_times)
|
|
|
|
if avg_delay >= expected_delay_time - 1:
|
|
confidence = (
|
|
"HIGH" if avg_delay >= expected_delay_time
|
|
else "MEDIUM"
|
|
)
|
|
|
|
return {
|
|
"vulnerable":
|
|
True,
|
|
"database_type":
|
|
db_type,
|
|
"payload":
|
|
payload,
|
|
"baseline_time":
|
|
f"{baseline_mean:.3f}s",
|
|
"response_time":
|
|
f"{avg_delay:.3f}s",
|
|
"expected_delay":
|
|
f"{expected_delay_time:.3f}s",
|
|
"confidence":
|
|
confidence,
|
|
"individual_times":
|
|
[f"{t:.3f}s" for t in delay_times],
|
|
}
|
|
|
|
return {
|
|
"vulnerable": False,
|
|
"baseline_time": f"{baseline_mean:.3f}s",
|
|
"threshold": f"{threshold:.3f}s",
|
|
"description": "No time-based SQLi detected",
|
|
}
|
|
|
|
except Exception as e:
|
|
return {
|
|
"vulnerable": False,
|
|
"error": str(e),
|
|
"description": "Error testing time-based SQLi",
|
|
}
|
|
|
|
def _create_vulnerable_result(
|
|
self,
|
|
details: str,
|
|
evidence: dict[str,
|
|
Any],
|
|
severity: Severity = Severity.CRITICAL,
|
|
recommendations: list[str] | None = None,
|
|
) -> TestResultCreate:
|
|
"""
|
|
Create a vulnerable scan result
|
|
|
|
Args:
|
|
details: Vulnerability description
|
|
evidence: Evidence dictionary
|
|
severity: Vulnerability severity
|
|
recommendations: List of remediation recommendations
|
|
|
|
Returns:
|
|
TestResultCreate: Vulnerable result
|
|
"""
|
|
return TestResultCreate(
|
|
test_name = TestType.SQLI,
|
|
status = ScanStatus.VULNERABLE,
|
|
severity = severity,
|
|
details = details,
|
|
evidence_json = evidence,
|
|
recommendations_json = recommendations or [],
|
|
)
|