blackops-sql 0.1.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- blackops_sql-0.1.6.dist-info/METADATA +250 -0
- blackops_sql-0.1.6.dist-info/RECORD +29 -0
- blackops_sql-0.1.6.dist-info/WHEEL +4 -0
- blackops_sql-0.1.6.dist-info/entry_points.txt +2 -0
- blackops_sql-0.1.6.dist-info/licenses/LICENSE +661 -0
- blackops_sql-0.1.6.dist-info/licenses/NOTICE +27 -0
- blackopssql/__init__.py +111 -0
- blackopssql/__main__.py +287 -0
- blackopssql/_cli/__init__.py +0 -0
- blackopssql/_cli/args.py +229 -0
- blackopssql/_cli/summary.py +216 -0
- blackopssql/engine/__init__.py +35 -0
- blackopssql/engine/_scanner/__init__.py +0 -0
- blackopssql/engine/_scanner/active/__init__.py +526 -0
- blackopssql/engine/_scanner/active/_helpers.py +301 -0
- blackopssql/engine/_scanner/blind.py +315 -0
- blackopssql/engine/_scanner/extract.py +302 -0
- blackopssql/engine/_scanner/options.py +96 -0
- blackopssql/engine/_scanner/passive.py +86 -0
- blackopssql/engine/_scanner/payloads/__init__.py +80 -0
- blackopssql/engine/_scanner/pipeline.py +547 -0
- blackopssql/engine/_scanner/stacked.py +131 -0
- blackopssql/engine/crawler.py +7 -0
- blackopssql/engine/http/__init__.py +0 -0
- blackopssql/engine/http/injector.py +10 -0
- blackopssql/engine/http/waf_detect.py +51 -0
- blackopssql/engine/log.py +7 -0
- blackopssql/engine/reporter.py +208 -0
- blackopssql/engine/scanner.py +95 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
# Copyright (c) 2026 CommonHuman-Lab
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import io
|
|
6
|
+
import re as _re
|
|
7
|
+
import sys
|
|
8
|
+
import urllib.parse as _up
|
|
9
|
+
|
|
10
|
+
from blackops_cli.colour import BOLD, CYAN, DIM, GREEN, RED, YELLOW
|
|
11
|
+
|
|
12
|
+
_ANSI_RE = _re.compile(r"\x1b\[[0-9;]*m")
|
|
13
|
+
|
|
14
|
+
_MARKER_RE = _re.compile(r"BreachSQL_[A-Za-z0-9]+")
|
|
15
|
+
_CHAR_RE = _re.compile(r"\bchar\(\d[\d,]+\)")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _ascii_table(columns: list[str], rows: list[list], max_col_w: int = 55) -> list[str]:
|
|
19
|
+
"""Return lines for a bordered ASCII table."""
|
|
20
|
+
widths = [len(c) for c in columns]
|
|
21
|
+
for row in rows:
|
|
22
|
+
for i, cell in enumerate(row):
|
|
23
|
+
if i < len(widths):
|
|
24
|
+
widths[i] = min(max_col_w, max(widths[i], len(str(cell))))
|
|
25
|
+
|
|
26
|
+
def border() -> str:
|
|
27
|
+
return "+" + "+".join("-" * (w + 2) for w in widths) + "+"
|
|
28
|
+
|
|
29
|
+
def fmt_row(cells: list) -> str:
|
|
30
|
+
parts = []
|
|
31
|
+
for i, cell in enumerate(cells):
|
|
32
|
+
w = widths[i] if i < len(widths) else 0
|
|
33
|
+
s = str(cell)
|
|
34
|
+
if len(s) > w:
|
|
35
|
+
s = s[:w - 3] + "..."
|
|
36
|
+
parts.append(f" {s.ljust(w)} ")
|
|
37
|
+
return "|" + "|".join(parts) + "|"
|
|
38
|
+
|
|
39
|
+
lines = [border(), fmt_row(columns), border()]
|
|
40
|
+
for row in rows:
|
|
41
|
+
lines.append(fmt_row(row))
|
|
42
|
+
lines.append(border())
|
|
43
|
+
return lines
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _clean_payload(payload: str) -> str:
|
|
47
|
+
"""Replace random marker strings with a stable placeholder."""
|
|
48
|
+
s = _MARKER_RE.sub("<marker>", payload)
|
|
49
|
+
s = _CHAR_RE.sub("char(<marker>)", s)
|
|
50
|
+
return s
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _proof_url(url: str, param: str, payload: str, original: str = "1") -> str:
|
|
54
|
+
"""
|
|
55
|
+
Build a proof-of-concept URL by injecting *payload* into *param*.
|
|
56
|
+
|
|
57
|
+
The payload is appended to the original param value (matching how the
|
|
58
|
+
scanner injects it) so the link reproduces the exact request that
|
|
59
|
+
triggered the finding. The result is percent-encoded so it is safe
|
|
60
|
+
to paste into a browser address bar or terminal.
|
|
61
|
+
"""
|
|
62
|
+
try:
|
|
63
|
+
parsed = _up.urlparse(url)
|
|
64
|
+
qs = _up.parse_qs(parsed.query, keep_blank_values=True)
|
|
65
|
+
orig_val = qs.get(param, [original])[0]
|
|
66
|
+
injected = orig_val + payload
|
|
67
|
+
qs[param] = [injected]
|
|
68
|
+
new_query = _up.urlencode(qs, doseq=True)
|
|
69
|
+
return _up.urlunparse(parsed._replace(query=new_query))
|
|
70
|
+
except Exception:
|
|
71
|
+
return ""
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def print_summary(result) -> None:
|
|
75
|
+
print()
|
|
76
|
+
print(BOLD("=" * 60))
|
|
77
|
+
print(BOLD(" BlackOpsSQL — Scan Summary"))
|
|
78
|
+
print(BOLD("=" * 60))
|
|
79
|
+
print(f" Target : {result.target}")
|
|
80
|
+
print(f" Duration : {result.duration_s}s")
|
|
81
|
+
print(f" Requests sent : {result.requests_sent}")
|
|
82
|
+
print(f" URLs crawled : {result.crawled_urls}")
|
|
83
|
+
print(f" Params tested : {result.params_tested}")
|
|
84
|
+
print(f" WAF detected : {result.waf_detected or 'None'}")
|
|
85
|
+
print(f" Evasion used : {result.evasion_applied or 'None'}")
|
|
86
|
+
print(f" DBMS detected : {result.dbms_detected or 'Unknown'}")
|
|
87
|
+
print()
|
|
88
|
+
|
|
89
|
+
if result.total_findings == 0:
|
|
90
|
+
print(DIM(" No findings."))
|
|
91
|
+
else:
|
|
92
|
+
print(GREEN(f" Total findings: {result.total_findings}"))
|
|
93
|
+
print()
|
|
94
|
+
i = 1
|
|
95
|
+
|
|
96
|
+
for f in result.error_based:
|
|
97
|
+
print(f" {i}. {RED('[ERROR-BASED SQLi]')} Confirmed")
|
|
98
|
+
print(f" Param : {f.parameter}")
|
|
99
|
+
print(f" URL : {f.url}")
|
|
100
|
+
print(f" Method : {f.method}")
|
|
101
|
+
print(f" DBMS : {f.dbms}")
|
|
102
|
+
print(f" Payload : {f.payload}")
|
|
103
|
+
if f.method.upper() == "GET":
|
|
104
|
+
print(f" Proof : {CYAN(_proof_url(f.url, f.parameter, f.payload))}")
|
|
105
|
+
print()
|
|
106
|
+
i += 1
|
|
107
|
+
|
|
108
|
+
for f in result.boolean_based:
|
|
109
|
+
status = GREEN("[CONFIRMED]") if f.confirmed else YELLOW("[LIKELY]")
|
|
110
|
+
print(f" {i}. {status} {YELLOW('Boolean-based SQLi')}")
|
|
111
|
+
print(f" Param : {f.parameter}")
|
|
112
|
+
print(f" URL : {f.url}")
|
|
113
|
+
print(f" Method : {f.method}")
|
|
114
|
+
print(f" True payload : {f.payload_true}")
|
|
115
|
+
print(f" False payload : {f.payload_false}")
|
|
116
|
+
print(f" Diff score : {f.diff_score:.2f}")
|
|
117
|
+
if f.method.upper() == "GET":
|
|
118
|
+
print(f" Proof (true) : {CYAN(_proof_url(f.url, f.parameter, f.payload_true))}")
|
|
119
|
+
print(f" Proof (false): {CYAN(_proof_url(f.url, f.parameter, f.payload_false))}")
|
|
120
|
+
print()
|
|
121
|
+
i += 1
|
|
122
|
+
|
|
123
|
+
for f in result.time_based:
|
|
124
|
+
print(f" {i}. {CYAN('[TIME-BASED BLIND SQLi]')}")
|
|
125
|
+
print(f" Param : {f.parameter}")
|
|
126
|
+
print(f" URL : {f.url}")
|
|
127
|
+
print(f" Method : {f.method}")
|
|
128
|
+
print(f" DBMS hint : {f.dbms}")
|
|
129
|
+
print(f" Payload : {f.payload}")
|
|
130
|
+
print(f" Delay : {f.observed_delay:.2f}s (threshold: {f.threshold}s)")
|
|
131
|
+
if f.method.upper() == "GET":
|
|
132
|
+
print(f" Proof : {CYAN(_proof_url(f.url, f.parameter, f.payload))}")
|
|
133
|
+
print()
|
|
134
|
+
i += 1
|
|
135
|
+
|
|
136
|
+
for f in result.union_based:
|
|
137
|
+
print(f" {i}. {RED('[UNION-BASED SQLi]')} Confirmed")
|
|
138
|
+
print(f" Param : {f.parameter}")
|
|
139
|
+
print(f" URL : {f.url}")
|
|
140
|
+
print(f" Method : {f.method}")
|
|
141
|
+
print(f" Columns : {f.column_count}")
|
|
142
|
+
print(f" Payload : {_clean_payload(f.payload)}")
|
|
143
|
+
if f.method.upper() == "GET":
|
|
144
|
+
print(f" Proof : {CYAN(_proof_url(f.url, f.parameter, f.payload))}")
|
|
145
|
+
print()
|
|
146
|
+
i += 1
|
|
147
|
+
|
|
148
|
+
for f in result.oob:
|
|
149
|
+
print(f" {i}. {CYAN('[OOB SQLi]')} Payload injected")
|
|
150
|
+
print(f" Param : {f.parameter}")
|
|
151
|
+
print(f" URL : {f.url}")
|
|
152
|
+
print(f" Callback : {f.callback_url}")
|
|
153
|
+
print(f" Payload : {f.payload}")
|
|
154
|
+
if f.method.upper() == "GET":
|
|
155
|
+
print(f" Proof : {CYAN(_proof_url(f.url, f.parameter, f.payload))}")
|
|
156
|
+
print()
|
|
157
|
+
i += 1
|
|
158
|
+
|
|
159
|
+
for f in result.stacked:
|
|
160
|
+
print(f" {i}. {RED('[STACKED QUERY SQLi]')} Confirmed")
|
|
161
|
+
print(f" Param : {f.parameter}")
|
|
162
|
+
print(f" URL : {f.url}")
|
|
163
|
+
print(f" Method : {f.method}")
|
|
164
|
+
print(f" DBMS : {f.dbms}")
|
|
165
|
+
print(f" Payload : {f.payload}")
|
|
166
|
+
if f.method.upper() == "GET":
|
|
167
|
+
print(f" Proof : {CYAN(_proof_url(f.url, f.parameter, f.payload))}")
|
|
168
|
+
print()
|
|
169
|
+
i += 1
|
|
170
|
+
|
|
171
|
+
if result.extracted:
|
|
172
|
+
print(f" {GREEN('─' * 56)}")
|
|
173
|
+
print(f" {GREEN(BOLD(' Extracted Data'))}")
|
|
174
|
+
print(f" {GREEN('─' * 56)}")
|
|
175
|
+
for f in result.extracted:
|
|
176
|
+
mode_label = f.mode if f.mode == "union" else f"{f.mode}-blind"
|
|
177
|
+
print(f" {i}. {GREEN('[EXTRACTED]')} via {mode_label}")
|
|
178
|
+
print(f" Param : {f.parameter}")
|
|
179
|
+
print(f" URL : {f.url}")
|
|
180
|
+
print(f" Expr : {DIM(f.expr)}")
|
|
181
|
+
print(f" Value : {BOLD(f.value)}")
|
|
182
|
+
print()
|
|
183
|
+
i += 1
|
|
184
|
+
|
|
185
|
+
if result.table_dumps:
|
|
186
|
+
print(f" {GREEN('─' * 56)}")
|
|
187
|
+
print(f" {GREEN(BOLD(' Table Dumps'))}")
|
|
188
|
+
print(f" {GREEN('─' * 56)}")
|
|
189
|
+
_MAX_DISPLAY = 20
|
|
190
|
+
for td in result.table_dumps:
|
|
191
|
+
print(f" {GREEN('[DUMP]')} {BOLD(td.table)}"
|
|
192
|
+
f" ({len(td.rows)} row(s)) param:{td.parameter}")
|
|
193
|
+
display_rows = td.rows[:_MAX_DISPLAY]
|
|
194
|
+
for line in _ascii_table(td.columns, display_rows):
|
|
195
|
+
print(f" {line}")
|
|
196
|
+
if len(td.rows) > _MAX_DISPLAY:
|
|
197
|
+
print(f" {DIM(f' ... {len(td.rows) - _MAX_DISPLAY} more row(s) in dump file')}")
|
|
198
|
+
print()
|
|
199
|
+
|
|
200
|
+
if result.errors:
|
|
201
|
+
print(RED(" Errors:"))
|
|
202
|
+
for e in result.errors:
|
|
203
|
+
print(f" - {e}")
|
|
204
|
+
|
|
205
|
+
print(BOLD("=" * 60))
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def format_summary(result) -> str:
|
|
209
|
+
"""Return print_summary output as a plain string with ANSI codes stripped."""
|
|
210
|
+
buf = io.StringIO()
|
|
211
|
+
old, sys.stdout = sys.stdout, buf
|
|
212
|
+
try:
|
|
213
|
+
print_summary(result)
|
|
214
|
+
finally:
|
|
215
|
+
sys.stdout = old
|
|
216
|
+
return _ANSI_RE.sub("", buf.getvalue())
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
# Copyright (c) 2026 CommonHuman-Lab
|
|
3
|
+
"""
|
|
4
|
+
BlackOpsSQL — engine/__init__.py
|
|
5
|
+
Public API surface for the BlackOpsSQL engine.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .reporter import (
|
|
9
|
+
ErrorBasedFinding,
|
|
10
|
+
BooleanFinding,
|
|
11
|
+
TimeFinding,
|
|
12
|
+
UnionFinding,
|
|
13
|
+
OOBFinding,
|
|
14
|
+
StackedFinding,
|
|
15
|
+
ExtractionFinding,
|
|
16
|
+
TableDumpFinding,
|
|
17
|
+
FindingType,
|
|
18
|
+
ScanResult,
|
|
19
|
+
)
|
|
20
|
+
from .scanner import ScanOptions, scan
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"scan",
|
|
24
|
+
"ScanOptions",
|
|
25
|
+
"ScanResult",
|
|
26
|
+
"FindingType",
|
|
27
|
+
"ErrorBasedFinding",
|
|
28
|
+
"BooleanFinding",
|
|
29
|
+
"TimeFinding",
|
|
30
|
+
"UnionFinding",
|
|
31
|
+
"OOBFinding",
|
|
32
|
+
"StackedFinding",
|
|
33
|
+
"ExtractionFinding",
|
|
34
|
+
"TableDumpFinding",
|
|
35
|
+
]
|
|
File without changes
|