bbot 2.0.1.4720rc0__py3-none-any.whl → 2.3.0.5397rc0__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.
Potentially problematic release.
This version of bbot might be problematic. Click here for more details.
- bbot/__init__.py +1 -1
- bbot/cli.py +3 -7
- bbot/core/config/files.py +0 -1
- bbot/core/config/logger.py +34 -4
- bbot/core/core.py +21 -4
- bbot/core/engine.py +9 -8
- bbot/core/event/base.py +131 -52
- bbot/core/helpers/bloom.py +10 -3
- bbot/core/helpers/command.py +8 -7
- bbot/core/helpers/depsinstaller/installer.py +31 -13
- bbot/core/helpers/diff.py +10 -10
- bbot/core/helpers/dns/brute.py +7 -4
- bbot/core/helpers/dns/dns.py +1 -2
- bbot/core/helpers/dns/engine.py +4 -6
- bbot/core/helpers/dns/helpers.py +2 -2
- bbot/core/helpers/dns/mock.py +0 -1
- bbot/core/helpers/files.py +1 -1
- bbot/core/helpers/helper.py +7 -4
- bbot/core/helpers/interactsh.py +3 -3
- bbot/core/helpers/libmagic.py +65 -0
- bbot/core/helpers/misc.py +65 -22
- bbot/core/helpers/names_generator.py +17 -3
- bbot/core/helpers/process.py +0 -20
- bbot/core/helpers/regex.py +1 -1
- bbot/core/helpers/regexes.py +12 -6
- bbot/core/helpers/validators.py +1 -2
- bbot/core/helpers/web/client.py +1 -1
- bbot/core/helpers/web/engine.py +1 -2
- bbot/core/helpers/web/web.py +4 -114
- bbot/core/helpers/wordcloud.py +5 -5
- bbot/core/modules.py +36 -27
- bbot/core/multiprocess.py +58 -0
- bbot/core/shared_deps.py +46 -3
- bbot/db/sql/models.py +147 -0
- bbot/defaults.yml +11 -9
- bbot/modules/anubisdb.py +2 -2
- bbot/modules/apkpure.py +63 -0
- bbot/modules/azure_tenant.py +2 -2
- bbot/modules/baddns.py +35 -19
- bbot/modules/baddns_direct.py +92 -0
- bbot/modules/baddns_zone.py +3 -8
- bbot/modules/badsecrets.py +4 -3
- bbot/modules/base.py +195 -51
- bbot/modules/bevigil.py +7 -7
- bbot/modules/binaryedge.py +7 -4
- bbot/modules/bufferoverrun.py +47 -0
- bbot/modules/builtwith.py +6 -10
- bbot/modules/bypass403.py +5 -5
- bbot/modules/c99.py +10 -7
- bbot/modules/censys.py +9 -13
- bbot/modules/certspotter.py +5 -3
- bbot/modules/chaos.py +9 -7
- bbot/modules/code_repository.py +1 -0
- bbot/modules/columbus.py +3 -3
- bbot/modules/crt.py +5 -3
- bbot/modules/deadly/dastardly.py +1 -1
- bbot/modules/deadly/ffuf.py +9 -9
- bbot/modules/deadly/nuclei.py +3 -3
- bbot/modules/deadly/vhost.py +4 -3
- bbot/modules/dehashed.py +1 -1
- bbot/modules/digitorus.py +1 -1
- bbot/modules/dnsbimi.py +145 -0
- bbot/modules/dnscaa.py +3 -3
- bbot/modules/dnsdumpster.py +4 -4
- bbot/modules/dnstlsrpt.py +144 -0
- bbot/modules/docker_pull.py +7 -5
- bbot/modules/dockerhub.py +2 -2
- bbot/modules/dotnetnuke.py +18 -19
- bbot/modules/emailformat.py +1 -1
- bbot/modules/extractous.py +122 -0
- bbot/modules/filedownload.py +9 -7
- bbot/modules/fullhunt.py +7 -4
- bbot/modules/generic_ssrf.py +5 -5
- bbot/modules/github_codesearch.py +3 -2
- bbot/modules/github_org.py +4 -4
- bbot/modules/github_workflows.py +4 -4
- bbot/modules/gitlab.py +2 -5
- bbot/modules/google_playstore.py +93 -0
- bbot/modules/gowitness.py +48 -50
- bbot/modules/hackertarget.py +5 -3
- bbot/modules/host_header.py +5 -5
- bbot/modules/httpx.py +1 -4
- bbot/modules/hunterio.py +3 -9
- bbot/modules/iis_shortnames.py +19 -30
- bbot/modules/internal/cloudcheck.py +27 -12
- bbot/modules/internal/dnsresolve.py +22 -20
- bbot/modules/internal/excavate.py +85 -48
- bbot/modules/internal/speculate.py +41 -32
- bbot/modules/internetdb.py +4 -2
- bbot/modules/ip2location.py +3 -5
- bbot/modules/ipneighbor.py +1 -1
- bbot/modules/ipstack.py +3 -8
- bbot/modules/jadx.py +87 -0
- bbot/modules/leakix.py +11 -10
- bbot/modules/myssl.py +2 -2
- bbot/modules/newsletters.py +2 -2
- bbot/modules/otx.py +5 -3
- bbot/modules/output/asset_inventory.py +7 -7
- bbot/modules/output/base.py +1 -1
- bbot/modules/output/csv.py +1 -1
- bbot/modules/output/http.py +20 -14
- bbot/modules/output/mysql.py +51 -0
- bbot/modules/output/neo4j.py +7 -2
- bbot/modules/output/postgres.py +49 -0
- bbot/modules/output/slack.py +0 -1
- bbot/modules/output/sqlite.py +29 -0
- bbot/modules/output/stdout.py +2 -2
- bbot/modules/output/teams.py +107 -6
- bbot/modules/paramminer_headers.py +5 -8
- bbot/modules/passivetotal.py +13 -13
- bbot/modules/portscan.py +32 -6
- bbot/modules/postman.py +50 -126
- bbot/modules/postman_download.py +220 -0
- bbot/modules/rapiddns.py +3 -8
- bbot/modules/report/asn.py +11 -11
- bbot/modules/robots.py +3 -3
- bbot/modules/securitytrails.py +7 -10
- bbot/modules/securitytxt.py +1 -1
- bbot/modules/shodan_dns.py +7 -9
- bbot/modules/sitedossier.py +1 -1
- bbot/modules/skymem.py +2 -2
- bbot/modules/social.py +2 -1
- bbot/modules/subdomaincenter.py +1 -1
- bbot/modules/subdomainradar.py +160 -0
- bbot/modules/telerik.py +8 -8
- bbot/modules/templates/bucket.py +1 -1
- bbot/modules/templates/github.py +22 -14
- bbot/modules/templates/postman.py +21 -0
- bbot/modules/templates/shodan.py +14 -13
- bbot/modules/templates/sql.py +95 -0
- bbot/modules/templates/subdomain_enum.py +51 -16
- bbot/modules/templates/webhook.py +2 -4
- bbot/modules/trickest.py +8 -37
- bbot/modules/trufflehog.py +10 -12
- bbot/modules/url_manipulation.py +3 -3
- bbot/modules/urlscan.py +1 -1
- bbot/modules/viewdns.py +1 -1
- bbot/modules/virustotal.py +8 -30
- bbot/modules/wafw00f.py +1 -1
- bbot/modules/wayback.py +1 -1
- bbot/modules/wpscan.py +17 -11
- bbot/modules/zoomeye.py +11 -6
- bbot/presets/baddns-thorough.yml +12 -0
- bbot/presets/fast.yml +16 -0
- bbot/presets/kitchen-sink.yml +1 -0
- bbot/presets/spider.yml +4 -0
- bbot/presets/subdomain-enum.yml +7 -7
- bbot/scanner/manager.py +5 -16
- bbot/scanner/preset/args.py +44 -26
- bbot/scanner/preset/environ.py +7 -2
- bbot/scanner/preset/path.py +7 -4
- bbot/scanner/preset/preset.py +36 -23
- bbot/scanner/scanner.py +172 -62
- bbot/scanner/target.py +236 -434
- bbot/scripts/docs.py +1 -1
- bbot/test/bbot_fixtures.py +13 -3
- bbot/test/conftest.py +132 -100
- bbot/test/fastapi_test.py +17 -0
- bbot/test/owasp_mastg.apk +0 -0
- bbot/test/run_tests.sh +4 -4
- bbot/test/test.conf +2 -0
- bbot/test/test_step_1/test_bbot_fastapi.py +82 -0
- bbot/test/test_step_1/test_bloom_filter.py +2 -0
- bbot/test/test_step_1/test_cli.py +138 -64
- bbot/test/test_step_1/test_dns.py +62 -25
- bbot/test/test_step_1/test_engine.py +17 -17
- bbot/test/test_step_1/test_events.py +183 -28
- bbot/test/test_step_1/test_helpers.py +64 -28
- bbot/test/test_step_1/test_manager_deduplication.py +1 -1
- bbot/test/test_step_1/test_manager_scope_accuracy.py +333 -330
- bbot/test/test_step_1/test_modules_basic.py +68 -70
- bbot/test/test_step_1/test_presets.py +184 -96
- bbot/test/test_step_1/test_python_api.py +7 -2
- bbot/test/test_step_1/test_regexes.py +35 -5
- bbot/test/test_step_1/test_scan.py +39 -5
- bbot/test/test_step_1/test_scope.py +4 -3
- bbot/test/test_step_1/test_target.py +243 -145
- bbot/test/test_step_1/test_web.py +14 -8
- bbot/test/test_step_2/module_tests/base.py +15 -7
- bbot/test/test_step_2/module_tests/test_module_anubisdb.py +1 -1
- bbot/test/test_step_2/module_tests/test_module_apkpure.py +71 -0
- bbot/test/test_step_2/module_tests/test_module_asset_inventory.py +0 -1
- bbot/test/test_step_2/module_tests/test_module_azure_realm.py +1 -1
- bbot/test/test_step_2/module_tests/test_module_baddns.py +6 -6
- bbot/test/test_step_2/module_tests/test_module_baddns_direct.py +62 -0
- bbot/test/test_step_2/module_tests/test_module_bevigil.py +29 -2
- bbot/test/test_step_2/module_tests/test_module_binaryedge.py +4 -2
- bbot/test/test_step_2/module_tests/test_module_bucket_amazon.py +2 -2
- bbot/test/test_step_2/module_tests/test_module_bucket_azure.py +1 -1
- bbot/test/test_step_2/module_tests/test_module_bufferoverrun.py +35 -0
- bbot/test/test_step_2/module_tests/test_module_builtwith.py +2 -2
- bbot/test/test_step_2/module_tests/test_module_bypass403.py +1 -1
- bbot/test/test_step_2/module_tests/test_module_c99.py +126 -0
- bbot/test/test_step_2/module_tests/test_module_censys.py +4 -1
- bbot/test/test_step_2/module_tests/test_module_cloudcheck.py +4 -0
- bbot/test/test_step_2/module_tests/test_module_code_repository.py +11 -1
- bbot/test/test_step_2/module_tests/test_module_columbus.py +1 -1
- bbot/test/test_step_2/module_tests/test_module_credshed.py +3 -3
- bbot/test/test_step_2/module_tests/test_module_dastardly.py +2 -1
- bbot/test/test_step_2/module_tests/test_module_dehashed.py +2 -2
- bbot/test/test_step_2/module_tests/test_module_digitorus.py +1 -1
- bbot/test/test_step_2/module_tests/test_module_discord.py +1 -1
- bbot/test/test_step_2/module_tests/test_module_dnsbimi.py +103 -0
- bbot/test/test_step_2/module_tests/test_module_dnsbrute.py +9 -10
- bbot/test/test_step_2/module_tests/test_module_dnsbrute_mutations.py +1 -2
- bbot/test/test_step_2/module_tests/test_module_dnscommonsrv.py +1 -2
- bbot/test/test_step_2/module_tests/test_module_dnsdumpster.py +4 -4
- bbot/test/test_step_2/module_tests/test_module_dnstlsrpt.py +64 -0
- bbot/test/test_step_2/module_tests/test_module_dotnetnuke.py +0 -8
- bbot/test/test_step_2/module_tests/test_module_excavate.py +17 -37
- bbot/test/test_step_2/module_tests/test_module_extractous.py +54 -0
- bbot/test/test_step_2/module_tests/test_module_ffuf_shortnames.py +1 -1
- bbot/test/test_step_2/module_tests/test_module_filedownload.py +14 -14
- bbot/test/test_step_2/module_tests/test_module_git_clone.py +2 -2
- bbot/test/test_step_2/module_tests/test_module_github_org.py +19 -8
- bbot/test/test_step_2/module_tests/test_module_github_workflows.py +1 -1
- bbot/test/test_step_2/module_tests/test_module_gitlab.py +9 -4
- bbot/test/test_step_2/module_tests/test_module_google_playstore.py +83 -0
- bbot/test/test_step_2/module_tests/test_module_gowitness.py +4 -4
- bbot/test/test_step_2/module_tests/test_module_host_header.py +1 -1
- bbot/test/test_step_2/module_tests/test_module_http.py +4 -4
- bbot/test/test_step_2/module_tests/test_module_httpx.py +10 -8
- bbot/test/test_step_2/module_tests/test_module_hunterio.py +68 -4
- bbot/test/test_step_2/module_tests/test_module_jadx.py +55 -0
- bbot/test/test_step_2/module_tests/test_module_json.py +22 -9
- bbot/test/test_step_2/module_tests/test_module_leakix.py +7 -3
- bbot/test/test_step_2/module_tests/test_module_mysql.py +76 -0
- bbot/test/test_step_2/module_tests/test_module_myssl.py +1 -1
- bbot/test/test_step_2/module_tests/test_module_neo4j.py +1 -1
- bbot/test/test_step_2/module_tests/test_module_newsletters.py +6 -6
- bbot/test/test_step_2/module_tests/test_module_ntlm.py +7 -7
- bbot/test/test_step_2/module_tests/test_module_oauth.py +1 -1
- bbot/test/test_step_2/module_tests/test_module_otx.py +1 -1
- bbot/test/test_step_2/module_tests/test_module_paramminer_cookies.py +1 -2
- bbot/test/test_step_2/module_tests/test_module_paramminer_getparams.py +0 -6
- bbot/test/test_step_2/module_tests/test_module_paramminer_headers.py +2 -9
- bbot/test/test_step_2/module_tests/test_module_passivetotal.py +3 -1
- bbot/test/test_step_2/module_tests/test_module_portscan.py +9 -8
- bbot/test/test_step_2/module_tests/test_module_postgres.py +74 -0
- bbot/test/test_step_2/module_tests/test_module_postman.py +84 -253
- bbot/test/test_step_2/module_tests/test_module_postman_download.py +439 -0
- bbot/test/test_step_2/module_tests/test_module_rapiddns.py +93 -1
- bbot/test/test_step_2/module_tests/test_module_shodan_dns.py +20 -1
- bbot/test/test_step_2/module_tests/test_module_sitedossier.py +2 -2
- bbot/test/test_step_2/module_tests/test_module_smuggler.py +1 -1
- bbot/test/test_step_2/module_tests/test_module_social.py +11 -1
- bbot/test/test_step_2/module_tests/test_module_speculate.py +2 -6
- bbot/test/test_step_2/module_tests/test_module_splunk.py +4 -4
- bbot/test/test_step_2/module_tests/test_module_sqlite.py +18 -0
- bbot/test/test_step_2/module_tests/test_module_sslcert.py +1 -1
- bbot/test/test_step_2/module_tests/test_module_stdout.py +5 -3
- bbot/test/test_step_2/module_tests/test_module_subdomaincenter.py +1 -1
- bbot/test/test_step_2/module_tests/test_module_subdomainradar.py +208 -0
- bbot/test/test_step_2/module_tests/test_module_subdomains.py +1 -1
- bbot/test/test_step_2/module_tests/test_module_teams.py +8 -6
- bbot/test/test_step_2/module_tests/test_module_telerik.py +1 -1
- bbot/test/test_step_2/module_tests/test_module_trufflehog.py +317 -14
- bbot/test/test_step_2/module_tests/test_module_wayback.py +1 -1
- bbot/test/test_step_2/template_tests/test_template_subdomain_enum.py +2 -2
- {bbot-2.0.1.4720rc0.dist-info → bbot-2.3.0.5397rc0.dist-info}/METADATA +48 -18
- bbot-2.3.0.5397rc0.dist-info/RECORD +421 -0
- {bbot-2.0.1.4720rc0.dist-info → bbot-2.3.0.5397rc0.dist-info}/WHEEL +1 -1
- bbot/modules/unstructured.py +0 -163
- bbot/test/test_step_2/module_tests/test_module_unstructured.py +0 -102
- bbot-2.0.1.4720rc0.dist-info/RECORD +0 -387
- {bbot-2.0.1.4720rc0.dist-info → bbot-2.3.0.5397rc0.dist-info}/LICENSE +0 -0
- {bbot-2.0.1.4720rc0.dist-info → bbot-2.3.0.5397rc0.dist-info}/entry_points.txt +0 -0
|
@@ -3,11 +3,11 @@ from contextlib import suppress
|
|
|
3
3
|
|
|
4
4
|
from bbot.errors import ValidationError
|
|
5
5
|
from bbot.core.helpers.dns.engine import all_rdtypes
|
|
6
|
-
from bbot.modules.base import InterceptModule, BaseModule
|
|
7
6
|
from bbot.core.helpers.dns.helpers import extract_targets
|
|
7
|
+
from bbot.modules.base import BaseInterceptModule, BaseModule
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
class DNSResolve(
|
|
10
|
+
class DNSResolve(BaseInterceptModule):
|
|
11
11
|
watched_events = ["*"]
|
|
12
12
|
_priority = 1
|
|
13
13
|
scope_distance_modifier = None
|
|
@@ -16,12 +16,6 @@ class DNSResolve(InterceptModule):
|
|
|
16
16
|
_name = "host"
|
|
17
17
|
_type = "internal"
|
|
18
18
|
|
|
19
|
-
def _outgoing_dedup_hash(self, event):
|
|
20
|
-
# this exists to ensure a second, more interesting host isn't passed up
|
|
21
|
-
# because its ugly cousin spent its one dedup token before it arrived
|
|
22
|
-
# by removing those race conditions, this makes for more consistent results
|
|
23
|
-
return hash((event, self.name, event.always_emit))
|
|
24
|
-
|
|
25
19
|
@property
|
|
26
20
|
def module_threads(self):
|
|
27
21
|
return self.dns_config.get("threads", 25)
|
|
@@ -85,11 +79,21 @@ class DNSResolve(InterceptModule):
|
|
|
85
79
|
await self.resolve_event(main_host_event, types=non_minimal_rdtypes)
|
|
86
80
|
# check for wildcards if the event is within the scan's search distance
|
|
87
81
|
if new_event and main_host_event.scope_distance <= self.scan.scope_search_distance:
|
|
88
|
-
await self.handle_wildcard_event(main_host_event)
|
|
82
|
+
event_data_changed = await self.handle_wildcard_event(main_host_event)
|
|
83
|
+
if event_data_changed:
|
|
84
|
+
# since data has changed, we check again whether it's a duplicate
|
|
85
|
+
if self.scan.ingress_module.is_incoming_duplicate(event, add=True):
|
|
86
|
+
if not event._graph_important:
|
|
87
|
+
return False, "event was already emitted by its module"
|
|
88
|
+
else:
|
|
89
|
+
self.debug(
|
|
90
|
+
f"Event {event} was already emitted by its module, but it's graph-important so it gets a pass"
|
|
91
|
+
)
|
|
89
92
|
|
|
90
93
|
# if there weren't any DNS children and it's not an IP address, tag as unresolved
|
|
91
94
|
if not main_host_event.raw_dns_records and not event_is_ip:
|
|
92
95
|
main_host_event.add_tag("unresolved")
|
|
96
|
+
main_host_event.type = "DNS_NAME_UNRESOLVED"
|
|
93
97
|
|
|
94
98
|
# main_host_event.add_tag(f"resolve-distance-{main_host_event.dns_resolve_distance}")
|
|
95
99
|
|
|
@@ -109,10 +113,6 @@ class DNSResolve(InterceptModule):
|
|
|
109
113
|
if not self.minimal:
|
|
110
114
|
await self.emit_dns_children(main_host_event)
|
|
111
115
|
|
|
112
|
-
# If the event is unresolved, change its type to DNS_NAME_UNRESOLVED
|
|
113
|
-
if main_host_event.type == "DNS_NAME" and "unresolved" in main_host_event.tags:
|
|
114
|
-
main_host_event.type = "DNS_NAME_UNRESOLVED"
|
|
115
|
-
|
|
116
116
|
# emit the main DNS_NAME or IP_ADDRESS
|
|
117
117
|
if (
|
|
118
118
|
new_event
|
|
@@ -131,9 +131,9 @@ class DNSResolve(InterceptModule):
|
|
|
131
131
|
event.host, rdtypes=rdtypes, raw_dns_records=event.raw_dns_records
|
|
132
132
|
)
|
|
133
133
|
for rdtype, (is_wildcard, wildcard_host) in wildcard_rdtypes.items():
|
|
134
|
-
if is_wildcard
|
|
134
|
+
if is_wildcard is False:
|
|
135
135
|
continue
|
|
136
|
-
elif is_wildcard
|
|
136
|
+
elif is_wildcard is True:
|
|
137
137
|
event.add_tag("wildcard")
|
|
138
138
|
wildcard_tag = "wildcard"
|
|
139
139
|
else:
|
|
@@ -142,16 +142,16 @@ class DNSResolve(InterceptModule):
|
|
|
142
142
|
event.add_tag(f"{rdtype}-{wildcard_tag}")
|
|
143
143
|
|
|
144
144
|
# wildcard event modification (www.evilcorp.com --> _wildcard.evilcorp.com)
|
|
145
|
-
if wildcard_rdtypes and
|
|
145
|
+
if wildcard_rdtypes and "target" not in event.tags:
|
|
146
146
|
# these are the rdtypes that have wildcards
|
|
147
147
|
wildcard_rdtypes_set = set(wildcard_rdtypes)
|
|
148
148
|
# consider the event a full wildcard if all its records are wildcards
|
|
149
149
|
event_is_wildcard = False
|
|
150
150
|
if wildcard_rdtypes_set:
|
|
151
|
-
event_is_wildcard = all(r[0]
|
|
151
|
+
event_is_wildcard = all(r[0] is True for r in wildcard_rdtypes.values())
|
|
152
152
|
|
|
153
153
|
if event_is_wildcard:
|
|
154
|
-
if event.type in ("DNS_NAME",) and
|
|
154
|
+
if event.type in ("DNS_NAME",) and "_wildcard" not in event.data.split("."):
|
|
155
155
|
wildcard_parent = self.helpers.parent_domain(event.host)
|
|
156
156
|
for rdtype, (_is_wildcard, _parent_domain) in wildcard_rdtypes.items():
|
|
157
157
|
if _is_wildcard:
|
|
@@ -161,6 +161,8 @@ class DNSResolve(InterceptModule):
|
|
|
161
161
|
if wildcard_data != event.data:
|
|
162
162
|
self.debug(f'Wildcard detected, changing event.data "{event.data}" --> "{wildcard_data}"')
|
|
163
163
|
event.data = wildcard_data
|
|
164
|
+
return True
|
|
165
|
+
return False
|
|
164
166
|
|
|
165
167
|
async def emit_dns_children(self, event):
|
|
166
168
|
for rdtype, children in event.dns_children.items():
|
|
@@ -271,7 +273,7 @@ class DNSResolve(InterceptModule):
|
|
|
271
273
|
# tag event with errors
|
|
272
274
|
for rdtype, errors in dns_errors.items():
|
|
273
275
|
# only consider it an error if there weren't any results for that rdtype
|
|
274
|
-
if errors and not
|
|
276
|
+
if errors and rdtype not in event.dns_children:
|
|
275
277
|
event.add_tag(f"{rdtype}-error")
|
|
276
278
|
|
|
277
279
|
def get_dns_parent(self, event):
|
|
@@ -305,7 +307,7 @@ class DNSResolve(InterceptModule):
|
|
|
305
307
|
def emit_raw_records(self):
|
|
306
308
|
if self._emit_raw_records is None:
|
|
307
309
|
watching_raw_records = any(
|
|
308
|
-
|
|
310
|
+
"RAW_DNS_RECORD" in m.get_watched_events() for m in self.scan.modules.values()
|
|
309
311
|
)
|
|
310
312
|
omitted_event_types = self.scan.config.get("omit_event_types", [])
|
|
311
313
|
omit_raw_records = "RAW_DNS_RECORD" in omitted_event_types
|
|
@@ -6,6 +6,7 @@ import regex as re
|
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
from bbot.errors import ExcavateError
|
|
8
8
|
import bbot.core.helpers.regexes as bbot_regexes
|
|
9
|
+
from bbot.modules.base import BaseInterceptModule
|
|
9
10
|
from bbot.modules.internal.base import BaseInternalModule
|
|
10
11
|
from urllib.parse import urlparse, urljoin, parse_qs, urlunparse
|
|
11
12
|
|
|
@@ -61,7 +62,6 @@ def _exclude_key(original_dict, key_to_exclude):
|
|
|
61
62
|
|
|
62
63
|
|
|
63
64
|
def extract_params_url(parsed_url):
|
|
64
|
-
|
|
65
65
|
params = parse_qs(parsed_url.query)
|
|
66
66
|
flat_params = {k: v[0] for k, v in params.items()}
|
|
67
67
|
|
|
@@ -93,7 +93,6 @@ def extract_params_location(location_header_value, original_parsed_url):
|
|
|
93
93
|
|
|
94
94
|
|
|
95
95
|
class YaraRuleSettings:
|
|
96
|
-
|
|
97
96
|
def __init__(self, description, tags, emit_match):
|
|
98
97
|
self.description = description
|
|
99
98
|
self.tags = tags
|
|
@@ -153,7 +152,9 @@ class ExcavateRule:
|
|
|
153
152
|
yara_rule_settings = YaraRuleSettings(description, tags, emit_match)
|
|
154
153
|
yara_results = {}
|
|
155
154
|
for h in r.strings:
|
|
156
|
-
yara_results[h.identifier.lstrip("$")] = sorted(
|
|
155
|
+
yara_results[h.identifier.lstrip("$")] = sorted(
|
|
156
|
+
{i.matched_data.decode("utf-8", errors="ignore") for i in h.instances}
|
|
157
|
+
)
|
|
157
158
|
await self.process(yara_results, event, yara_rule_settings, discovery_context)
|
|
158
159
|
|
|
159
160
|
async def process(self, yara_results, event, yara_rule_settings, discovery_context):
|
|
@@ -179,7 +180,7 @@ class ExcavateRule:
|
|
|
179
180
|
Returns:
|
|
180
181
|
None
|
|
181
182
|
"""
|
|
182
|
-
for
|
|
183
|
+
for results in yara_results.values():
|
|
183
184
|
for result in results:
|
|
184
185
|
event_data = {"description": f"{discovery_context} {yara_rule_settings.description}"}
|
|
185
186
|
if yara_rule_settings.emit_match:
|
|
@@ -260,7 +261,6 @@ class ExcavateRule:
|
|
|
260
261
|
|
|
261
262
|
|
|
262
263
|
class CustomExtractor(ExcavateRule):
|
|
263
|
-
|
|
264
264
|
def __init__(self, excavate):
|
|
265
265
|
super().__init__(excavate)
|
|
266
266
|
|
|
@@ -279,7 +279,7 @@ class CustomExtractor(ExcavateRule):
|
|
|
279
279
|
await self.report(event_data, event, yara_rule_settings, discovery_context)
|
|
280
280
|
|
|
281
281
|
|
|
282
|
-
class excavate(BaseInternalModule):
|
|
282
|
+
class excavate(BaseInternalModule, BaseInterceptModule):
|
|
283
283
|
"""
|
|
284
284
|
Example (simple) Excavate Rules:
|
|
285
285
|
|
|
@@ -310,10 +310,11 @@ class excavate(BaseInternalModule):
|
|
|
310
310
|
"custom_yara_rules": "Include custom Yara rules",
|
|
311
311
|
}
|
|
312
312
|
scope_distance_modifier = None
|
|
313
|
+
accept_dupes = False
|
|
313
314
|
|
|
314
315
|
_module_threads = 8
|
|
315
316
|
|
|
316
|
-
parameter_blacklist =
|
|
317
|
+
parameter_blacklist = {
|
|
317
318
|
p.lower()
|
|
318
319
|
for p in [
|
|
319
320
|
"__VIEWSTATE",
|
|
@@ -328,7 +329,7 @@ class excavate(BaseInternalModule):
|
|
|
328
329
|
"JSESSIONID",
|
|
329
330
|
"PHPSESSID",
|
|
330
331
|
]
|
|
331
|
-
|
|
332
|
+
}
|
|
332
333
|
|
|
333
334
|
yara_rule_name_regex = re.compile(r"rule\s(\w+)\s{")
|
|
334
335
|
yara_rule_regex = re.compile(r"(?s)((?:rule\s+\w+\s*{[^{}]*(?:{[^{}]*}[^{}]*)*[^{}]*(?:/\S*?}[^/]*?/)*)*})")
|
|
@@ -354,7 +355,6 @@ class excavate(BaseInternalModule):
|
|
|
354
355
|
)
|
|
355
356
|
|
|
356
357
|
class ParameterExtractor(ExcavateRule):
|
|
357
|
-
|
|
358
358
|
yara_rules = {}
|
|
359
359
|
|
|
360
360
|
class ParameterExtractorRule:
|
|
@@ -368,7 +368,6 @@ class excavate(BaseInternalModule):
|
|
|
368
368
|
self.result = result
|
|
369
369
|
|
|
370
370
|
class GetJquery(ParameterExtractorRule):
|
|
371
|
-
|
|
372
371
|
name = "GET jquery"
|
|
373
372
|
discovery_regex = r"/\$.get\([^\)].+\)/ nocase"
|
|
374
373
|
extraction_regex = re.compile(r"\$.get\([\'\"](.+)[\'\"].+(\{.+\})\)")
|
|
@@ -389,8 +388,12 @@ class excavate(BaseInternalModule):
|
|
|
389
388
|
for action, extracted_parameters in extracted_results:
|
|
390
389
|
extracted_parameters_dict = self.convert_to_dict(extracted_parameters)
|
|
391
390
|
for parameter_name, original_value in extracted_parameters_dict.items():
|
|
392
|
-
yield
|
|
393
|
-
|
|
391
|
+
yield (
|
|
392
|
+
self.output_type,
|
|
393
|
+
parameter_name,
|
|
394
|
+
original_value,
|
|
395
|
+
action,
|
|
396
|
+
_exclude_key(extracted_parameters_dict, parameter_name),
|
|
394
397
|
)
|
|
395
398
|
|
|
396
399
|
class PostJquery(GetJquery):
|
|
@@ -414,8 +417,12 @@ class excavate(BaseInternalModule):
|
|
|
414
417
|
k: v[0] if isinstance(v, list) and len(v) == 1 else v for k, v in query_strings.items()
|
|
415
418
|
}
|
|
416
419
|
for parameter_name, original_value in query_strings_dict.items():
|
|
417
|
-
yield
|
|
418
|
-
|
|
420
|
+
yield (
|
|
421
|
+
self.output_type,
|
|
422
|
+
parameter_name,
|
|
423
|
+
original_value,
|
|
424
|
+
url,
|
|
425
|
+
_exclude_key(query_strings_dict, parameter_name),
|
|
419
426
|
)
|
|
420
427
|
|
|
421
428
|
class GetForm(ParameterExtractorRule):
|
|
@@ -440,8 +447,12 @@ class excavate(BaseInternalModule):
|
|
|
440
447
|
form_parameters[parameter_name] = original_value
|
|
441
448
|
|
|
442
449
|
for parameter_name, original_value in form_parameters.items():
|
|
443
|
-
yield
|
|
444
|
-
|
|
450
|
+
yield (
|
|
451
|
+
self.output_type,
|
|
452
|
+
parameter_name,
|
|
453
|
+
original_value,
|
|
454
|
+
form_action,
|
|
455
|
+
_exclude_key(form_parameters, parameter_name),
|
|
445
456
|
)
|
|
446
457
|
|
|
447
458
|
class PostForm(GetForm):
|
|
@@ -481,7 +492,6 @@ class excavate(BaseInternalModule):
|
|
|
481
492
|
endpoint,
|
|
482
493
|
additional_params,
|
|
483
494
|
) in extracted_params:
|
|
484
|
-
|
|
485
495
|
self.excavate.debug(
|
|
486
496
|
f"Found Parameter [{parameter_name}] in [{parameterExtractorSubModule.name}] ParameterExtractor Submodule"
|
|
487
497
|
)
|
|
@@ -493,7 +503,6 @@ class excavate(BaseInternalModule):
|
|
|
493
503
|
)
|
|
494
504
|
|
|
495
505
|
if self.excavate.helpers.validate_parameter(parameter_name, parameter_type):
|
|
496
|
-
|
|
497
506
|
if self.excavate.in_bl(parameter_name) == False:
|
|
498
507
|
parsed_url = urlparse(url)
|
|
499
508
|
description = f"HTTP Extracted Parameter [{parameter_name}] ({parameterExtractorSubModule.name} Submodule)"
|
|
@@ -523,13 +532,11 @@ class excavate(BaseInternalModule):
|
|
|
523
532
|
async def process(self, yara_results, event, yara_rule_settings, discovery_context):
|
|
524
533
|
for identifier in yara_results.keys():
|
|
525
534
|
for csp_str in yara_results[identifier]:
|
|
526
|
-
domains = await self.
|
|
527
|
-
|
|
528
|
-
for domain in unique_domains:
|
|
535
|
+
domains = await self.excavate.scan.extract_in_scope_hostnames(csp_str)
|
|
536
|
+
for domain in domains:
|
|
529
537
|
await self.report(domain, event, yara_rule_settings, discovery_context, event_type="DNS_NAME")
|
|
530
538
|
|
|
531
539
|
class EmailExtractor(ExcavateRule):
|
|
532
|
-
|
|
533
540
|
yara_rules = {
|
|
534
541
|
"email": 'rule email { meta: description = "contains email address" strings: $email = /[^\\W_][\\w\\-\\.\\+\']{0,100}@[a-zA-Z0-9\\-]{1,100}(\\.[a-zA-Z0-9\\-]{1,100})*\\.[a-zA-Z]{2,63}/ nocase fullword condition: $email }',
|
|
535
542
|
}
|
|
@@ -548,7 +555,6 @@ class excavate(BaseInternalModule):
|
|
|
548
555
|
}
|
|
549
556
|
|
|
550
557
|
class ErrorExtractor(ExcavateRule):
|
|
551
|
-
|
|
552
558
|
signatures = {
|
|
553
559
|
"PHP_1": r"/\.php on line [0-9]+/",
|
|
554
560
|
"PHP_2": r"/\.php<\/b> on line <b>[0-9]+/",
|
|
@@ -586,7 +592,6 @@ class excavate(BaseInternalModule):
|
|
|
586
592
|
await self.report(event_data, event, yara_rule_settings, discovery_context, event_type="FINDING")
|
|
587
593
|
|
|
588
594
|
class SerializationExtractor(ExcavateRule):
|
|
589
|
-
|
|
590
595
|
regexes = {
|
|
591
596
|
"Java": re.compile(r"[^a-zA-Z0-9\/+]rO0[a-zA-Z0-9+\/]+={0,2}"),
|
|
592
597
|
"DOTNET": re.compile(r"[^a-zA-Z0-9\/+]AAEAAAD\/\/[a-zA-Z0-9\/+]+={0,2}"),
|
|
@@ -616,7 +621,6 @@ class excavate(BaseInternalModule):
|
|
|
616
621
|
await self.report(event_data, event, yara_rule_settings, discovery_context, event_type="FINDING")
|
|
617
622
|
|
|
618
623
|
class FunctionalityExtractor(ExcavateRule):
|
|
619
|
-
|
|
620
624
|
yara_rules = {
|
|
621
625
|
"File_Upload_Functionality": r'rule File_Upload_Functionality { meta: description = "contains file upload functionality" strings: $fileuploadfunc = /<input[^>]+type=["\']?file["\']?[^>]+>/ nocase condition: $fileuploadfunc }',
|
|
622
626
|
"Web_Service_WSDL": r'rule Web_Service_WSDL { meta: emit_match = "True" description = "contains a web service WSDL URL" strings: $wsdl = /https?:\/\/[^\s]*\.(wsdl)/ nocase condition: $wsdl }',
|
|
@@ -630,7 +634,7 @@ class excavate(BaseInternalModule):
|
|
|
630
634
|
scheme_blacklist = ["javascript", "mailto", "tel", "data", "vbscript", "about", "file"]
|
|
631
635
|
|
|
632
636
|
async def process(self, yara_results, event, yara_rule_settings, discovery_context):
|
|
633
|
-
for
|
|
637
|
+
for results in yara_results.values():
|
|
634
638
|
for url_str in results:
|
|
635
639
|
scheme = url_str.split("://")[0]
|
|
636
640
|
if scheme in self.scheme_blacklist:
|
|
@@ -669,15 +673,38 @@ class excavate(BaseInternalModule):
|
|
|
669
673
|
|
|
670
674
|
class URLExtractor(ExcavateRule):
|
|
671
675
|
yara_rules = {
|
|
672
|
-
"url_full":
|
|
673
|
-
|
|
676
|
+
"url_full": (
|
|
677
|
+
r"""
|
|
678
|
+
rule url_full {
|
|
679
|
+
meta:
|
|
680
|
+
tags = "spider-danger"
|
|
681
|
+
description = "contains full URL"
|
|
682
|
+
strings:
|
|
683
|
+
$url_full = /https?:\/\/([\w\.-]+)(:\d{1,5})?([\/\w\.-]*)/
|
|
684
|
+
condition:
|
|
685
|
+
$url_full
|
|
686
|
+
}
|
|
687
|
+
"""
|
|
688
|
+
),
|
|
689
|
+
"url_attr": (
|
|
690
|
+
r"""
|
|
691
|
+
rule url_attr {
|
|
692
|
+
meta:
|
|
693
|
+
tags = "spider-danger"
|
|
694
|
+
description = "contains tag with src or href attribute"
|
|
695
|
+
strings:
|
|
696
|
+
$url_attr = /<[^>]+(href|src)=["\'][^"\']*["\'][^>]*>/
|
|
697
|
+
condition:
|
|
698
|
+
$url_attr
|
|
699
|
+
}
|
|
700
|
+
"""
|
|
701
|
+
),
|
|
674
702
|
}
|
|
675
703
|
full_url_regex = re.compile(r"(https?)://((?:\w|\d)(?:[\d\w-]+\.?)+(?::\d{1,5})?(?:/[-\w\.\(\)]*[-\w\.]+)*/?)")
|
|
676
704
|
full_url_regex_strict = re.compile(r"^(https?):\/\/([\w.-]+)(?::\d{1,5})?(\/[\w\/\.-]*)?(\?[^\s]+)?$")
|
|
677
705
|
tag_attribute_regex = bbot_regexes.tag_attribute_regex
|
|
678
706
|
|
|
679
707
|
async def process(self, yara_results, event, yara_rule_settings, discovery_context):
|
|
680
|
-
|
|
681
708
|
for identifier, results in yara_results.items():
|
|
682
709
|
urls_found = 0
|
|
683
710
|
final_url = ""
|
|
@@ -741,20 +768,33 @@ class excavate(BaseInternalModule):
|
|
|
741
768
|
|
|
742
769
|
def __init__(self, excavate):
|
|
743
770
|
super().__init__(excavate)
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
for i, r in enumerate(excavate.scan.dns_regexes_yara):
|
|
747
|
-
regexes_component_list.append(rf"$dns_name_{i} = /\b{r.pattern}/ nocase")
|
|
748
|
-
regexes_component = " ".join(regexes_component_list)
|
|
749
|
-
self.yara_rules[f"hostname_extraction"] = (
|
|
750
|
-
f'rule hostname_extraction {{meta: description = "matches DNS hostname pattern derived from target(s)" strings: {regexes_component} condition: any of them}}'
|
|
751
|
-
)
|
|
771
|
+
if excavate.scan.dns_yara_rules_uncompiled:
|
|
772
|
+
self.yara_rules[f"hostname_extraction"] = excavate.scan.dns_yara_rules_uncompiled
|
|
752
773
|
|
|
753
774
|
async def process(self, yara_results, event, yara_rule_settings, discovery_context):
|
|
754
775
|
for identifier in yara_results.keys():
|
|
755
776
|
for domain_str in yara_results[identifier]:
|
|
756
777
|
await self.report(domain_str, event, yara_rule_settings, discovery_context, event_type="DNS_NAME")
|
|
757
778
|
|
|
779
|
+
class LoginPageExtractor(ExcavateRule):
|
|
780
|
+
yara_rules = {
|
|
781
|
+
"login_page": r"""
|
|
782
|
+
rule login_page {
|
|
783
|
+
meta:
|
|
784
|
+
description = "Detects login pages with username and password fields"
|
|
785
|
+
strings:
|
|
786
|
+
$username_field = /<input[^>]+name=["']?(user|login|email)/ nocase
|
|
787
|
+
$password_field = /<input[^>]+name=["']?passw?/ nocase
|
|
788
|
+
condition:
|
|
789
|
+
$username_field and $password_field
|
|
790
|
+
}
|
|
791
|
+
"""
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
async def process(self, yara_results, event, yara_rule_settings, discovery_context):
|
|
795
|
+
if yara_results:
|
|
796
|
+
event.add_tag("login-page")
|
|
797
|
+
|
|
758
798
|
def add_yara_rule(self, rule_name, rule_content, rule_instance):
|
|
759
799
|
rule_instance.name = rule_name
|
|
760
800
|
self.yara_rules_dict[rule_name] = rule_content
|
|
@@ -805,9 +845,9 @@ class excavate(BaseInternalModule):
|
|
|
805
845
|
if Path(self.custom_yara_rules).is_file():
|
|
806
846
|
with open(self.custom_yara_rules) as f:
|
|
807
847
|
rules_content = f.read()
|
|
808
|
-
self.debug(f"Successfully loaded
|
|
848
|
+
self.debug(f"Successfully loaded custom yara rules file [{self.custom_yara_rules}]")
|
|
809
849
|
else:
|
|
810
|
-
self.debug(f"Custom
|
|
850
|
+
self.debug(f"Custom yara rules file is NOT a file. Will attempt to treat it as rule content")
|
|
811
851
|
rules_content = self.custom_yara_rules
|
|
812
852
|
|
|
813
853
|
self.debug(f"Final combined yara rule contents: {rules_content}")
|
|
@@ -816,13 +856,11 @@ class excavate(BaseInternalModule):
|
|
|
816
856
|
try:
|
|
817
857
|
yara.compile(source=rule_content)
|
|
818
858
|
except yara.SyntaxError as e:
|
|
819
|
-
|
|
820
|
-
return False
|
|
859
|
+
return False, f"Custom Yara rule failed to compile: {e}"
|
|
821
860
|
|
|
822
861
|
rule_match = await self.helpers.re.search(self.yara_rule_name_regex, rule_content)
|
|
823
862
|
if not rule_match:
|
|
824
|
-
|
|
825
|
-
return False
|
|
863
|
+
return False, f"Custom Yara formatted incorrectly: could not find rule name"
|
|
826
864
|
|
|
827
865
|
rule_name = rule_match.groups(1)[0]
|
|
828
866
|
c = CustomExtractor(self)
|
|
@@ -836,11 +874,13 @@ class excavate(BaseInternalModule):
|
|
|
836
874
|
yara.set_config(max_match_data=yara_max_match_data)
|
|
837
875
|
yara_rules_combined = "\n".join(self.yara_rules_dict.values())
|
|
838
876
|
try:
|
|
877
|
+
self.info(f"Compiling {len(self.yara_rules_dict):,} YARA rules")
|
|
878
|
+
for rule_name, rule_content in self.yara_rules_dict.items():
|
|
879
|
+
self.debug(f" - {rule_name}")
|
|
839
880
|
self.yara_rules = yara.compile(source=yara_rules_combined)
|
|
840
881
|
except yara.SyntaxError as e:
|
|
841
|
-
self.hugewarning(f"Yara Rules failed to compile with error: [{e}]")
|
|
842
882
|
self.debug(yara_rules_combined)
|
|
843
|
-
return False
|
|
883
|
+
return False, f"Yara Rules failed to compile with error: [{e}]"
|
|
844
884
|
|
|
845
885
|
# pre-load valid URL schemes
|
|
846
886
|
valid_schemes_filename = self.helpers.wordlist_dir / "valid_url_schemes.txt"
|
|
@@ -857,7 +897,6 @@ class excavate(BaseInternalModule):
|
|
|
857
897
|
decoded_data = await self.helpers.re.recursive_decode(data)
|
|
858
898
|
|
|
859
899
|
if self.parameter_extraction:
|
|
860
|
-
|
|
861
900
|
content_type_lower = content_type.lower() if content_type else ""
|
|
862
901
|
extraction_map = {
|
|
863
902
|
"json": self.helpers.extract_params_json,
|
|
@@ -894,7 +933,6 @@ class excavate(BaseInternalModule):
|
|
|
894
933
|
self.hugewarning(f"YARA Rule {rule_name} not found in pre-compiled rules")
|
|
895
934
|
|
|
896
935
|
async def handle_event(self, event):
|
|
897
|
-
|
|
898
936
|
if event.type == "HTTP_RESPONSE":
|
|
899
937
|
# Harvest GET parameters from URL, if it came directly from the target, and parameter extraction is enabled
|
|
900
938
|
if (
|
|
@@ -983,7 +1021,6 @@ class excavate(BaseInternalModule):
|
|
|
983
1021
|
|
|
984
1022
|
# Try to extract parameters from the redirect URL
|
|
985
1023
|
if self.parameter_extraction:
|
|
986
|
-
|
|
987
1024
|
for (
|
|
988
1025
|
method,
|
|
989
1026
|
parsed_url,
|
|
@@ -32,10 +32,11 @@ class speculate(BaseInternalModule):
|
|
|
32
32
|
"author": "@liquidsec",
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
options = {"max_hosts": 65536, "ports": "80,443"}
|
|
35
|
+
options = {"max_hosts": 65536, "ports": "80,443", "essential_only": False}
|
|
36
36
|
options_desc = {
|
|
37
37
|
"max_hosts": "Max number of IP_RANGE hosts to convert into IP_ADDRESS events",
|
|
38
38
|
"ports": "The set of ports to speculate on",
|
|
39
|
+
"essential_only": "Only enable essential speculate features (no extra discovery)",
|
|
39
40
|
}
|
|
40
41
|
scope_distance_modifier = 1
|
|
41
42
|
_priority = 4
|
|
@@ -44,14 +45,15 @@ class speculate(BaseInternalModule):
|
|
|
44
45
|
|
|
45
46
|
async def setup(self):
|
|
46
47
|
scan_modules = [m for m in self.scan.modules.values() if m._type == "scan"]
|
|
47
|
-
self.open_port_consumers = any(
|
|
48
|
+
self.open_port_consumers = any("OPEN_TCP_PORT" in m.watched_events for m in scan_modules)
|
|
48
49
|
# only consider active portscanners (still speculate if only passive ones are enabled)
|
|
49
50
|
self.portscanner_enabled = any(
|
|
50
|
-
|
|
51
|
+
"portscan" in m.flags and "active" in m.flags for m in self.scan.modules.values()
|
|
51
52
|
)
|
|
52
53
|
self.emit_open_ports = self.open_port_consumers and not self.portscanner_enabled
|
|
53
54
|
self.range_to_ip = True
|
|
54
55
|
self.dns_disable = self.scan.config.get("dns", {}).get("disable", False)
|
|
56
|
+
self.essential_only = self.config.get("essential_only", False)
|
|
55
57
|
self.org_stubs_seen = set()
|
|
56
58
|
|
|
57
59
|
port_string = self.config.get("ports", "80,443")
|
|
@@ -63,18 +65,26 @@ class speculate(BaseInternalModule):
|
|
|
63
65
|
if not self.portscanner_enabled:
|
|
64
66
|
self.info(f"No portscanner enabled. Assuming open ports: {', '.join(str(x) for x in self.ports)}")
|
|
65
67
|
|
|
66
|
-
target_len = len(self.scan.target)
|
|
68
|
+
target_len = len(self.scan.target.seeds)
|
|
67
69
|
if target_len > self.config.get("max_hosts", 65536):
|
|
68
70
|
if not self.portscanner_enabled:
|
|
69
71
|
self.hugewarning(
|
|
70
72
|
f"Selected target ({target_len:,} hosts) is too large, skipping IP_RANGE --> IP_ADDRESS speculation"
|
|
71
73
|
)
|
|
72
|
-
self.hugewarning(
|
|
74
|
+
self.hugewarning('Enabling the "portscan" module is highly recommended')
|
|
73
75
|
self.range_to_ip = False
|
|
74
76
|
|
|
75
77
|
return True
|
|
76
78
|
|
|
77
79
|
async def handle_event(self, event):
|
|
80
|
+
### BEGIN ESSENTIAL SPECULATION ###
|
|
81
|
+
# These features are required for smooth operation of bbot
|
|
82
|
+
# I.e. they are not "osinty" or intended to discover anything, they only compliment other modules
|
|
83
|
+
|
|
84
|
+
# we speculate on distance-1 stuff too, because distance-1 open ports are needed by certain modules like sslcert
|
|
85
|
+
event_in_scope_distance = event.scope_distance <= (self.scan.scope_search_distance + 1)
|
|
86
|
+
speculate_open_ports = self.emit_open_ports and event_in_scope_distance
|
|
87
|
+
|
|
78
88
|
# generate individual IP addresses from IP range
|
|
79
89
|
if event.type == "IP_RANGE" and self.range_to_ip:
|
|
80
90
|
net = ipaddress.ip_network(event.data)
|
|
@@ -89,28 +99,46 @@ class speculate(BaseInternalModule):
|
|
|
89
99
|
context=f"speculate converted range into individual IP_ADDRESS: {ip}",
|
|
90
100
|
)
|
|
91
101
|
|
|
102
|
+
# IP_ADDRESS / DNS_NAME --> OPEN_TCP_PORT
|
|
103
|
+
if speculate_open_ports:
|
|
104
|
+
# don't act on unresolved DNS_NAMEs
|
|
105
|
+
usable_dns = False
|
|
106
|
+
if event.type == "DNS_NAME":
|
|
107
|
+
if self.dns_disable or ("a-record" in event.tags or "aaaa-record" in event.tags):
|
|
108
|
+
usable_dns = True
|
|
109
|
+
|
|
110
|
+
if event.type == "IP_ADDRESS" or usable_dns:
|
|
111
|
+
for port in self.ports:
|
|
112
|
+
await self.emit_event(
|
|
113
|
+
self.helpers.make_netloc(event.data, port),
|
|
114
|
+
"OPEN_TCP_PORT",
|
|
115
|
+
parent=event,
|
|
116
|
+
internal=True,
|
|
117
|
+
context="speculated {event.type}: {event.data}",
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
### END ESSENTIAL SPECULATION ###
|
|
121
|
+
if self.essential_only:
|
|
122
|
+
return
|
|
123
|
+
|
|
92
124
|
# parent domains
|
|
93
125
|
if event.type.startswith("DNS_NAME"):
|
|
94
126
|
parent = self.helpers.parent_domain(event.host_original)
|
|
95
127
|
if parent != event.data:
|
|
96
128
|
await self.emit_event(
|
|
97
|
-
parent, "DNS_NAME", parent=event, context=
|
|
129
|
+
parent, "DNS_NAME", parent=event, context="speculated parent {event.type}: {event.data}"
|
|
98
130
|
)
|
|
99
131
|
|
|
100
|
-
# we speculate on distance-1 stuff too, because distance-1 open ports are needed by certain modules like sslcert
|
|
101
|
-
event_in_scope_distance = event.scope_distance <= (self.scan.scope_search_distance + 1)
|
|
102
|
-
speculate_open_ports = self.emit_open_ports and event_in_scope_distance
|
|
103
|
-
|
|
104
132
|
# URL --> OPEN_TCP_PORT
|
|
105
|
-
|
|
133
|
+
event_is_url = event.type == "URL"
|
|
134
|
+
if event_is_url or (event.type == "URL_UNVERIFIED" and self.open_port_consumers):
|
|
106
135
|
# only speculate port from a URL if it wouldn't be speculated naturally from the host
|
|
107
136
|
if event.host and (event.port not in self.ports or not speculate_open_ports):
|
|
108
137
|
await self.emit_event(
|
|
109
138
|
self.helpers.make_netloc(event.host, event.port),
|
|
110
139
|
"OPEN_TCP_PORT",
|
|
111
140
|
parent=event,
|
|
112
|
-
internal=
|
|
113
|
-
quick=(event.type == "URL"),
|
|
141
|
+
internal=not event_is_url, # if the URL is verified, the port is definitely open
|
|
114
142
|
context=f"speculated {{event.type}} from {event.type}: {{event.data}}",
|
|
115
143
|
)
|
|
116
144
|
|
|
@@ -144,25 +172,6 @@ class speculate(BaseInternalModule):
|
|
|
144
172
|
context="speculated {event.type}: {event.data}",
|
|
145
173
|
)
|
|
146
174
|
|
|
147
|
-
# IP_ADDRESS / DNS_NAME --> OPEN_TCP_PORT
|
|
148
|
-
if speculate_open_ports:
|
|
149
|
-
# don't act on unresolved DNS_NAMEs
|
|
150
|
-
usable_dns = False
|
|
151
|
-
if event.type == "DNS_NAME":
|
|
152
|
-
if self.dns_disable or ("a-record" in event.tags or "aaaa-record" in event.tags):
|
|
153
|
-
usable_dns = True
|
|
154
|
-
|
|
155
|
-
if event.type == "IP_ADDRESS" or usable_dns:
|
|
156
|
-
for port in self.ports:
|
|
157
|
-
await self.emit_event(
|
|
158
|
-
self.helpers.make_netloc(event.data, port),
|
|
159
|
-
"OPEN_TCP_PORT",
|
|
160
|
-
parent=event,
|
|
161
|
-
internal=True,
|
|
162
|
-
quick=True,
|
|
163
|
-
context="speculated {event.type}: {event.data}",
|
|
164
|
-
)
|
|
165
|
-
|
|
166
175
|
# ORG_STUB from TLD, SOCIAL, AZURE_TENANT
|
|
167
176
|
org_stubs = set()
|
|
168
177
|
if event.type == "DNS_NAME" and event.scope_distance == 0:
|
bbot/modules/internetdb.py
CHANGED
|
@@ -48,6 +48,9 @@ class internetdb(BaseModule):
|
|
|
48
48
|
"show_open_ports": "Display OPEN_TCP_PORT events in output, even if they didn't lead to an interesting discovery"
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
# we get lots of 404s, that's normal
|
|
52
|
+
_api_failure_abort_threshold = 9999999999
|
|
53
|
+
|
|
51
54
|
_qsize = 500
|
|
52
55
|
|
|
53
56
|
base_url = "https://internetdb.shodan.io"
|
|
@@ -64,7 +67,7 @@ class internetdb(BaseModule):
|
|
|
64
67
|
if ip is None:
|
|
65
68
|
return
|
|
66
69
|
url = f"{self.base_url}/{ip}"
|
|
67
|
-
r = await self.
|
|
70
|
+
r = await self.api_request(url)
|
|
68
71
|
if r is None:
|
|
69
72
|
self.debug(f"No response for {event.data}")
|
|
70
73
|
return
|
|
@@ -113,7 +116,6 @@ class internetdb(BaseModule):
|
|
|
113
116
|
"OPEN_TCP_PORT",
|
|
114
117
|
parent=event,
|
|
115
118
|
internal=(not self.show_open_ports),
|
|
116
|
-
quick=True,
|
|
117
119
|
context=f'{{module}} queried Shodan\'s InternetDB API for "{query_host}" and found {{event.type}}: {{event.data}}',
|
|
118
120
|
)
|
|
119
121
|
vulns = data.get("vulns", [])
|
bbot/modules/ip2location.py
CHANGED
|
@@ -32,12 +32,10 @@ class IP2Location(BaseModule):
|
|
|
32
32
|
|
|
33
33
|
async def ping(self):
|
|
34
34
|
url = self.build_url("8.8.8.8")
|
|
35
|
-
|
|
36
|
-
resp_content = getattr(r, "text", "")
|
|
37
|
-
assert getattr(r, "status_code", 0) == 200, resp_content
|
|
35
|
+
await super().ping(url)
|
|
38
36
|
|
|
39
37
|
def build_url(self, data):
|
|
40
|
-
url = f"{self.base_url}/?key={
|
|
38
|
+
url = f"{self.base_url}/?key={{api_key}}&ip={data}&format=json&source=bbot"
|
|
41
39
|
if self.lang:
|
|
42
40
|
url = f"{url}&lang={self.lang}"
|
|
43
41
|
return url
|
|
@@ -45,7 +43,7 @@ class IP2Location(BaseModule):
|
|
|
45
43
|
async def handle_event(self, event):
|
|
46
44
|
try:
|
|
47
45
|
url = self.build_url(event.data)
|
|
48
|
-
result = await self.
|
|
46
|
+
result = await self.api_request(url)
|
|
49
47
|
if result:
|
|
50
48
|
geo_data = result.json()
|
|
51
49
|
if not geo_data:
|
bbot/modules/ipneighbor.py
CHANGED
|
@@ -31,7 +31,7 @@ class ipneighbor(BaseModule):
|
|
|
31
31
|
netmask = main_ip.max_prefixlen - min(main_ip.max_prefixlen, self.num_bits)
|
|
32
32
|
network = ipaddress.ip_network(f"{main_ip}/{netmask}", strict=False)
|
|
33
33
|
subnet_hash = hash(network)
|
|
34
|
-
if not
|
|
34
|
+
if subnet_hash not in self.processed:
|
|
35
35
|
self.processed.add(subnet_hash)
|
|
36
36
|
for ip in network:
|
|
37
37
|
if ip != main_ip:
|