bbot 2.5.0__py3-none-any.whl → 2.7.2.7424rc0__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.
- bbot/__init__.py +1 -1
- bbot/cli.py +22 -8
- bbot/core/engine.py +1 -1
- bbot/core/event/__init__.py +2 -2
- bbot/core/event/base.py +138 -110
- bbot/core/flags.py +1 -0
- bbot/core/helpers/bloom.py +6 -7
- bbot/core/helpers/command.py +5 -2
- bbot/core/helpers/depsinstaller/installer.py +78 -7
- bbot/core/helpers/dns/dns.py +0 -1
- bbot/core/helpers/dns/engine.py +0 -2
- bbot/core/helpers/files.py +2 -2
- bbot/core/helpers/git.py +17 -0
- bbot/core/helpers/helper.py +6 -5
- bbot/core/helpers/misc.py +15 -28
- bbot/core/helpers/names_generator.py +5 -0
- bbot/core/helpers/ntlm.py +0 -2
- bbot/core/helpers/regex.py +1 -1
- bbot/core/helpers/regexes.py +25 -8
- bbot/core/helpers/web/engine.py +1 -1
- bbot/core/helpers/web/web.py +2 -1
- bbot/core/modules.py +22 -60
- bbot/core/shared_deps.py +38 -0
- bbot/defaults.yml +4 -2
- bbot/modules/apkpure.py +2 -2
- bbot/modules/aspnet_bin_exposure.py +80 -0
- bbot/modules/baddns.py +1 -1
- bbot/modules/baddns_direct.py +1 -1
- bbot/modules/baddns_zone.py +1 -1
- bbot/modules/badsecrets.py +1 -1
- bbot/modules/base.py +129 -40
- bbot/modules/bucket_amazon.py +1 -1
- bbot/modules/bucket_digitalocean.py +1 -1
- bbot/modules/bucket_firebase.py +1 -1
- bbot/modules/bucket_google.py +1 -1
- bbot/modules/{bucket_azure.py → bucket_microsoft.py} +2 -2
- bbot/modules/builtwith.py +4 -2
- bbot/modules/c99.py +1 -1
- bbot/modules/dnsbimi.py +1 -4
- bbot/modules/dnsbrute.py +6 -1
- bbot/modules/dnscommonsrv.py +1 -0
- bbot/modules/dnsdumpster.py +35 -52
- bbot/modules/dnstlsrpt.py +0 -6
- bbot/modules/docker_pull.py +2 -2
- bbot/modules/emailformat.py +17 -1
- bbot/modules/ffuf.py +4 -1
- bbot/modules/ffuf_shortnames.py +6 -3
- bbot/modules/filedownload.py +8 -5
- bbot/modules/fullhunt.py +1 -1
- bbot/modules/git_clone.py +47 -22
- bbot/modules/gitdumper.py +5 -15
- bbot/modules/github_workflows.py +6 -5
- bbot/modules/gitlab_com.py +31 -0
- bbot/modules/gitlab_onprem.py +84 -0
- bbot/modules/gowitness.py +60 -30
- bbot/modules/graphql_introspection.py +145 -0
- bbot/modules/httpx.py +2 -0
- bbot/modules/hunt.py +10 -3
- bbot/modules/iis_shortnames.py +16 -7
- bbot/modules/internal/cloudcheck.py +65 -72
- bbot/modules/internal/unarchive.py +9 -3
- bbot/modules/lightfuzz/lightfuzz.py +6 -2
- bbot/modules/lightfuzz/submodules/esi.py +42 -0
- bbot/modules/{deadly/medusa.py → medusa.py} +4 -7
- bbot/modules/nuclei.py +2 -2
- bbot/modules/otx.py +9 -2
- bbot/modules/output/base.py +3 -11
- bbot/modules/paramminer_headers.py +10 -7
- bbot/modules/passivetotal.py +1 -1
- bbot/modules/portfilter.py +2 -0
- bbot/modules/portscan.py +1 -1
- bbot/modules/postman_download.py +2 -2
- bbot/modules/retirejs.py +232 -0
- bbot/modules/securitytxt.py +0 -3
- bbot/modules/sslcert.py +2 -2
- bbot/modules/subdomaincenter.py +1 -16
- bbot/modules/telerik.py +7 -2
- bbot/modules/templates/bucket.py +24 -4
- bbot/modules/templates/gitlab.py +98 -0
- bbot/modules/trufflehog.py +7 -4
- bbot/modules/wafw00f.py +2 -2
- bbot/presets/web/dotnet-audit.yml +1 -0
- bbot/presets/web/lightfuzz-heavy.yml +1 -1
- bbot/presets/web/lightfuzz-medium.yml +1 -1
- bbot/presets/web/lightfuzz-superheavy.yml +1 -1
- bbot/scanner/manager.py +44 -37
- bbot/scanner/scanner.py +17 -4
- bbot/scripts/benchmark_report.py +433 -0
- bbot/test/benchmarks/__init__.py +2 -0
- bbot/test/benchmarks/test_bloom_filter_benchmarks.py +105 -0
- bbot/test/benchmarks/test_closest_match_benchmarks.py +76 -0
- bbot/test/benchmarks/test_event_validation_benchmarks.py +438 -0
- bbot/test/benchmarks/test_excavate_benchmarks.py +291 -0
- bbot/test/benchmarks/test_ipaddress_benchmarks.py +143 -0
- bbot/test/benchmarks/test_weighted_shuffle_benchmarks.py +70 -0
- bbot/test/conftest.py +1 -1
- bbot/test/test_step_1/test_bbot_fastapi.py +2 -2
- bbot/test/test_step_1/test_events.py +22 -21
- bbot/test/test_step_1/test_helpers.py +20 -0
- bbot/test/test_step_1/test_manager_scope_accuracy.py +45 -0
- bbot/test/test_step_1/test_modules_basic.py +40 -15
- bbot/test/test_step_1/test_python_api.py +2 -2
- bbot/test/test_step_1/test_regexes.py +21 -4
- bbot/test/test_step_1/test_scan.py +7 -8
- bbot/test/test_step_1/test_web.py +46 -0
- bbot/test/test_step_2/module_tests/base.py +6 -1
- bbot/test/test_step_2/module_tests/test_module_aspnet_bin_exposure.py +73 -0
- bbot/test/test_step_2/module_tests/test_module_bucket_amazon.py +52 -18
- bbot/test/test_step_2/module_tests/test_module_bucket_google.py +1 -1
- bbot/test/test_step_2/module_tests/{test_module_bucket_azure.py → test_module_bucket_microsoft.py} +7 -5
- bbot/test/test_step_2/module_tests/test_module_cloudcheck.py +19 -31
- bbot/test/test_step_2/module_tests/test_module_dnsbimi.py +2 -1
- bbot/test/test_step_2/module_tests/test_module_dnsdumpster.py +3 -5
- bbot/test/test_step_2/module_tests/test_module_emailformat.py +1 -1
- bbot/test/test_step_2/module_tests/test_module_emails.py +2 -2
- bbot/test/test_step_2/module_tests/test_module_excavate.py +64 -5
- bbot/test/test_step_2/module_tests/test_module_extractous.py +13 -1
- bbot/test/test_step_2/module_tests/test_module_github_workflows.py +10 -1
- bbot/test/test_step_2/module_tests/test_module_gitlab_com.py +66 -0
- bbot/test/test_step_2/module_tests/{test_module_gitlab.py → test_module_gitlab_onprem.py} +4 -69
- bbot/test/test_step_2/module_tests/test_module_gowitness.py +5 -5
- bbot/test/test_step_2/module_tests/test_module_graphql_introspection.py +34 -0
- bbot/test/test_step_2/module_tests/test_module_iis_shortnames.py +46 -1
- bbot/test/test_step_2/module_tests/test_module_jadx.py +9 -0
- bbot/test/test_step_2/module_tests/test_module_lightfuzz.py +71 -3
- bbot/test/test_step_2/module_tests/test_module_nuclei.py +8 -6
- bbot/test/test_step_2/module_tests/test_module_otx.py +3 -0
- bbot/test/test_step_2/module_tests/test_module_portfilter.py +2 -0
- bbot/test/test_step_2/module_tests/test_module_retirejs.py +161 -0
- bbot/test/test_step_2/module_tests/test_module_telerik.py +1 -1
- bbot/test/test_step_2/module_tests/test_module_trufflehog.py +10 -1
- bbot/test/test_step_2/module_tests/test_module_unarchive.py +9 -0
- {bbot-2.5.0.dist-info → bbot-2.7.2.7424rc0.dist-info}/METADATA +12 -9
- {bbot-2.5.0.dist-info → bbot-2.7.2.7424rc0.dist-info}/RECORD +137 -124
- {bbot-2.5.0.dist-info → bbot-2.7.2.7424rc0.dist-info}/WHEEL +1 -1
- {bbot-2.5.0.dist-info → bbot-2.7.2.7424rc0.dist-info/licenses}/LICENSE +98 -58
- bbot/modules/binaryedge.py +0 -42
- bbot/modules/censys.py +0 -98
- bbot/modules/gitlab.py +0 -141
- bbot/modules/zoomeye.py +0 -77
- bbot/test/test_step_2/module_tests/test_module_binaryedge.py +0 -33
- bbot/test/test_step_2/module_tests/test_module_censys.py +0 -83
- bbot/test/test_step_2/module_tests/test_module_zoomeye.py +0 -35
- {bbot-2.5.0.dist-info → bbot-2.7.2.7424rc0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from bbot.modules.base import BaseModule
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class graphql_introspection(BaseModule):
|
|
7
|
+
watched_events = ["URL"]
|
|
8
|
+
produced_events = ["FINDING"]
|
|
9
|
+
flags = ["safe", "active", "web-basic"]
|
|
10
|
+
meta = {
|
|
11
|
+
"description": "Perform GraphQL introspection on a target",
|
|
12
|
+
"created_date": "2025-07-01",
|
|
13
|
+
"author": "@mukesh-dream11",
|
|
14
|
+
}
|
|
15
|
+
options = {
|
|
16
|
+
"graphql_endpoint_urls": ["/", "/graphql", "/v1/graphql"],
|
|
17
|
+
"output_folder": "",
|
|
18
|
+
}
|
|
19
|
+
options_desc = {
|
|
20
|
+
"graphql_endpoint_urls": "List of GraphQL endpoint to suffix to the target URL",
|
|
21
|
+
"output_folder": "Folder to save the GraphQL schemas to",
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async def setup(self):
|
|
25
|
+
output_folder = self.config.get("output_folder", "")
|
|
26
|
+
if output_folder:
|
|
27
|
+
self.output_dir = Path(output_folder) / "graphql-schemas"
|
|
28
|
+
else:
|
|
29
|
+
self.output_dir = self.scan.home / "graphql-schemas"
|
|
30
|
+
return True
|
|
31
|
+
|
|
32
|
+
async def filter_event(self, event):
|
|
33
|
+
# Dedup by the base URL
|
|
34
|
+
base_url = event.parsed_url._replace(path="/", query="", fragment="").geturl()
|
|
35
|
+
return hash(base_url)
|
|
36
|
+
|
|
37
|
+
async def handle_event(self, event):
|
|
38
|
+
base_url = event.parsed_url._replace(path="/", query="", fragment="").geturl().rstrip("/")
|
|
39
|
+
for endpoint_url in self.config.get("graphql_endpoint_urls", []):
|
|
40
|
+
url = f"{base_url}{endpoint_url}"
|
|
41
|
+
request_args = {
|
|
42
|
+
"url": url,
|
|
43
|
+
"method": "POST",
|
|
44
|
+
"json": {
|
|
45
|
+
"query": """\
|
|
46
|
+
query IntrospectionQuery {
|
|
47
|
+
__schema {
|
|
48
|
+
queryType {
|
|
49
|
+
name
|
|
50
|
+
}
|
|
51
|
+
mutationType {
|
|
52
|
+
name
|
|
53
|
+
}
|
|
54
|
+
types {
|
|
55
|
+
name
|
|
56
|
+
kind
|
|
57
|
+
description
|
|
58
|
+
fields(includeDeprecated: true) {
|
|
59
|
+
name
|
|
60
|
+
description
|
|
61
|
+
type {
|
|
62
|
+
... TypeRef
|
|
63
|
+
}
|
|
64
|
+
isDeprecated
|
|
65
|
+
deprecationReason
|
|
66
|
+
}
|
|
67
|
+
interfaces {
|
|
68
|
+
... TypeRef
|
|
69
|
+
}
|
|
70
|
+
possibleTypes {
|
|
71
|
+
... TypeRef
|
|
72
|
+
}
|
|
73
|
+
enumValues(includeDeprecated: true) {
|
|
74
|
+
name
|
|
75
|
+
description
|
|
76
|
+
isDeprecated
|
|
77
|
+
deprecationReason
|
|
78
|
+
}
|
|
79
|
+
ofType {
|
|
80
|
+
... TypeRef
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
fragment TypeRef on __Type {
|
|
87
|
+
kind
|
|
88
|
+
name
|
|
89
|
+
ofType {
|
|
90
|
+
kind
|
|
91
|
+
name
|
|
92
|
+
ofType {
|
|
93
|
+
kind
|
|
94
|
+
name
|
|
95
|
+
ofType {
|
|
96
|
+
kind
|
|
97
|
+
name
|
|
98
|
+
ofType {
|
|
99
|
+
kind
|
|
100
|
+
name
|
|
101
|
+
ofType {
|
|
102
|
+
kind
|
|
103
|
+
name
|
|
104
|
+
ofType {
|
|
105
|
+
kind
|
|
106
|
+
name
|
|
107
|
+
ofType {
|
|
108
|
+
kind
|
|
109
|
+
name
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}"""
|
|
118
|
+
},
|
|
119
|
+
}
|
|
120
|
+
response = await self.helpers.request(**request_args)
|
|
121
|
+
if not response or response.status_code != 200:
|
|
122
|
+
self.debug(
|
|
123
|
+
f"Failed to get GraphQL schema for {url} "
|
|
124
|
+
f"{f'(status code {response.status_code})' if response else ''}"
|
|
125
|
+
)
|
|
126
|
+
continue
|
|
127
|
+
try:
|
|
128
|
+
response_json = response.json()
|
|
129
|
+
except json.JSONDecodeError:
|
|
130
|
+
self.debug(f"Failed to parse JSON for {url}")
|
|
131
|
+
continue
|
|
132
|
+
if response_json.get("data", {}).get("__schema", {}).get("types", []):
|
|
133
|
+
self.helpers.mkdir(self.output_dir)
|
|
134
|
+
filename = f"schema-{self.helpers.tagify(url)}.json"
|
|
135
|
+
filename = self.output_dir / filename
|
|
136
|
+
with open(filename, "w") as f:
|
|
137
|
+
json.dump(response_json, f)
|
|
138
|
+
await self.emit_event(
|
|
139
|
+
{"url": url, "description": "GraphQL schema", "path": str(filename.relative_to(self.scan.home))},
|
|
140
|
+
"FINDING",
|
|
141
|
+
event,
|
|
142
|
+
context=f"{{module}} found GraphQL schema at {url}",
|
|
143
|
+
)
|
|
144
|
+
# return, because we only want to find one schema per target
|
|
145
|
+
return
|
bbot/modules/httpx.py
CHANGED
bbot/modules/hunt.py
CHANGED
|
@@ -50,7 +50,16 @@ hunt_param_dict = {
|
|
|
50
50
|
"cfg",
|
|
51
51
|
"config",
|
|
52
52
|
],
|
|
53
|
-
"Directory Traversal": [
|
|
53
|
+
"Directory Traversal": [
|
|
54
|
+
"entry",
|
|
55
|
+
"download",
|
|
56
|
+
"attachment",
|
|
57
|
+
"basepath",
|
|
58
|
+
"path",
|
|
59
|
+
"file",
|
|
60
|
+
"source",
|
|
61
|
+
"dest",
|
|
62
|
+
],
|
|
54
63
|
"Local File Include": [
|
|
55
64
|
"file",
|
|
56
65
|
"document",
|
|
@@ -279,8 +288,6 @@ class hunt(BaseModule):
|
|
|
279
288
|
"author": "@liquidsec",
|
|
280
289
|
"created_date": "2022-07-20",
|
|
281
290
|
}
|
|
282
|
-
# accept all events regardless of scope distance
|
|
283
|
-
scope_distance_modifier = None
|
|
284
291
|
|
|
285
292
|
async def handle_event(self, event):
|
|
286
293
|
p = event.data["name"]
|
bbot/modules/iis_shortnames.py
CHANGED
|
@@ -116,13 +116,6 @@ class iis_shortnames(BaseModule):
|
|
|
116
116
|
|
|
117
117
|
return duplicates
|
|
118
118
|
|
|
119
|
-
async def threaded_request(self, method, url, affirmative_status_code, c):
|
|
120
|
-
r = await self.helpers.request(method=method, url=url, allow_redirects=False, retries=2, timeout=10)
|
|
121
|
-
if r is not None:
|
|
122
|
-
if r.status_code == affirmative_status_code:
|
|
123
|
-
return True, c
|
|
124
|
-
return None, c
|
|
125
|
-
|
|
126
119
|
async def solve_valid_chars(self, method, target, affirmative_status_code):
|
|
127
120
|
confirmed_chars = []
|
|
128
121
|
confirmed_exts = []
|
|
@@ -166,6 +159,10 @@ class iis_shortnames(BaseModule):
|
|
|
166
159
|
|
|
167
160
|
cl = ext_char_list if extension_mode is True else char_list
|
|
168
161
|
|
|
162
|
+
self.debug(
|
|
163
|
+
f"Solving shortname recursive for {target} with prefix {prefix} and extension mode {extension_mode}"
|
|
164
|
+
)
|
|
165
|
+
|
|
169
166
|
urls_and_kwargs = []
|
|
170
167
|
|
|
171
168
|
for c in cl:
|
|
@@ -334,6 +331,18 @@ class iis_shortnames(BaseModule):
|
|
|
334
331
|
for url_hint in url_hint_list:
|
|
335
332
|
if "." in url_hint:
|
|
336
333
|
hint_type = "shortname-endpoint"
|
|
334
|
+
# Check if it's a ZIP file
|
|
335
|
+
if url_hint.lower().endswith(".zip"):
|
|
336
|
+
await self.emit_event(
|
|
337
|
+
{
|
|
338
|
+
"host": str(event.host),
|
|
339
|
+
"url": event.data,
|
|
340
|
+
"description": f"Possible backup file (zip) in web root: {normalized_url}{url_hint}",
|
|
341
|
+
},
|
|
342
|
+
"FINDING",
|
|
343
|
+
event,
|
|
344
|
+
context=f"{{module}} discovered possible backup file in web root: {url_hint}",
|
|
345
|
+
)
|
|
337
346
|
else:
|
|
338
347
|
hint_type = "shortname-directory"
|
|
339
348
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import regex as re
|
|
1
3
|
from contextlib import suppress
|
|
2
4
|
|
|
3
5
|
from bbot.modules.base import BaseInterceptModule
|
|
@@ -10,107 +12,98 @@ class CloudCheck(BaseInterceptModule):
|
|
|
10
12
|
"created_date": "2024-07-07",
|
|
11
13
|
"author": "@TheTechromancer",
|
|
12
14
|
}
|
|
13
|
-
|
|
15
|
+
# tag events up to and including distance-2
|
|
16
|
+
scope_distance_modifier = 2
|
|
14
17
|
_priority = 3
|
|
15
18
|
|
|
16
19
|
async def setup(self):
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
await update()
|
|
20
|
-
self.dummy_modules = None
|
|
20
|
+
self._cloud_hostname_regexes = None
|
|
21
|
+
self._cloud_hostname_regexes_lock = asyncio.Lock()
|
|
21
22
|
return True
|
|
22
23
|
|
|
23
|
-
def make_dummy_modules(self):
|
|
24
|
-
self.dummy_modules = {}
|
|
25
|
-
for provider_name in self.helpers.cloud.providers.keys():
|
|
26
|
-
module = self.scan._make_dummy_module(f"cloud_{provider_name}", _type="scan")
|
|
27
|
-
module.default_discovery_context = "{module} derived {event.type}: {event.host}"
|
|
28
|
-
self.dummy_modules[provider_name] = module
|
|
29
|
-
|
|
30
24
|
async def filter_event(self, event):
|
|
31
25
|
if (not event.host) or (event.type in ("IP_RANGE",)):
|
|
32
26
|
return False, "event does not have host attribute"
|
|
33
27
|
return True
|
|
34
28
|
|
|
35
29
|
async def handle_event(self, event, **kwargs):
|
|
36
|
-
# don't hold up the event loop loading cloud IPs etc.
|
|
37
|
-
if self.dummy_modules is None:
|
|
38
|
-
self.make_dummy_modules()
|
|
39
30
|
# cloud tagging by hosts
|
|
40
31
|
hosts_to_check = set(event.resolved_hosts)
|
|
41
32
|
with suppress(KeyError):
|
|
42
33
|
hosts_to_check.remove(event.host_original)
|
|
43
|
-
hosts_to_check = [event.host_original] + list(hosts_to_check)
|
|
34
|
+
hosts_to_check = [str(event.host_original)] + list(hosts_to_check)
|
|
44
35
|
|
|
45
36
|
for i, host in enumerate(hosts_to_check):
|
|
46
37
|
host_is_ip = self.helpers.is_ip(host)
|
|
47
38
|
try:
|
|
48
|
-
cloudcheck_results = self.helpers.cloudcheck(host)
|
|
39
|
+
cloudcheck_results = await self.helpers.cloudcheck.lookup(host)
|
|
49
40
|
except Exception as e:
|
|
50
41
|
self.trace(f"Error running cloudcheck against {event} (host: {host}): {e}")
|
|
51
42
|
continue
|
|
52
|
-
for provider
|
|
53
|
-
|
|
54
|
-
|
|
43
|
+
for provider in cloudcheck_results:
|
|
44
|
+
provider_name = provider["name"].lower()
|
|
45
|
+
tags = provider.get("tags", [])
|
|
46
|
+
for tag in tags:
|
|
47
|
+
event.add_tag(tag)
|
|
48
|
+
event.add_tag(f"{tag}-{provider_name}")
|
|
55
49
|
if host_is_ip:
|
|
56
|
-
event.add_tag(f"{
|
|
50
|
+
event.add_tag(f"{provider_name}-ip")
|
|
57
51
|
else:
|
|
58
52
|
# if the original hostname is a cloud domain, tag it as such
|
|
59
53
|
if i == 0:
|
|
60
|
-
event.add_tag(f"{
|
|
54
|
+
event.add_tag(f"{provider_name}-domain")
|
|
61
55
|
# any children are tagged as CNAMEs
|
|
62
56
|
else:
|
|
63
|
-
event.add_tag(f"{
|
|
57
|
+
event.add_tag(f"{provider_name}-cname")
|
|
64
58
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
# loop through each provider
|
|
69
|
-
for provider in self.helpers.cloud.providers.values():
|
|
70
|
-
provider_name = provider.name.lower()
|
|
71
|
-
base_kwargs = {
|
|
72
|
-
"parent": event,
|
|
73
|
-
"tags": [f"{provider.provider_type}-{provider_name}"],
|
|
74
|
-
"_provider": provider_name,
|
|
75
|
-
}
|
|
76
|
-
# loop through the provider's regex signatures, if any
|
|
77
|
-
for event_type, sigs in provider.signatures.items():
|
|
78
|
-
if event_type != "STORAGE_BUCKET":
|
|
79
|
-
raise ValueError(f'Unknown cloudcheck event type "{event_type}"')
|
|
80
|
-
base_kwargs["event_type"] = event_type
|
|
81
|
-
for sig in sigs:
|
|
82
|
-
matches = []
|
|
83
|
-
# TODO: convert this to an excavate YARA hook
|
|
84
|
-
# if event.type == "HTTP_RESPONSE":
|
|
85
|
-
# matches = await self.helpers.re.findall(sig, event.data.get("body", ""))
|
|
86
|
-
if event.type.startswith("DNS_NAME"):
|
|
87
|
-
for host in str_hosts_to_check:
|
|
88
|
-
match = sig.match(host)
|
|
89
|
-
if match:
|
|
90
|
-
matches.append(match.groups())
|
|
91
|
-
for match in matches:
|
|
92
|
-
if match not in found:
|
|
93
|
-
found.add(match)
|
|
59
|
+
# we only generate storage buckets off of in-scope or distance-1 events
|
|
60
|
+
if event.scope_distance >= self.max_scope_distance:
|
|
61
|
+
return
|
|
94
62
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
63
|
+
# see if any of our hosts are storage buckets, etc.
|
|
64
|
+
regexes = await self.cloud_hostname_regexes()
|
|
65
|
+
regexes = regexes.get("STORAGE_BUCKET_HOSTNAME", [])
|
|
66
|
+
for regex_name, regex in regexes.items():
|
|
67
|
+
for host in hosts_to_check:
|
|
68
|
+
if match := regex.match(host):
|
|
69
|
+
try:
|
|
70
|
+
bucket_name, bucket_domain = match.groups()
|
|
71
|
+
except Exception as e:
|
|
72
|
+
self.error(
|
|
73
|
+
f"Bucket regex {regex_name} ({regex}) is not formatted correctly to extract bucket name and domain: {e}"
|
|
74
|
+
)
|
|
75
|
+
continue
|
|
76
|
+
bucket_name, bucket_domain = match.groups()
|
|
77
|
+
bucket_url = f"https://{bucket_name}.{bucket_domain}"
|
|
78
|
+
await self.emit_event(
|
|
79
|
+
{
|
|
80
|
+
"name": bucket_name,
|
|
81
|
+
"url": bucket_url,
|
|
82
|
+
"context": f"{{module}} analyzed {event.type} and found {{event.type}}: {bucket_url}",
|
|
83
|
+
},
|
|
84
|
+
"STORAGE_BUCKET",
|
|
85
|
+
parent=event,
|
|
86
|
+
)
|
|
100
87
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
"context": f"{{module}} analyzed {event.type} and found {{event.type}}: {bucket_url}",
|
|
108
|
-
}
|
|
109
|
-
await self.emit_event(**_kwargs)
|
|
88
|
+
async def cloud_hostname_regexes(self):
|
|
89
|
+
async with self._cloud_hostname_regexes_lock:
|
|
90
|
+
if not self._cloud_hostname_regexes:
|
|
91
|
+
storage_bucket_regexes = {}
|
|
92
|
+
self._cloud_hostname_regexes = {"STORAGE_BUCKET_HOSTNAME": storage_bucket_regexes}
|
|
93
|
+
from cloudcheck import providers
|
|
110
94
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
95
|
+
for attr in dir(providers):
|
|
96
|
+
if attr.startswith("_"):
|
|
97
|
+
continue
|
|
98
|
+
provider = getattr(providers, attr)
|
|
99
|
+
provider_regexes = getattr(provider, "regexes", {})
|
|
100
|
+
for regex_name, regexes in provider_regexes.items():
|
|
101
|
+
for i, regex in enumerate(regexes):
|
|
102
|
+
if not regex_name in ("STORAGE_BUCKET_HOSTNAME"):
|
|
103
|
+
continue
|
|
104
|
+
try:
|
|
105
|
+
storage_bucket_regexes[f"{attr}-{regex_name}-{i}"] = re.compile(regex)
|
|
106
|
+
except Exception as e:
|
|
107
|
+
self.error(f"Error compiling regex for {attr}-{regex_name}: {e}")
|
|
108
|
+
continue
|
|
109
|
+
return self._cloud_hostname_regexes
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
|
+
from contextlib import suppress
|
|
2
3
|
from bbot.modules.internal.base import BaseInternalModule
|
|
3
4
|
from bbot.core.helpers.libmagic import get_magic_info, get_compression
|
|
4
5
|
|
|
@@ -62,15 +63,20 @@ class unarchive(BaseInternalModule):
|
|
|
62
63
|
context=f'extracted "{path}" to: {output_dir}',
|
|
63
64
|
)
|
|
64
65
|
else:
|
|
65
|
-
|
|
66
|
+
with suppress(OSError):
|
|
67
|
+
output_dir.rmdir()
|
|
66
68
|
|
|
67
69
|
async def extract_file(self, path, output_dir):
|
|
68
70
|
extension, mime_type, description, confidence = get_magic_info(path)
|
|
69
71
|
compression_format = get_compression(mime_type)
|
|
70
72
|
cmd_list = self.compression_methods.get(compression_format, [])
|
|
71
73
|
if cmd_list:
|
|
72
|
-
|
|
73
|
-
|
|
74
|
+
# output dir must not already exist
|
|
75
|
+
try:
|
|
76
|
+
output_dir.mkdir(exist_ok=False)
|
|
77
|
+
except FileExistsError:
|
|
78
|
+
self.warning(f"Destination directory {output_dir} already exists, aborting unarchive for {path}")
|
|
79
|
+
return False
|
|
74
80
|
command = [s.format(filename=path, extract_dir=output_dir) for s in cmd_list]
|
|
75
81
|
try:
|
|
76
82
|
await self.run_process(command, check=True)
|
|
@@ -11,7 +11,7 @@ class lightfuzz(BaseModule):
|
|
|
11
11
|
|
|
12
12
|
options = {
|
|
13
13
|
"force_common_headers": False,
|
|
14
|
-
"enabled_submodules": ["sqli", "cmdi", "xss", "path", "ssti", "crypto", "serial"],
|
|
14
|
+
"enabled_submodules": ["sqli", "cmdi", "xss", "path", "ssti", "crypto", "serial", "esi"],
|
|
15
15
|
"disable_post": False,
|
|
16
16
|
}
|
|
17
17
|
options_desc = {
|
|
@@ -34,6 +34,7 @@ class lightfuzz(BaseModule):
|
|
|
34
34
|
self.event_dict = {}
|
|
35
35
|
self.interactsh_subdomain_tags = {}
|
|
36
36
|
self.interactsh_instance = None
|
|
37
|
+
self.interactsh_domain = None
|
|
37
38
|
self.disable_post = self.config.get("disable_post", False)
|
|
38
39
|
self.enabled_submodules = self.config.get("enabled_submodules")
|
|
39
40
|
self.interactsh_disable = self.scan.config.get("interactsh_disable", False)
|
|
@@ -51,13 +52,16 @@ class lightfuzz(BaseModule):
|
|
|
51
52
|
self.submodules[submodule_name] = submodule_class
|
|
52
53
|
|
|
53
54
|
interactsh_needed = any(submodule.uses_interactsh for submodule in self.submodules.values())
|
|
54
|
-
|
|
55
55
|
if interactsh_needed and not self.interactsh_disable:
|
|
56
56
|
try:
|
|
57
57
|
self.interactsh_instance = self.helpers.interactsh()
|
|
58
58
|
self.interactsh_domain = await self.interactsh_instance.register(callback=self.interactsh_callback)
|
|
59
|
+
if not self.interactsh_domain:
|
|
60
|
+
self.warning("Interactsh failure: No domain returned from self.interactsh_instance.register()")
|
|
61
|
+
self.interactsh_instance = None
|
|
59
62
|
except InteractshError as e:
|
|
60
63
|
self.warning(f"Interactsh failure: {e}")
|
|
64
|
+
self.interactsh_instance = None
|
|
61
65
|
return True
|
|
62
66
|
|
|
63
67
|
async def interactsh_callback(self, r):
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from .base import BaseLightfuzz
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class esi(BaseLightfuzz):
|
|
5
|
+
"""
|
|
6
|
+
Detects Edge Side Includes (ESI) processing vulnerabilities.
|
|
7
|
+
|
|
8
|
+
Tests if the server processes ESI tags by sending a payload containing ESI tags
|
|
9
|
+
and checking if the tags are processed (removed) in the response.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
# Technique lifted from https://github.com/PortSwigger/active-scan-plus-plus
|
|
13
|
+
|
|
14
|
+
friendly_name = "Edge Side Includes"
|
|
15
|
+
|
|
16
|
+
async def check_probe(self, cookies, probe, match):
|
|
17
|
+
"""
|
|
18
|
+
Sends the probe and checks if the expected match string is found in the response.
|
|
19
|
+
"""
|
|
20
|
+
probe_result = await self.standard_probe(self.event.data["type"], cookies, probe)
|
|
21
|
+
if probe_result and match in probe_result.text:
|
|
22
|
+
self.results.append(
|
|
23
|
+
{
|
|
24
|
+
"type": "FINDING",
|
|
25
|
+
"description": f"Edge Side Include. Parameter: [{self.event.data['name']}] Parameter Type: [{self.event.data['type']}]",
|
|
26
|
+
}
|
|
27
|
+
)
|
|
28
|
+
return True
|
|
29
|
+
return False
|
|
30
|
+
|
|
31
|
+
async def fuzz(self):
|
|
32
|
+
"""
|
|
33
|
+
Main fuzzing method that sends the ESI test payload and checks for processing.
|
|
34
|
+
"""
|
|
35
|
+
cookies = self.event.data.get("assigned_cookies", {})
|
|
36
|
+
|
|
37
|
+
# ESI test payload: if ESI is processed, <!--esi--> will be removed
|
|
38
|
+
# leaving AABB<!--esx-->CC in the response
|
|
39
|
+
payload = "AA<!--esi-->BB<!--esx-->CC"
|
|
40
|
+
detection_string = "AABB<!--esx-->CC"
|
|
41
|
+
|
|
42
|
+
await self.check_probe(cookies, payload, detection_string)
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import re
|
|
2
2
|
from bbot.modules.base import BaseModule
|
|
3
|
-
from bbot.errors import WordlistError
|
|
4
3
|
|
|
5
4
|
|
|
6
5
|
class medusa(BaseModule):
|
|
@@ -102,13 +101,11 @@ class medusa(BaseModule):
|
|
|
102
101
|
},
|
|
103
102
|
]
|
|
104
103
|
|
|
105
|
-
async def
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
self.snmp_wordlist_path = await self.helpers.wordlist(self.config.get("snmp_wordlist"))
|
|
109
|
-
except WordlistError as e:
|
|
110
|
-
return False, f"Error retrieving wordlist: {e}"
|
|
104
|
+
async def setup_deps(self):
|
|
105
|
+
self.snmp_wordlist_path = await self.helpers.wordlist(self.config.get("snmp_wordlist"))
|
|
106
|
+
return True
|
|
111
107
|
|
|
108
|
+
async def setup(self):
|
|
112
109
|
self.password_match_regex = re.compile(r"Password:\s*(\S+)")
|
|
113
110
|
self.success_indicator_match_regex = re.compile(r"\[([^\]]+)\]\s*$")
|
|
114
111
|
|
bbot/modules/nuclei.py
CHANGED
|
@@ -15,7 +15,7 @@ class nuclei(BaseModule):
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
options = {
|
|
18
|
-
"version": "3.
|
|
18
|
+
"version": "3.6.2",
|
|
19
19
|
"tags": "",
|
|
20
20
|
"templates": "",
|
|
21
21
|
"severity": "",
|
|
@@ -282,7 +282,7 @@ class nuclei(BaseModule):
|
|
|
282
282
|
else:
|
|
283
283
|
self.debug("Nuclei result missing one or more required elements, not reporting. JSON: ({j})")
|
|
284
284
|
finally:
|
|
285
|
-
stats_file.unlink()
|
|
285
|
+
stats_file.unlink(missing_ok=True)
|
|
286
286
|
|
|
287
287
|
def log_nuclei_status(self, line):
|
|
288
288
|
if self.silent:
|
bbot/modules/otx.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
from bbot.modules.templates.subdomain_enum import
|
|
1
|
+
from bbot.modules.templates.subdomain_enum import subdomain_enum_apikey
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
class otx(
|
|
4
|
+
class otx(subdomain_enum_apikey):
|
|
5
5
|
flags = ["subdomain-enum", "passive", "safe"]
|
|
6
6
|
watched_events = ["DNS_NAME"]
|
|
7
7
|
produced_events = ["DNS_NAME"]
|
|
@@ -9,10 +9,17 @@ class otx(subdomain_enum):
|
|
|
9
9
|
"description": "Query otx.alienvault.com for subdomains",
|
|
10
10
|
"created_date": "2022-08-24",
|
|
11
11
|
"author": "@TheTechromancer",
|
|
12
|
+
"auth_required": True,
|
|
12
13
|
}
|
|
14
|
+
options = {"api_key": ""}
|
|
15
|
+
options_desc = {"api_key": "OTX API key"}
|
|
13
16
|
|
|
14
17
|
base_url = "https://otx.alienvault.com"
|
|
15
18
|
|
|
19
|
+
def prepare_api_request(self, url, kwargs):
|
|
20
|
+
kwargs["headers"]["X-OTX-API-KEY"] = self.api_key
|
|
21
|
+
return url, kwargs
|
|
22
|
+
|
|
16
23
|
def request_url(self, query):
|
|
17
24
|
url = f"{self.base_url}/api/v1/indicators/domain/{self.helpers.quote(query)}/passive_dns"
|
|
18
25
|
return self.api_request(url)
|
bbot/modules/output/base.py
CHANGED
|
@@ -38,26 +38,18 @@ class BaseOutputModule(BaseModule):
|
|
|
38
38
|
if self._is_graph_important(event):
|
|
39
39
|
return True, "event is critical to the graph"
|
|
40
40
|
|
|
41
|
-
# exclude certain URLs (e.g. javascript):
|
|
42
|
-
# TODO: revisit this after httpx rework
|
|
43
|
-
if event.type.startswith("URL") and self.name != "httpx" and "httpx-only" in event.tags:
|
|
44
|
-
return False, (f"Omitting {event} from output because it's marked as httpx-only")
|
|
45
|
-
|
|
46
41
|
# omit certain event types
|
|
47
42
|
if event._omit:
|
|
48
|
-
if
|
|
49
|
-
reason = "it's a target"
|
|
50
|
-
self.debug(f"Allowing omitted event: {event} because {reason}")
|
|
51
|
-
elif event.type in self.get_watched_events():
|
|
43
|
+
if event.type in self.get_watched_events():
|
|
52
44
|
reason = "its type is explicitly in watched_events"
|
|
53
45
|
self.debug(f"Allowing omitted event: {event} because {reason}")
|
|
54
46
|
else:
|
|
55
|
-
return False, "
|
|
47
|
+
return False, "its type is omitted in the config"
|
|
56
48
|
|
|
57
49
|
# internal events like those from speculate, ipneighbor
|
|
58
50
|
# or events that are over our report distance
|
|
59
51
|
if event._internal:
|
|
60
|
-
return False, "
|
|
52
|
+
return False, "event is internal and output modules don't accept internal events"
|
|
61
53
|
|
|
62
54
|
return True, reason
|
|
63
55
|
|
|
@@ -82,18 +82,21 @@ class paramminer_headers(BaseModule):
|
|
|
82
82
|
|
|
83
83
|
header_regex = re.compile(r"^[!#$%&\'*+\-.^_`|~0-9a-zA-Z]+: [^\r\n]+$")
|
|
84
84
|
|
|
85
|
-
async def
|
|
86
|
-
self.recycle_words = self.config.get("recycle_words", True)
|
|
87
|
-
self.event_dict = {}
|
|
88
|
-
self.already_checked = set()
|
|
85
|
+
async def setup_deps(self):
|
|
89
86
|
wordlist = self.config.get("wordlist", "")
|
|
90
87
|
if not wordlist:
|
|
91
88
|
wordlist = f"{self.helpers.wordlist_dir}/{self.default_wordlist}"
|
|
89
|
+
self.wordlist_file = await self.helpers.wordlist(wordlist)
|
|
92
90
|
self.debug(f"Using wordlist: [{wordlist}]")
|
|
91
|
+
return True
|
|
92
|
+
|
|
93
|
+
async def setup(self):
|
|
94
|
+
self.recycle_words = self.config.get("recycle_words", True)
|
|
95
|
+
self.event_dict = {}
|
|
96
|
+
self.already_checked = set()
|
|
97
|
+
|
|
93
98
|
self.wl = {
|
|
94
|
-
h.strip().lower()
|
|
95
|
-
for h in self.helpers.read_file(await self.helpers.wordlist(wordlist))
|
|
96
|
-
if len(h) > 0 and "%" not in h
|
|
99
|
+
h.strip().lower() for h in self.helpers.read_file(self.wordlist_file) if len(h) > 0 and "%" not in h
|
|
97
100
|
}
|
|
98
101
|
|
|
99
102
|
# check against the boring list (if the option is set)
|
bbot/modules/passivetotal.py
CHANGED
|
@@ -21,7 +21,7 @@ class passivetotal(subdomain_enum_apikey):
|
|
|
21
21
|
|
|
22
22
|
async def ping(self):
|
|
23
23
|
url = f"{self.base_url}/account/quota"
|
|
24
|
-
j = (await self.api_request(url)).json()
|
|
24
|
+
j = (await self.api_request(url, retry_on_http_429=False)).json()
|
|
25
25
|
limit = j["user"]["limits"]["search_api"]
|
|
26
26
|
used = j["user"]["counts"]["search_api"]
|
|
27
27
|
assert used < limit, "No quota remaining"
|
bbot/modules/portfilter.py
CHANGED
|
@@ -19,6 +19,8 @@ class portfilter(BaseInterceptModule):
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
_priority = 4
|
|
22
|
+
# we consume URLs but we don't want to automatically enable httpx
|
|
23
|
+
_disable_auto_module_deps = True
|
|
22
24
|
|
|
23
25
|
async def setup(self):
|
|
24
26
|
self.cdn_tags = [t.strip() for t in self.config.get("cdn_tags", "").split(",")]
|