bbot 2.0.1.4654rc0__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.

Files changed (270) hide show
  1. bbot/__init__.py +1 -1
  2. bbot/cli.py +3 -7
  3. bbot/core/config/files.py +0 -1
  4. bbot/core/config/logger.py +34 -4
  5. bbot/core/core.py +21 -6
  6. bbot/core/engine.py +9 -8
  7. bbot/core/event/base.py +162 -63
  8. bbot/core/helpers/bloom.py +10 -3
  9. bbot/core/helpers/command.py +9 -8
  10. bbot/core/helpers/depsinstaller/installer.py +89 -32
  11. bbot/core/helpers/depsinstaller/sudo_askpass.py +38 -2
  12. bbot/core/helpers/diff.py +10 -10
  13. bbot/core/helpers/dns/brute.py +18 -14
  14. bbot/core/helpers/dns/dns.py +16 -15
  15. bbot/core/helpers/dns/engine.py +159 -132
  16. bbot/core/helpers/dns/helpers.py +2 -2
  17. bbot/core/helpers/dns/mock.py +26 -8
  18. bbot/core/helpers/files.py +1 -1
  19. bbot/core/helpers/helper.py +7 -4
  20. bbot/core/helpers/interactsh.py +3 -3
  21. bbot/core/helpers/libmagic.py +65 -0
  22. bbot/core/helpers/misc.py +65 -22
  23. bbot/core/helpers/names_generator.py +17 -3
  24. bbot/core/helpers/process.py +0 -20
  25. bbot/core/helpers/regex.py +1 -1
  26. bbot/core/helpers/regexes.py +12 -6
  27. bbot/core/helpers/validators.py +1 -2
  28. bbot/core/helpers/web/client.py +1 -1
  29. bbot/core/helpers/web/engine.py +18 -13
  30. bbot/core/helpers/web/web.py +25 -116
  31. bbot/core/helpers/wordcloud.py +5 -5
  32. bbot/core/modules.py +36 -27
  33. bbot/core/multiprocess.py +58 -0
  34. bbot/core/shared_deps.py +46 -3
  35. bbot/db/sql/models.py +147 -0
  36. bbot/defaults.yml +15 -10
  37. bbot/errors.py +0 -8
  38. bbot/modules/anubisdb.py +2 -2
  39. bbot/modules/apkpure.py +63 -0
  40. bbot/modules/azure_tenant.py +2 -2
  41. bbot/modules/baddns.py +35 -19
  42. bbot/modules/baddns_direct.py +92 -0
  43. bbot/modules/baddns_zone.py +3 -8
  44. bbot/modules/badsecrets.py +4 -3
  45. bbot/modules/base.py +195 -51
  46. bbot/modules/bevigil.py +7 -7
  47. bbot/modules/binaryedge.py +7 -4
  48. bbot/modules/bufferoverrun.py +47 -0
  49. bbot/modules/builtwith.py +6 -10
  50. bbot/modules/bypass403.py +5 -5
  51. bbot/modules/c99.py +10 -7
  52. bbot/modules/censys.py +9 -13
  53. bbot/modules/certspotter.py +5 -3
  54. bbot/modules/chaos.py +9 -7
  55. bbot/modules/code_repository.py +1 -0
  56. bbot/modules/columbus.py +3 -3
  57. bbot/modules/crt.py +5 -3
  58. bbot/modules/deadly/dastardly.py +1 -1
  59. bbot/modules/deadly/ffuf.py +9 -9
  60. bbot/modules/deadly/nuclei.py +3 -3
  61. bbot/modules/deadly/vhost.py +4 -3
  62. bbot/modules/dehashed.py +1 -1
  63. bbot/modules/digitorus.py +1 -1
  64. bbot/modules/dnsbimi.py +145 -0
  65. bbot/modules/dnscaa.py +3 -3
  66. bbot/modules/dnsdumpster.py +4 -4
  67. bbot/modules/dnstlsrpt.py +144 -0
  68. bbot/modules/docker_pull.py +7 -5
  69. bbot/modules/dockerhub.py +2 -2
  70. bbot/modules/dotnetnuke.py +18 -19
  71. bbot/modules/emailformat.py +1 -1
  72. bbot/modules/extractous.py +122 -0
  73. bbot/modules/filedownload.py +9 -7
  74. bbot/modules/fullhunt.py +7 -4
  75. bbot/modules/generic_ssrf.py +5 -5
  76. bbot/modules/github_codesearch.py +3 -2
  77. bbot/modules/github_org.py +4 -4
  78. bbot/modules/github_workflows.py +4 -4
  79. bbot/modules/gitlab.py +2 -5
  80. bbot/modules/google_playstore.py +93 -0
  81. bbot/modules/gowitness.py +48 -50
  82. bbot/modules/hackertarget.py +5 -3
  83. bbot/modules/host_header.py +5 -5
  84. bbot/modules/httpx.py +1 -4
  85. bbot/modules/hunterio.py +3 -9
  86. bbot/modules/iis_shortnames.py +19 -30
  87. bbot/modules/internal/cloudcheck.py +27 -12
  88. bbot/modules/internal/dnsresolve.py +250 -276
  89. bbot/modules/internal/excavate.py +100 -64
  90. bbot/modules/internal/speculate.py +42 -33
  91. bbot/modules/internetdb.py +4 -2
  92. bbot/modules/ip2location.py +3 -5
  93. bbot/modules/ipneighbor.py +1 -1
  94. bbot/modules/ipstack.py +3 -8
  95. bbot/modules/jadx.py +87 -0
  96. bbot/modules/leakix.py +11 -10
  97. bbot/modules/myssl.py +2 -2
  98. bbot/modules/newsletters.py +2 -2
  99. bbot/modules/otx.py +5 -3
  100. bbot/modules/output/asset_inventory.py +7 -7
  101. bbot/modules/output/base.py +1 -1
  102. bbot/modules/output/csv.py +1 -2
  103. bbot/modules/output/http.py +20 -14
  104. bbot/modules/output/mysql.py +51 -0
  105. bbot/modules/output/neo4j.py +7 -2
  106. bbot/modules/output/postgres.py +49 -0
  107. bbot/modules/output/slack.py +0 -1
  108. bbot/modules/output/sqlite.py +29 -0
  109. bbot/modules/output/stdout.py +2 -2
  110. bbot/modules/output/teams.py +107 -6
  111. bbot/modules/paramminer_headers.py +5 -8
  112. bbot/modules/passivetotal.py +13 -13
  113. bbot/modules/portscan.py +32 -6
  114. bbot/modules/postman.py +50 -126
  115. bbot/modules/postman_download.py +220 -0
  116. bbot/modules/rapiddns.py +3 -8
  117. bbot/modules/report/asn.py +11 -11
  118. bbot/modules/robots.py +3 -3
  119. bbot/modules/securitytrails.py +7 -10
  120. bbot/modules/securitytxt.py +128 -0
  121. bbot/modules/shodan_dns.py +7 -9
  122. bbot/modules/sitedossier.py +1 -1
  123. bbot/modules/skymem.py +2 -2
  124. bbot/modules/social.py +2 -1
  125. bbot/modules/subdomaincenter.py +1 -1
  126. bbot/modules/subdomainradar.py +160 -0
  127. bbot/modules/telerik.py +8 -8
  128. bbot/modules/templates/bucket.py +1 -1
  129. bbot/modules/templates/github.py +22 -14
  130. bbot/modules/templates/postman.py +21 -0
  131. bbot/modules/templates/shodan.py +14 -13
  132. bbot/modules/templates/sql.py +95 -0
  133. bbot/modules/templates/subdomain_enum.py +53 -17
  134. bbot/modules/templates/webhook.py +2 -4
  135. bbot/modules/trickest.py +8 -37
  136. bbot/modules/trufflehog.py +18 -3
  137. bbot/modules/url_manipulation.py +3 -3
  138. bbot/modules/urlscan.py +1 -1
  139. bbot/modules/viewdns.py +1 -1
  140. bbot/modules/virustotal.py +8 -30
  141. bbot/modules/wafw00f.py +1 -1
  142. bbot/modules/wayback.py +1 -1
  143. bbot/modules/wpscan.py +17 -11
  144. bbot/modules/zoomeye.py +11 -6
  145. bbot/presets/baddns-thorough.yml +12 -0
  146. bbot/presets/fast.yml +16 -0
  147. bbot/presets/kitchen-sink.yml +1 -0
  148. bbot/presets/spider.yml +4 -0
  149. bbot/presets/subdomain-enum.yml +7 -7
  150. bbot/scanner/manager.py +5 -16
  151. bbot/scanner/preset/args.py +44 -26
  152. bbot/scanner/preset/environ.py +7 -2
  153. bbot/scanner/preset/path.py +7 -4
  154. bbot/scanner/preset/preset.py +36 -23
  155. bbot/scanner/scanner.py +176 -63
  156. bbot/scanner/target.py +236 -434
  157. bbot/scripts/docs.py +1 -1
  158. bbot/test/bbot_fixtures.py +22 -3
  159. bbot/test/conftest.py +132 -100
  160. bbot/test/fastapi_test.py +17 -0
  161. bbot/test/owasp_mastg.apk +0 -0
  162. bbot/test/run_tests.sh +4 -4
  163. bbot/test/test.conf +2 -0
  164. bbot/test/test_step_1/test_bbot_fastapi.py +82 -0
  165. bbot/test/test_step_1/test_bloom_filter.py +2 -0
  166. bbot/test/test_step_1/test_cli.py +138 -64
  167. bbot/test/test_step_1/test_dns.py +392 -70
  168. bbot/test/test_step_1/test_engine.py +17 -17
  169. bbot/test/test_step_1/test_events.py +203 -37
  170. bbot/test/test_step_1/test_helpers.py +64 -28
  171. bbot/test/test_step_1/test_manager_deduplication.py +1 -1
  172. bbot/test/test_step_1/test_manager_scope_accuracy.py +336 -338
  173. bbot/test/test_step_1/test_modules_basic.py +69 -71
  174. bbot/test/test_step_1/test_presets.py +184 -96
  175. bbot/test/test_step_1/test_python_api.py +7 -2
  176. bbot/test/test_step_1/test_regexes.py +35 -5
  177. bbot/test/test_step_1/test_scan.py +39 -5
  178. bbot/test/test_step_1/test_scope.py +5 -4
  179. bbot/test/test_step_1/test_target.py +243 -145
  180. bbot/test/test_step_1/test_web.py +48 -10
  181. bbot/test/test_step_2/module_tests/base.py +17 -20
  182. bbot/test/test_step_2/module_tests/test_module_anubisdb.py +1 -1
  183. bbot/test/test_step_2/module_tests/test_module_apkpure.py +71 -0
  184. bbot/test/test_step_2/module_tests/test_module_asset_inventory.py +0 -1
  185. bbot/test/test_step_2/module_tests/test_module_azure_realm.py +1 -1
  186. bbot/test/test_step_2/module_tests/test_module_baddns.py +6 -6
  187. bbot/test/test_step_2/module_tests/test_module_baddns_direct.py +62 -0
  188. bbot/test/test_step_2/module_tests/test_module_bevigil.py +29 -2
  189. bbot/test/test_step_2/module_tests/test_module_binaryedge.py +4 -2
  190. bbot/test/test_step_2/module_tests/test_module_bucket_amazon.py +2 -2
  191. bbot/test/test_step_2/module_tests/test_module_bucket_azure.py +1 -1
  192. bbot/test/test_step_2/module_tests/test_module_bufferoverrun.py +35 -0
  193. bbot/test/test_step_2/module_tests/test_module_builtwith.py +2 -2
  194. bbot/test/test_step_2/module_tests/test_module_bypass403.py +1 -1
  195. bbot/test/test_step_2/module_tests/test_module_c99.py +126 -0
  196. bbot/test/test_step_2/module_tests/test_module_censys.py +4 -1
  197. bbot/test/test_step_2/module_tests/test_module_cloudcheck.py +4 -0
  198. bbot/test/test_step_2/module_tests/test_module_code_repository.py +11 -1
  199. bbot/test/test_step_2/module_tests/test_module_columbus.py +1 -1
  200. bbot/test/test_step_2/module_tests/test_module_credshed.py +3 -3
  201. bbot/test/test_step_2/module_tests/test_module_dastardly.py +2 -1
  202. bbot/test/test_step_2/module_tests/test_module_dehashed.py +2 -2
  203. bbot/test/test_step_2/module_tests/test_module_digitorus.py +1 -1
  204. bbot/test/test_step_2/module_tests/test_module_discord.py +1 -1
  205. bbot/test/test_step_2/module_tests/test_module_dnsbimi.py +103 -0
  206. bbot/test/test_step_2/module_tests/test_module_dnsbrute.py +9 -10
  207. bbot/test/test_step_2/module_tests/test_module_dnsbrute_mutations.py +1 -2
  208. bbot/test/test_step_2/module_tests/test_module_dnscommonsrv.py +1 -2
  209. bbot/test/test_step_2/module_tests/test_module_dnsdumpster.py +4 -4
  210. bbot/test/test_step_2/module_tests/test_module_dnstlsrpt.py +64 -0
  211. bbot/test/test_step_2/module_tests/test_module_dotnetnuke.py +0 -8
  212. bbot/test/test_step_2/module_tests/test_module_excavate.py +17 -37
  213. bbot/test/test_step_2/module_tests/test_module_extractous.py +54 -0
  214. bbot/test/test_step_2/module_tests/test_module_ffuf_shortnames.py +1 -1
  215. bbot/test/test_step_2/module_tests/test_module_filedownload.py +14 -14
  216. bbot/test/test_step_2/module_tests/test_module_git_clone.py +2 -2
  217. bbot/test/test_step_2/module_tests/test_module_github_org.py +19 -8
  218. bbot/test/test_step_2/module_tests/test_module_github_workflows.py +1 -1
  219. bbot/test/test_step_2/module_tests/test_module_gitlab.py +9 -4
  220. bbot/test/test_step_2/module_tests/test_module_google_playstore.py +83 -0
  221. bbot/test/test_step_2/module_tests/test_module_gowitness.py +4 -4
  222. bbot/test/test_step_2/module_tests/test_module_host_header.py +1 -1
  223. bbot/test/test_step_2/module_tests/test_module_http.py +4 -4
  224. bbot/test/test_step_2/module_tests/test_module_httpx.py +10 -8
  225. bbot/test/test_step_2/module_tests/test_module_hunterio.py +68 -4
  226. bbot/test/test_step_2/module_tests/test_module_jadx.py +55 -0
  227. bbot/test/test_step_2/module_tests/test_module_json.py +24 -11
  228. bbot/test/test_step_2/module_tests/test_module_leakix.py +7 -3
  229. bbot/test/test_step_2/module_tests/test_module_mysql.py +76 -0
  230. bbot/test/test_step_2/module_tests/test_module_myssl.py +1 -1
  231. bbot/test/test_step_2/module_tests/test_module_neo4j.py +1 -1
  232. bbot/test/test_step_2/module_tests/test_module_newsletters.py +6 -6
  233. bbot/test/test_step_2/module_tests/test_module_ntlm.py +7 -7
  234. bbot/test/test_step_2/module_tests/test_module_oauth.py +1 -1
  235. bbot/test/test_step_2/module_tests/test_module_otx.py +1 -1
  236. bbot/test/test_step_2/module_tests/test_module_paramminer_cookies.py +1 -2
  237. bbot/test/test_step_2/module_tests/test_module_paramminer_getparams.py +0 -6
  238. bbot/test/test_step_2/module_tests/test_module_paramminer_headers.py +2 -9
  239. bbot/test/test_step_2/module_tests/test_module_passivetotal.py +3 -1
  240. bbot/test/test_step_2/module_tests/test_module_portscan.py +9 -8
  241. bbot/test/test_step_2/module_tests/test_module_postgres.py +74 -0
  242. bbot/test/test_step_2/module_tests/test_module_postman.py +84 -253
  243. bbot/test/test_step_2/module_tests/test_module_postman_download.py +439 -0
  244. bbot/test/test_step_2/module_tests/test_module_rapiddns.py +93 -1
  245. bbot/test/test_step_2/module_tests/test_module_securitytxt.py +50 -0
  246. bbot/test/test_step_2/module_tests/test_module_shodan_dns.py +20 -1
  247. bbot/test/test_step_2/module_tests/test_module_sitedossier.py +2 -2
  248. bbot/test/test_step_2/module_tests/test_module_smuggler.py +1 -1
  249. bbot/test/test_step_2/module_tests/test_module_social.py +11 -1
  250. bbot/test/test_step_2/module_tests/test_module_speculate.py +2 -6
  251. bbot/test/test_step_2/module_tests/test_module_splunk.py +4 -4
  252. bbot/test/test_step_2/module_tests/test_module_sqlite.py +18 -0
  253. bbot/test/test_step_2/module_tests/test_module_sslcert.py +1 -1
  254. bbot/test/test_step_2/module_tests/test_module_stdout.py +5 -3
  255. bbot/test/test_step_2/module_tests/test_module_subdomaincenter.py +1 -1
  256. bbot/test/test_step_2/module_tests/test_module_subdomainradar.py +208 -0
  257. bbot/test/test_step_2/module_tests/test_module_subdomains.py +1 -1
  258. bbot/test/test_step_2/module_tests/test_module_teams.py +8 -6
  259. bbot/test/test_step_2/module_tests/test_module_telerik.py +1 -1
  260. bbot/test/test_step_2/module_tests/test_module_trufflehog.py +317 -11
  261. bbot/test/test_step_2/module_tests/test_module_wayback.py +1 -1
  262. bbot/test/test_step_2/template_tests/test_template_subdomain_enum.py +135 -0
  263. {bbot-2.0.1.4654rc0.dist-info → bbot-2.3.0.5397rc0.dist-info}/METADATA +48 -18
  264. bbot-2.3.0.5397rc0.dist-info/RECORD +421 -0
  265. {bbot-2.0.1.4654rc0.dist-info → bbot-2.3.0.5397rc0.dist-info}/WHEEL +1 -1
  266. bbot/modules/unstructured.py +0 -163
  267. bbot/test/test_step_2/module_tests/test_module_unstructured.py +0 -102
  268. bbot-2.0.1.4654rc0.dist-info/RECORD +0 -385
  269. {bbot-2.0.1.4654rc0.dist-info → bbot-2.3.0.5397rc0.dist-info}/LICENSE +0 -0
  270. {bbot-2.0.1.4654rc0.dist-info → bbot-2.3.0.5397rc0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,144 @@
1
+ # dnstlsrpt.py
2
+ #
3
+ # Checks for and parses common TLS-RPT TXT records, e.g. _smtp._tls.target.domain
4
+ #
5
+ # TLS-RPT policies may contain email addresses or URL's for reporting destinations, typically the email addresses are software processed inboxes, but they may also be to individual humans or team inboxes.
6
+ #
7
+ # The domain portion of any email address or URL is also passively checked and added as appropriate, for additional inspection by other modules.
8
+ #
9
+ # Example records,
10
+ # _smtp._tls.example.com TXT "v=TLSRPTv1;rua=https://tlsrpt.azurewebsites.net/report"
11
+ # _smtp._tls.example.net TXT "v=TLSRPTv1; rua=mailto:sts-reports@example.net;"
12
+ #
13
+ # TODO: extract %{UNIQUE_ID}% from hosted services as ORG_STUB ?
14
+ # e.g. %{UNIQUE_ID}%@tlsrpt.hosted.service.provider is usually a tenant specific ID.
15
+ # e.g. tlsrpt@%{UNIQUE_ID}%.hosted.service.provider is usually a tenant specific ID.
16
+
17
+ from bbot.modules.base import BaseModule
18
+ from bbot.core.helpers.dns.helpers import service_record
19
+
20
+ import re
21
+
22
+ from bbot.core.helpers.regexes import email_regex, url_regexes
23
+
24
+ _tlsrpt_regex = r"^v=(?P<v>TLSRPTv[0-9]+); *(?P<kvps>.*)$"
25
+ tlsrpt_regex = re.compile(_tlsrpt_regex, re.I)
26
+
27
+ _tlsrpt_kvp_regex = r"(?P<k>\w+)=(?P<v>[^;]+);*"
28
+ tlsrpt_kvp_regex = re.compile(_tlsrpt_kvp_regex)
29
+
30
+ _csul = r"(?P<uri>[^, ]+)"
31
+ csul = re.compile(_csul)
32
+
33
+
34
+ class dnstlsrpt(BaseModule):
35
+ watched_events = ["DNS_NAME"]
36
+ produced_events = ["EMAIL_ADDRESS", "URL_UNVERIFIED", "RAW_DNS_RECORD"]
37
+ flags = ["subdomain-enum", "cloud-enum", "email-enum", "passive", "safe"]
38
+ meta = {
39
+ "description": "Check for TLS-RPT records",
40
+ "author": "@colin-stubbs",
41
+ "created_date": "2024-07-26",
42
+ }
43
+ options = {
44
+ "emit_emails": True,
45
+ "emit_raw_dns_records": False,
46
+ "emit_urls": True,
47
+ "emit_vulnerabilities": True,
48
+ }
49
+ options_desc = {
50
+ "emit_emails": "Emit EMAIL_ADDRESS events",
51
+ "emit_raw_dns_records": "Emit RAW_DNS_RECORD events",
52
+ "emit_urls": "Emit URL_UNVERIFIED events",
53
+ "emit_vulnerabilities": "Emit VULNERABILITY events",
54
+ }
55
+
56
+ async def setup(self):
57
+ self.emit_emails = self.config.get("emit_emails", True)
58
+ self.emit_raw_dns_records = self.config.get("emit_raw_dns_records", False)
59
+ self.emit_urls = self.config.get("emit_urls", True)
60
+ self.emit_vulnerabilities = self.config.get("emit_vulnerabilities", True)
61
+ return await super().setup()
62
+
63
+ def _incoming_dedup_hash(self, event):
64
+ # dedupe by parent
65
+ parent_domain = self.helpers.parent_domain(event.data)
66
+ return hash(parent_domain), "already processed parent domain"
67
+
68
+ async def filter_event(self, event):
69
+ if "_wildcard" in str(event.host).split("."):
70
+ return False, "event is wildcard"
71
+
72
+ # there's no value in inspecting service records
73
+ if service_record(event.host) == True:
74
+ return False, "service record detected"
75
+
76
+ return True
77
+
78
+ async def handle_event(self, event):
79
+ rdtype = "TXT"
80
+ tags = ["tlsrpt-record"]
81
+ hostname = f"_smtp._tls.{event.host}"
82
+
83
+ r = await self.helpers.resolve_raw(hostname, type=rdtype)
84
+
85
+ if r:
86
+ raw_results, errors = r
87
+ for answer in raw_results:
88
+ if self.emit_raw_dns_records:
89
+ await self.emit_event(
90
+ {"host": hostname, "type": rdtype, "answer": answer.to_text()},
91
+ "RAW_DNS_RECORD",
92
+ parent=event,
93
+ tags=tags.append(f"{rdtype.lower()}-record"),
94
+ context=f"{rdtype} lookup on {hostname} produced {{event.type}}",
95
+ )
96
+
97
+ # we need to fix TXT data that may have been split across two different rdata's
98
+ # e.g. we will get a single string, but within that string we may have two parts such as:
99
+ # answer = '"part 1 that was really long" "part 2 that did not fit in part 1"'
100
+ # NOTE: the leading and trailing double quotes are essential as part of a raw DNS TXT record, or another record type that contains a free form text string as a component.
101
+ s = answer.to_text().strip('"').replace('" "', "")
102
+
103
+ # validate TLSRPT record, tag appropriately
104
+ tlsrpt_match = tlsrpt_regex.search(s)
105
+
106
+ if (
107
+ tlsrpt_match
108
+ and tlsrpt_match.group("v")
109
+ and tlsrpt_match.group("kvps")
110
+ and tlsrpt_match.group("kvps") != ""
111
+ ):
112
+ for kvp_match in tlsrpt_kvp_regex.finditer(tlsrpt_match.group("kvps")):
113
+ key = kvp_match.group("k").lower()
114
+
115
+ if key == "rua":
116
+ for csul_match in csul.finditer(kvp_match.group("v")):
117
+ if csul_match.group("uri"):
118
+ for match in email_regex.finditer(csul_match.group("uri")):
119
+ start, end = match.span()
120
+ email = csul_match.group("uri")[start:end]
121
+
122
+ if self.emit_emails:
123
+ await self.emit_event(
124
+ email,
125
+ "EMAIL_ADDRESS",
126
+ tags=tags.append(f"tlsrpt-record-{key}"),
127
+ parent=event,
128
+ )
129
+
130
+ for url_regex in url_regexes:
131
+ for match in url_regex.finditer(csul_match.group("uri")):
132
+ start, end = match.span()
133
+ url = csul_match.group("uri")[start:end]
134
+
135
+ if self.emit_urls:
136
+ await self.emit_event(
137
+ url,
138
+ "URL_UNVERIFIED",
139
+ tags=tags.append(f"tlsrpt-record-{key}"),
140
+ parent=event,
141
+ )
142
+
143
+
144
+ # EOF
@@ -86,12 +86,12 @@ class docker_pull(BaseModule):
86
86
  service = www_authenticate_headers.split('service="')[1].split('"')[0]
87
87
  scope = www_authenticate_headers.split('scope="')[1].split('"')[0]
88
88
  except (KeyError, IndexError):
89
- self.log.error(f"Could not obtain realm, service or scope from {url}")
89
+ self.log.warning(f"Could not obtain realm, service or scope from {url}")
90
90
  break
91
91
  auth_url = f"{realm}?service={service}&scope={scope}"
92
92
  auth_response = await self.helpers.request(auth_url)
93
93
  if not auth_response:
94
- self.log.error(f"Could not obtain token from {auth_url}")
94
+ self.log.warning(f"Could not obtain token from {auth_url}")
95
95
  break
96
96
  auth_json = auth_response.json()
97
97
  token = auth_json["token"]
@@ -102,7 +102,8 @@ class docker_pull(BaseModule):
102
102
  url = f"{registry}/v2/{repository}/tags/list"
103
103
  r = await self.docker_api_request(url)
104
104
  if r is None or r.status_code != 200:
105
- self.log.warning(f"Could not retrieve all tags for {repository} asuming tag:latest only.")
105
+ self.log.warning(f"Could not retrieve all tags for {repository} assuming tag:latest only.")
106
+ self.log.debug(f"Response: {r}")
106
107
  return ["latest"]
107
108
  try:
108
109
  tags = r.json().get("tags", ["latest"])
@@ -115,14 +116,15 @@ class docker_pull(BaseModule):
115
116
  else:
116
117
  return [tags[-1]]
117
118
  except (KeyError, IndexError):
118
- self.log.error(f"Could not retrieve tags for {repository}.")
119
+ self.log.warning(f"Could not retrieve tags for {repository}.")
119
120
  return ["latest"]
120
121
 
121
122
  async def get_manifest(self, registry, repository, tag):
122
123
  url = f"{registry}/v2/{repository}/manifests/{tag}"
123
124
  r = await self.docker_api_request(url)
124
125
  if r is None or r.status_code != 200:
125
- self.log.error(f"Could not retrieve manifest for {repository}:{tag}.")
126
+ self.log.warning(f"Could not retrieve manifest for {repository}:{tag}.")
127
+ self.log.debug(f"Response: {r}")
126
128
  return {}
127
129
  response_json = r.json()
128
130
  if response_json.get("manifests", []):
bbot/modules/dockerhub.py CHANGED
@@ -31,7 +31,7 @@ class dockerhub(BaseModule):
31
31
  async def handle_org_stub(self, event):
32
32
  profile_name = event.data
33
33
  # docker usernames are case sensitive, so if there are capitalizations we also try a lowercase variation
34
- profiles_to_check = set([profile_name, profile_name.lower()])
34
+ profiles_to_check = {profile_name, profile_name.lower()}
35
35
  for p in profiles_to_check:
36
36
  api_url = f"{self.api_url}/users/{p}"
37
37
  api_result = await self.helpers.request(api_url, follow_redirects=True)
@@ -64,7 +64,7 @@ class dockerhub(BaseModule):
64
64
  async def get_repos(self, username):
65
65
  repos = []
66
66
  url = f"{self.api_url}/repositories/{username}?page_size=25&page=" + "{page}"
67
- agen = self.helpers.api_page_iter(url, json=False)
67
+ agen = self.api_page_iter(url, json=False)
68
68
  try:
69
69
  async for r in agen:
70
70
  if r is None:
@@ -32,7 +32,6 @@ class dotnetnuke(BaseModule):
32
32
  self.interactsh_instance = None
33
33
 
34
34
  if self.scan.config.get("interactsh_disable", False) == False:
35
-
36
35
  try:
37
36
  self.interactsh_instance = self.helpers.interactsh()
38
37
  self.interactsh_domain = await self.interactsh_instance.register(callback=self.interactsh_callback)
@@ -114,7 +113,6 @@ class dotnetnuke(BaseModule):
114
113
  )
