summit-scanner 0.7.2__tar.gz → 0.7.4__tar.gz
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.
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/PKG-INFO +1 -1
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/pyproject.toml +1 -1
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/summit/cli.py +31 -0
- summit_scanner-0.7.4/summit/race.py +109 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/summit/scan/core.py +1 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/summit_scanner.egg-info/PKG-INFO +1 -1
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/summit_scanner.egg-info/SOURCES.txt +1 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/LICENSE +0 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/README.md +0 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/setup.cfg +0 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/summit/__init__.py +0 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/summit/authorize.py +0 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/summit/captcha.py +0 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/summit/crawl/__init__.py +0 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/summit/crawl/js_endpoints.py +0 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/summit/crawl/spider.py +0 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/summit/cve.py +0 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/summit/fuzz/__init__.py +0 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/summit/fuzz/dirbuster.py +0 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/summit/graphql.py +0 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/summit/http/__init__.py +0 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/summit/http/client.py +0 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/summit/http/models.py +0 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/summit/openapi.py +0 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/summit/paramminer.py +0 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/summit/proxy/__init__.py +0 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/summit/proxy/server.py +0 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/summit/ratelimit.py +0 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/summit/recon.py +0 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/summit/report/__init__.py +0 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/summit/report/writer.py +0 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/summit/scan/__init__.py +0 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/summit/scan/engine.py +0 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/summit/scan/linkcheck.py +0 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/summit/scan/modules/__init__.py +0 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/summit/scan/modules/body_cves.py +0 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/summit/scan/modules/cacheable.py +0 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/summit/scan/modules/cors_reflect.py +0 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/summit/scan/modules/crlf.py +0 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/summit/scan/modules/csrf.py +0 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/summit/scan/modules/directory_listing.py +0 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/summit/scan/modules/enumeration.py +0 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/summit/scan/modules/format_string.py +0 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/summit/scan/modules/headers_grade.py +0 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/summit/scan/modules/host_header.py +0 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/summit/scan/modules/ldap.py +0 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/summit/scan/modules/nosql.py +0 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/summit/scan/modules/open_redirect.py +0 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/summit/scan/modules/os_command.py +0 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/summit/scan/modules/path_traversal.py +0 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/summit/scan/modules/sensitive_files.py +0 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/summit/scan/modules/sqli.py +0 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/summit/scan/modules/sqli_time.py +0 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/summit/scan/modules/ssti.py +0 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/summit/scan/modules/waf_bypass.py +0 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/summit/scan/modules/xss.py +0 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/summit/scan/passive.py +0 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/summit/smart.py +0 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/summit/takeover.py +0 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/summit/tls.py +0 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/summit/toolkit.py +0 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/summit/updates.py +0 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/summit/waf.py +0 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/summit_scanner.egg-info/dependency_links.txt +0 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/summit_scanner.egg-info/entry_points.txt +0 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/summit_scanner.egg-info/requires.txt +0 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/summit_scanner.egg-info/top_level.txt +0 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/tests/test_fp_guards.py +0 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/tests/test_linkcheck.py +0 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/tests/test_models.py +0 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/tests/test_new_checks.py +0 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/tests/test_passive.py +0 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/tests/test_retries.py +0 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/tests/test_severity.py +0 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/tests/test_tls.py +0 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/tests/test_v05.py +0 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/tests/test_v06.py +0 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/tests/test_v068.py +0 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/tests/test_v069.py +0 -0
- {summit_scanner-0.7.2 → summit_scanner-0.7.4}/tests/test_waf.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "summit-scanner"
|
|
7
|
-
version = "0.7.
|
|
7
|
+
version = "0.7.4"
|
|
8
8
|
description = "Summit - open-source web security testing platform (proxy, scanner, fuzzer) by Pennacle Security"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -13,6 +13,7 @@ from .http.models import HttpRequest
|
|
|
13
13
|
from .openapi import load_spec, spec_pages
|
|
14
14
|
from .paramminer import mine
|
|
15
15
|
from .proxy.server import ProxyServer
|
|
16
|
+
from .race import findings_from as race_findings_from, run_race_test
|
|
16
17
|
from .recon import missing_tools, render_report, run_recon
|
|
17
18
|
from .report.writer import write_console, write_html, write_json
|
|
18
19
|
from .scan.core import SEVERITIES, filter_by_min_severity, filter_by_severities
|
|
@@ -108,6 +109,15 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
108
109
|
tls_p.add_argument("--timeout", type=float, default=10.0)
|
|
109
110
|
tls_p.add_argument("--json", dest="json_path", help="write findings as JSON")
|
|
110
111
|
|
|
112
|
+
race_p = sub.add_parser("race", help="detect race conditions via concurrent identical requests")
|
|
113
|
+
race_p.add_argument("url", help="target URL that performs a state change")
|
|
114
|
+
race_p.add_argument("-n", "--concurrency", type=int, default=10, help="number of concurrent requests (default 10)")
|
|
115
|
+
race_p.add_argument("--method", default="GET", help="HTTP method (default GET)")
|
|
116
|
+
race_p.add_argument("--data", default=None, help="request body (for POST)")
|
|
117
|
+
race_p.add_argument("--cookie", default=None, help="Cookie header value")
|
|
118
|
+
race_p.add_argument("--timeout", type=float, default=10.0)
|
|
119
|
+
race_p.add_argument("--json", dest="json_path", help="write findings as JSON")
|
|
120
|
+
|
|
111
121
|
return parser
|
|
112
122
|
|
|
113
123
|
|
|
@@ -310,6 +320,25 @@ def _cmd_tls(args: argparse.Namespace) -> int:
|
|
|
310
320
|
return 0
|
|
311
321
|
|
|
312
322
|
|
|
323
|
+
def _cmd_race(args: argparse.Namespace) -> int:
|
|
324
|
+
print(BANNER.format(version=__version__))
|
|
325
|
+
client = HttpClient(timeout=args.timeout)
|
|
326
|
+
headers = {"Cookie": args.cookie} if args.cookie else {}
|
|
327
|
+
print(f"[*] testing {args.url} with {args.concurrency} concurrent {args.method} requests ...")
|
|
328
|
+
result = run_race_test(
|
|
329
|
+
client, args.url, concurrency=args.concurrency, method=args.method,
|
|
330
|
+
body=args.data.encode() if args.data else None, headers=headers,
|
|
331
|
+
)
|
|
332
|
+
print(f"[*] baseline: HTTP {result.baseline.status}/{len(result.baseline.body)}B")
|
|
333
|
+
print(f"[*] concurrent: {len(result.concurrent)} responses, {result.unique_responses} unique")
|
|
334
|
+
findings = race_findings_from([result])
|
|
335
|
+
print(write_console(findings, args.url))
|
|
336
|
+
if args.json_path:
|
|
337
|
+
write_json(findings, args.json_path)
|
|
338
|
+
print(f"[*] JSON report written to {args.json_path}")
|
|
339
|
+
return 0
|
|
340
|
+
|
|
341
|
+
|
|
313
342
|
def _cmd_encode(args: argparse.Namespace) -> int:
|
|
314
343
|
try:
|
|
315
344
|
print(encode_text(args.value, args.encoding))
|
|
@@ -354,6 +383,8 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
354
383
|
return _cmd_recon(args)
|
|
355
384
|
if args.command == "tls":
|
|
356
385
|
return _cmd_tls(args)
|
|
386
|
+
if args.command == "race":
|
|
387
|
+
return _cmd_race(args)
|
|
357
388
|
if args.command == "encode":
|
|
358
389
|
return _cmd_encode(args)
|
|
359
390
|
if args.command == "decode":
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Race condition detection - concurrent state manipulation.
|
|
2
|
+
|
|
3
|
+
Race conditions exist when concurrent requests produce a state that
|
|
4
|
+
sequential requests cannot. The classic examples: double-spending a
|
|
5
|
+
gift card, withdrawing more than the balance, creating duplicate
|
|
6
|
+
accounts, bypassing rate limits.
|
|
7
|
+
|
|
8
|
+
Detection strategy:
|
|
9
|
+
|
|
10
|
+
1. send one request sequentially to establish the baseline response
|
|
11
|
+
2. send N identical requests concurrently
|
|
12
|
+
3. compare: if concurrent responses differ from the baseline in a
|
|
13
|
+
meaningful way (different body, different status, different side
|
|
14
|
+
effects visible in the response), the server has a concurrency bug
|
|
15
|
+
|
|
16
|
+
This is always `potential` - only a human who understands the business
|
|
17
|
+
logic can confirm that the differing responses represent an exploit
|
|
18
|
+
rather than expected variation.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
24
|
+
from dataclasses import dataclass
|
|
25
|
+
|
|
26
|
+
from ...http.client import HttpClient
|
|
27
|
+
from ...http.models import HttpRequest, HttpResponse
|
|
28
|
+
from ..scan.core import Finding
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class RaceResult:
|
|
33
|
+
url: str
|
|
34
|
+
baseline: HttpResponse
|
|
35
|
+
concurrent: list[HttpResponse]
|
|
36
|
+
unique_responses: int
|
|
37
|
+
verdict: str # no-race | possible-race
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _fingerprint(response: HttpResponse) -> tuple:
|
|
41
|
+
return (response.status, len(response.body))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def run_race_test(
|
|
45
|
+
client: HttpClient,
|
|
46
|
+
url: str,
|
|
47
|
+
concurrency: int = 10,
|
|
48
|
+
method: str = "GET",
|
|
49
|
+
body: bytes | None = None,
|
|
50
|
+
headers: dict[str, str] | None = None,
|
|
51
|
+
) -> RaceResult:
|
|
52
|
+
"""Send one sequential request, then N concurrent identical requests."""
|
|
53
|
+
request = HttpRequest(method=method, url=url, headers=headers or {}, body=body)
|
|
54
|
+
|
|
55
|
+
baseline = client.send(request)
|
|
56
|
+
base_fp = _fingerprint(baseline)
|
|
57
|
+
|
|
58
|
+
concurrent_responses: list[HttpResponse] = []
|
|
59
|
+
with ThreadPoolExecutor(max_workers=concurrency) as pool:
|
|
60
|
+
futures = [pool.submit(client.send, request) for _ in range(concurrency)]
|
|
61
|
+
for future in as_completed(futures):
|
|
62
|
+
concurrent_responses.append(future.result())
|
|
63
|
+
|
|
64
|
+
unique_fps = {_fingerprint(r) for r in concurrent_responses}
|
|
65
|
+
all_fps = unique_fps | {base_fp}
|
|
66
|
+
|
|
67
|
+
if len(all_fps) == 1:
|
|
68
|
+
verdict = "no-race"
|
|
69
|
+
elif len(unique_fps) > 1 or (len(all_fps) > 1 and base_fp not in unique_fps):
|
|
70
|
+
verdict = "possible-race"
|
|
71
|
+
else:
|
|
72
|
+
verdict = "possible-race"
|
|
73
|
+
|
|
74
|
+
return RaceResult(
|
|
75
|
+
url=url,
|
|
76
|
+
baseline=baseline,
|
|
77
|
+
concurrent=concurrent_responses,
|
|
78
|
+
unique_responses=len(unique_fps),
|
|
79
|
+
verdict=verdict,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def findings_from(results: list[RaceResult]) -> list[Finding]:
|
|
84
|
+
findings: list[Finding] = []
|
|
85
|
+
for result in results:
|
|
86
|
+
if result.verdict != "possible-race":
|
|
87
|
+
continue
|
|
88
|
+
statuses = sorted({r.status for r in result.concurrent})
|
|
89
|
+
sizes = sorted({len(r.body) for r in result.concurrent})
|
|
90
|
+
findings.append(
|
|
91
|
+
Finding(
|
|
92
|
+
check_id="race-condition",
|
|
93
|
+
title="Race condition (concurrent requests produce different responses)",
|
|
94
|
+
severity="high",
|
|
95
|
+
url=result.url,
|
|
96
|
+
confidence="potential",
|
|
97
|
+
detail=(
|
|
98
|
+
f"{len(result.concurrent)} concurrent requests produced "
|
|
99
|
+
f"{result.unique_responses} unique response(s) that differ from the "
|
|
100
|
+
f"sequential baseline (HTTP {result.baseline.status}/{len(result.baseline.body)}B). "
|
|
101
|
+
"If this endpoint performs a state change (balance transfer, voucher redemption, "
|
|
102
|
+
"rate limit), concurrent requests may bypass the check. Verify manually by "
|
|
103
|
+
"inspecting server-side state after the race."
|
|
104
|
+
),
|
|
105
|
+
evidence=f"statuses: {statuses}, sizes: {sizes}, unique: {result.unique_responses}",
|
|
106
|
+
remediation="Use atomic operations, database-level locks, or idempotency keys for state-changing endpoints.",
|
|
107
|
+
)
|
|
108
|
+
)
|
|
109
|
+
return findings
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|