115
114
 
116
115
  if "endpoint" not in event.tags:
117
-
118
116
  # NewsArticlesSlider ImageHandler.ashx File Read
119
117
  result = await self.helpers.request(
120
118
  f'{event.data["url"]}/DesktopModules/dnnUI_NewsArticlesSlider/ImageHandler.ashx?img=~/web.config'
@@ -155,24 +153,25 @@ class dotnetnuke(BaseModule):
155
153
 
156
154
  # InstallWizard SuperUser Privilege Escalation
157
155
  result = await self.helpers.request(f'{event.data["url"]}/Install/InstallWizard.aspx')
158
- if result.status_code == 200:
159
- result_confirm = await self.helpers.request(
160
- f'{event.data["url"]}/Install/InstallWizard.aspx?__viewstate=1'
161
- )
162
- if result_confirm.status_code == 500:
163
- description = "DotNetNuke InstallWizard SuperUser Privilege Escalation"
164
- await self.emit_event(
165
- {
166
- "severity": "CRITICAL",
167
- "description": description,
168
- "host": str(event.host),
169
- "url": f'{event.data["url"]}/Install/InstallWizard.aspx',
170
- },
171
- "VULNERABILITY",
172
- event,
173
- context=f'{{module}} scanned {event.data["url"]} and found critical {{event.type}}: {description}',
156
+ if result:
157
+ if result.status_code == 200:
158
+ result_confirm = await self.helpers.request(
159
+ f'{event.data["url"]}/Install/InstallWizard.aspx?__viewstate=1'
174
160
  )
175
- return
161
+ if result_confirm.status_code == 500:
162
+ description = "DotNetNuke InstallWizard SuperUser Privilege Escalation"
163
+ await self.emit_event(
164
+ {
165
+ "severity": "CRITICAL",
166
+ "description": description,
167
+ "host": str(event.host),
168
+ "url": f'{event.data["url"]}/Install/InstallWizard.aspx',
169
+ },
170
+ "VULNERABILITY",
171
+ event,
172
+ context=f'{{module}} scanned {event.data["url"]} and found critical {{event.type}}: {description}',
173
+ )
174
+ return
176
175
 
177
176
  # DNNImageHandler.ashx Blind SSRF
178
177
  self.event_dict[event.data["url"]] = event
@@ -18,7 +18,7 @@ class emailformat(BaseModule):
18
18
  async def handle_event(self, event):
19
19
  _, query = self.helpers.split_domain(event.data)
20
20
  url = f"{self.base_url}/d/{self.helpers.quote(query)}/"
21
- r = await self.request_with_fail_count(url)
21
+ r = await self.api_request(url)
22
22
  if not r:
23
23
  return
24
24
  for email in await self.helpers.re.extract_emails(r.text):
@@ -0,0 +1,122 @@
1
+ from extractous import Extractor
2
+
3
+ from bbot.modules.base import BaseModule
4
+
5
+
6
+ class extractous(BaseModule):
7
+ watched_events = ["FILESYSTEM"]
8
+ produced_events = ["RAW_TEXT"]
9
+ flags = ["passive", "safe"]
10
+ meta = {
11
+ "description": "Module to extract data from files",
12
+ "created_date": "2024-06-03",
13
+ "author": "@domwhewell-sage",
14
+ }
15
+ options = {
16
+ "extensions": [
17
+ "bak", # Backup File
18
+ "bash", # Bash Script or Configuration
19
+ "bashrc", # Bash Script or Configuration
20
+ "conf", # Configuration File
21
+ "cfg", # Configuration File
22
+ "crt", # Certificate File
23
+ "csv", # Comma Separated Values File
24
+ "db", # SQLite Database File
25
+ "sqlite", # SQLite Database File
26
+ "doc", # Microsoft Word Document (Old Format)
27
+ "docx", # Microsoft Word Document
28
+ "ica", # Citrix Independent Computing Architecture File
29
+ "indd", # Adobe InDesign Document
30
+ "ini", # Initialization File
31
+ "key", # Private Key File
32
+ "pub", # Public Key File
33
+ "log", # Log File
34
+ "markdown", # Markdown File
35
+ "md", # Markdown File
36
+ "odg", # OpenDocument Graphics (LibreOffice, OpenOffice)
37
+ "odp", # OpenDocument Presentation (LibreOffice, OpenOffice)
38
+ "ods", # OpenDocument Spreadsheet (LibreOffice, OpenOffice)
39
+ "odt", # OpenDocument Text (LibreOffice, OpenOffice)
40
+ "pdf", # Adobe Portable Document Format
41
+ "pem", # Privacy Enhanced Mail (SSL certificate)
42
+ "pps", # Microsoft PowerPoint Slideshow (Old Format)
43
+ "ppsx", # Microsoft PowerPoint Slideshow
44
+ "ppt", # Microsoft PowerPoint Presentation (Old Format)
45
+ "pptx", # Microsoft PowerPoint Presentation
46
+ "ps1", # PowerShell Script
47
+ "rdp", # Remote Desktop Protocol File
48
+ "sh", # Shell Script
49
+ "sql", # SQL Database Dump
50
+ "swp", # Swap File (temporary file, often Vim)
51
+ "sxw", # OpenOffice.org Writer document
52
+ "txt", # Plain Text Document
53
+ "vbs", # Visual Basic Script
54
+ "wpd", # WordPerfect Document
55
+ "xls", # Microsoft Excel Spreadsheet (Old Format)
56
+ "xlsx", # Microsoft Excel Spreadsheet
57
+ "xml", # eXtensible Markup Language File
58
+ "yml", # YAML Ain't Markup Language
59
+ "yaml", # YAML Ain't Markup Language
60
+ ],
61
+ }
62
+ options_desc = {
63
+ "extensions": "File extensions to parse",
64
+ }
65
+
66
+ deps_pip = ["extractous"]
67
+ scope_distance_modifier = 1
68
+
69
+ async def setup(self):
70
+ self.extensions = list({e.lower().strip(".") for e in self.config.get("extensions", [])})
71
+ return True
72
+
73
+ async def filter_event(self, event):
74
+ if "file" in event.tags:
75
+ if not any(event.data["path"].endswith(f".{ext}") for ext in self.extensions):
76
+ return False, "File extension not in the allowed list"
77
+ else:
78
+ return False, "Event is not a file"
79
+ return True
80
+
81
+ async def handle_event(self, event):
82
+ file_path = event.data["path"]
83
+ content = await self.scan.helpers.run_in_executor_mp(extract_text, file_path)
84
+ if isinstance(content, tuple):
85
+ error, traceback = content
86
+ self.error(f"Error extracting text from {file_path}: {error}")
87
+ self.trace(traceback)
88
+ return
89
+
90
+ if content:
91
+ raw_text_event = self.make_event(
92
+ content,
93
+ "RAW_TEXT",
94
+ context=f"Extracted text from {file_path}",
95
+ parent=event,
96
+ )
97
+ await self.emit_event(raw_text_event)
98
+
99
+
100
+ def extract_text(file_path):
101
+ """
102
+ extract_text Extracts plaintext from a document path using extractous.
103
+
104
+ :param file_path: The path of the file to extract text from.
105
+ :return: ASCII-encoded plaintext extracted from the document.
106
+ """
107
+
108
+ try:
109
+ extractor = Extractor()
110
+ reader, metadata = extractor.extract_file(str(file_path))
111
+
112
+ result = ""
113
+ buffer = reader.read(4096)
114
+ while len(buffer) > 0:
115
+ result += buffer.decode("utf-8")
116
+ buffer = reader.read(4096)
117
+
118
+ return result.strip()
119
+ except Exception as e:
120
+ import traceback
121
+
122
+ return (str(e), traceback.format_exc())
@@ -25,12 +25,12 @@ class filedownload(BaseModule):
25
25
  "bak", # Backup File
26
26
  "bash", # Bash Script or Configuration
27
27
  "bashrc", # Bash Script or Configuration
28
- "conf", # Configuration File
29
28
  "cfg", # Configuration File
29
+ "conf", # Configuration File
30
30
  "crt", # Certificate File
31
31
  "csv", # Comma Separated Values File
32
32
  "db", # SQLite Database File
33
- "sqlite", # SQLite Database File
33
+ "dll", # Windows Dynamic Link Library
34
34
  "doc", # Microsoft Word Document (Old Format)
35
35
  "docx", # Microsoft Word Document
36
36
  "exe", # Windows PE executable
@@ -39,7 +39,6 @@ class filedownload(BaseModule):
39
39
  "ini", # Initialization File
40
40
  "jar", # Java Archive
41
41
  "key", # Private Key File
42
- "pub", # Public Key File
43
42
  "log", # Log File
44
43
  "markdown", # Markdown File
45
44
  "md", # Markdown File
@@ -55,23 +54,26 @@ class filedownload(BaseModule):
55
54
  "ppt", # Microsoft PowerPoint Presentation (Old Format)
56
55
  "pptx", # Microsoft PowerPoint Presentation
57
56
  "ps1", # PowerShell Script
57
+ "pub", # Public Key File
58
58
  "raw", # Raw Image File Format
59
59
  "rdp", # Remote Desktop Protocol File
60
60
  "sh", # Shell Script
61
61
  "sql", # SQL Database Dump
62
+ "sqlite", # SQLite Database File
62
63
  "swp", # Swap File (temporary file, often Vim)
63
64
  "sxw", # OpenOffice.org Writer document
64
- "tar", # Tar Archive
65
65
  "tar.gz", # Gzip-Compressed Tar Archive
66
- "zip", # Zip Archive
66
+ "tar", # Tar Archive
67
67
  "txt", # Plain Text Document
68
68
  "vbs", # Visual Basic Script
69
+ "war", # Java Web Archive
69
70
  "wpd", # WordPerfect Document
70
71
  "xls", # Microsoft Excel Spreadsheet (Old Format)
71
72
  "xlsx", # Microsoft Excel Spreadsheet
72
73
  "xml", # eXtensible Markup Language File
73
- "yml", # YAML Ain't Markup Language
74
74
  "yaml", # YAML Ain't Markup Language
75
+ "yml", # YAML Ain't Markup Language
76
+ "zip", # Zip Archive
75
77
  ],
76
78
  "max_filesize": "10MB",
77
79
  "base_64_encoded_file": "false",
@@ -85,7 +87,7 @@ class filedownload(BaseModule):
85
87
  scope_distance_modifier = 3
86
88
 
87
89
  async def setup(self):
88
- self.extensions = list(set([e.lower().strip(".") for e in self.config.get("extensions", [])]))
90
+ self.extensions = list({e.lower().strip(".") for e in self.config.get("extensions", [])})
89
91
  self.max_filesize = self.config.get("max_filesize", "10MB")
90
92
  self.download_dir = self.scan.home / "filedownload"
91
93
  self.helpers.mkdir(self.download_dir)
bbot/modules/fullhunt.py CHANGED
@@ -18,19 +18,22 @@ class fullhunt(subdomain_enum_apikey):
18
18
 
19
19
  async def setup(self):
20
20
  self.api_key = self.config.get("api_key", "")
21
- self.headers = {"x-api-key": self.api_key}
22
21
  return await super().setup()
23
22
 
24
23
  async def ping(self):
25
24
  url = f"{self.base_url}/auth/status"
26
- j = (await self.request_with_fail_count(url, headers=self.headers)).json()
25
+ j = (await self.api_request(url)).json()
27
26
  remaining = j["user_credits"]["remaining_credits"]
28
27
  assert remaining > 0, "No credits remaining"
29
28
 
29
+ def prepare_api_request(self, url, kwargs):
30
+ kwargs["headers"]["x-api-key"] = self.api_key
31
+ return url, kwargs
32
+
30
33
  async def request_url(self, query):
31
34
  url = f"{self.base_url}/domain/{self.helpers.quote(query)}/subdomains"
32
- response = await self.request_with_fail_count(url, headers=self.headers)
35
+ response = await self.api_request(url)
33
36
  return response
34
37
 
35
- def parse_results(self, r, query):
38
+ async def parse_results(self, r, query):
36
39
  return r.json().get("hosts", [])
@@ -137,10 +137,10 @@ class Generic_XXE(BaseSubmodule):
137
137
  post_body = f"""<?xml version="1.0" encoding="ISO-8859-1"?>
138
138
  <!DOCTYPE foo [
139
139
  <!ELEMENT foo ANY >
140
- <!ENTITY % {rand_entity} SYSTEM "http://{subdomain_tag}.{self.parent_module.interactsh_domain}" >
140
+ <!ENTITY {rand_entity} SYSTEM "http://{subdomain_tag}.{self.parent_module.interactsh_domain}" >
141
141
  ]>
142
142
  <foo>&{rand_entity};</foo>"""
143
- test_url = f"{event.parsed_url.scheme}://{event.parsed_url.netloc}/"
143
+ test_url = event.parsed_url.geturl()
144
144
  r = await self.parent_module.helpers.curl(
145
145
  url=test_url, method="POST", raw_body=post_body, headers={"Content-type": "application/xml"}
146
146
  )
@@ -163,7 +163,7 @@ class generic_ssrf(BaseModule):
163
163
  self.severity = None
164
164
  self.generic_only = self.config.get("generic_only", False)
165
165
 
166
- if self.scan.config.get("interactsh_disable", False) == False:
166
+ if self.scan.config.get("interactsh_disable", False) is False:
167
167
  try:
168
168
  self.interactsh_instance = self.helpers.interactsh()
169
169
  self.interactsh_domain = await self.interactsh_instance.register(callback=self.interactsh_callback)
@@ -216,7 +216,7 @@ class generic_ssrf(BaseModule):
216
216
  self.debug("skipping result because subdomain tag was missing")
217
217
 
218
218
  async def cleanup(self):
219
- if self.scan.config.get("interactsh_disable", False) == False:
219
+ if self.scan.config.get("interactsh_disable", False) is False:
220
220
  try:
221
221
  await self.interactsh_instance.deregister()
222
222
  self.debug(
@@ -226,7 +226,7 @@ class generic_ssrf(BaseModule):
226
226
  self.warning(f"Interactsh failure: {e}")
227
227
 
228
228
  async def finish(self):
229
- if self.scan.config.get("interactsh_disable", False) == False:
229
+ if self.scan.config.get("interactsh_disable", False) is False:
230
230
  await self.helpers.sleep(5)
231
231
  try:
232
232
  for r in await self.interactsh_instance.poll():
@@ -42,7 +42,7 @@ class github_codesearch(github, subdomain_enum):
42
42
  async def query(self, query):
43
43
  repos = {}
44
44
  url = f"{self.base_url}/search/code?per_page=100&type=Code&q={self.helpers.quote(query)}&page=" + "{page}"
45
- agen = self.helpers.api_page_iter(url, headers=self.headers, json=False)
45
+ agen = self.api_page_iter(url, headers=self.headers, json=False)
46
46
  num_results = 0
47
47
  try:
48
48
  async for r in agen:
@@ -50,9 +50,10 @@ class github_codesearch(github, subdomain_enum):
50
50
  break
51
51
  status_code = getattr(r, "status_code", 0)
52
52
  if status_code == 429:
53
- "Github is rate-limiting us (HTTP status: 429)"
53
+ self.info("Github is rate-limiting us (HTTP status: 429)")
54
54
  break
55
55
  if status_code != 200:
56
+ self.info(f"Unexpected response (HTTP status: {status_code})")
56
57
  break
57
58
  try:
58
59
  j = r.json()
@@ -104,7 +104,7 @@ class github_org(github):
104
104
  async def query_org_repos(self, query):
105
105
  repos = []
106
106
  url = f"{self.base_url}/orgs/{self.helpers.quote(query)}/repos?per_page=100&page=" + "{page}"
107
- agen = self.helpers.api_page_iter(url, headers=self.headers, json=False)
107
+ agen = self.api_page_iter(url, json=False)
108
108
  try:
109
109
  async for r in agen:
110
110
  if r is None:
@@ -132,7 +132,7 @@ class github_org(github):
132
132
  async def query_org_members(self, query):
133
133
  members = []
134
134
  url = f"{self.base_url}/orgs/{self.helpers.quote(query)}/members?per_page=100&page=" + "{page}"
135
- agen = self.helpers.api_page_iter(url, headers=self.headers, json=False)
135
+ agen = self.api_page_iter(url, json=False)
136
136
  try:
137
137
  async for r in agen:
138
138
  if r is None:
@@ -160,7 +160,7 @@ class github_org(github):
160
160
  async def query_user_repos(self, query):
161
161
  repos = []
162
162
  url = f"{self.base_url}/users/{self.helpers.quote(query)}/repos?per_page=100&page=" + "{page}"
163
- agen = self.helpers.api_page_iter(url, headers=self.headers, json=False)
163
+ agen = self.api_page_iter(url, json=False)
164
164
  try:
165
165
  async for r in agen:
166
166
  if r is None:
@@ -189,7 +189,7 @@ class github_org(github):
189
189
  is_org = False
190
190
  in_scope = False
191
191
  url = f"{self.base_url}/orgs/{org}"
192
- r = await self.helpers.request(url, headers=self.headers)
192
+ r = await self.api_request(url)
193
193
  if r is None:
194
194
  return is_org, in_scope
195
195
  status_code = getattr(r, "status_code", 0)
@@ -88,7 +88,7 @@ class github_workflows(github):
88
88
  async def get_workflows(self, owner, repo):
89
89
  workflows = []
90
90
  url = f"{self.base_url}/repos/{owner}/{repo}/actions/workflows?per_page=100&page=" + "{page}"
91
- agen = self.helpers.api_page_iter(url, headers=self.headers, json=False)
91
+ agen = self.api_page_iter(url, json=False)
92
92
  try:
93
93
  async for r in agen:
94
94
  if r is None:
@@ -115,7 +115,7 @@ class github_workflows(github):
115
115
  async def get_workflow_runs(self, owner, repo, workflow_id):
116
116
  runs = []
117
117
  url = f"{self.base_url}/repos/{owner}/{repo}/actions/workflows/{workflow_id}/runs?status=success&per_page={self.num_logs}"
118
- r = await self.helpers.request(url, headers=self.headers)
118
+ r = await self.api_request(url)
119
119
  if r is None:
120
120
  return runs
121
121
  status_code = getattr(r, "status_code", 0)
@@ -166,7 +166,7 @@ class github_workflows(github):
166
166
  main_logs = []
167
167
  with zipfile.ZipFile(file_destination, "r") as logzip:
168
168
  for name in logzip.namelist():
169
- if fnmatch.fnmatch(name, "*.txt") and not "/" in name:
169
+ if fnmatch.fnmatch(name, "*.txt") and "/" not in name:
170
170
  logzip.extract(name, folder)
171
171
  main_logs.append(folder / name)
172
172
  return main_logs
@@ -176,7 +176,7 @@ class github_workflows(github):
176
176
  async def get_run_artifacts(self, owner, repo, run_id):
177
177
  artifacts = []
178
178
  url = f"{self.base_url}/repos/{owner}/{repo}/actions/runs/{run_id}/artifacts"
179
- r = await self.helpers.request(url, headers=self.headers)
179
+ r = await self.api_request(url)
180
180
  if r is None:
181
181
  return artifacts
182
182
  status_code = getattr(r, "status_code", 0)
bbot/modules/gitlab.py CHANGED
@@ -16,10 +16,7 @@ class gitlab(BaseModule):
16
16
  scope_distance_modifier = 2
17
17
 
18
18
  async def setup(self):
19
- self.headers = {}
20
- self.api_key = self.config.get("api_key", "")
21
- if self.api_key:
22
- self.headers.update({"Authorization": f"Bearer {self.api_key}"})
19
+ await self.require_api_key()
23
20
  return True
24
21
 
25
22
  async def filter_event(self, event):
@@ -111,7 +108,7 @@ class gitlab(BaseModule):
111
108
  await self.handle_namespace(group, event)
112
109
 
113
110
  async def gitlab_json_request(self, url):
114
- response = await self.helpers.request(url, headers=self.headers)
111
+ response = await self.api_request(url)
115
112
  if response is not None:
116
113
  try:
117
114
  json = response.json()