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
bbot/modules/jadx.py ADDED
@@ -0,0 +1,87 @@
1
+ from pathlib import Path
2
+ from subprocess import CalledProcessError
3
+ from bbot.modules.internal.base import BaseModule
4
+
5
+
6
+ class jadx(BaseModule):
7
+ watched_events = ["FILESYSTEM"]
8
+ produced_events = ["FILESYSTEM"]
9
+ flags = ["passive", "safe"]
10
+ meta = {
11
+ "description": "Decompile APKs and XAPKs using JADX",
12
+ "created_date": "2024-11-04",
13
+ "author": "@domwhewell-sage",
14
+ }
15
+ options = {
16
+ "threads": 4,
17
+ }
18
+ options_desc = {
19
+ "threads": "Maximum jadx threads for extracting apk's, default: 4",
20
+ }
21
+ deps_common = ["java"]
22
+ deps_ansible = [
23
+ {
24
+ "name": "Create jadx directory",
25
+ "file": {"path": "#{BBOT_TOOLS}/jadx", "state": "directory", "mode": "0755"},
26
+ },
27
+ {
28
+ "name": "Download jadx",
29
+ "unarchive": {
30
+ "src": "https://github.com/skylot/jadx/releases/download/v1.5.0/jadx-1.5.0.zip",
31
+ "include": ["lib/jadx-1.5.0-all.jar", "bin/jadx"],
32
+ "dest": "#{BBOT_TOOLS}/jadx",
33
+ "remote_src": True,
34
+ },
35
+ },
36
+ ]
37
+
38
+ allowed_file_types = ["java archive", "android application package"]
39
+
40
+ async def setup(self):
41
+ self.threads = self.config.get("threads", 4)
42
+ return True
43
+
44
+ async def filter_event(self, event):
45
+ if "file" in event.tags:
46
+ if event.data["magic_description"].lower() not in self.allowed_file_types:
47
+ return False, f"Jadx is not able to decompile this file type: {event.data['magic_description']}"
48
+ else:
49
+ return False, "Event is not a file"
50
+ return True
51
+
52
+ async def handle_event(self, event):
53
+ path = Path(event.data["path"])
54
+ output_dir = path.parent / path.name.replace(".", "_")
55
+ self.helpers.mkdir(output_dir)
56
+ success = await self.decompile_apk(path, output_dir)
57
+
58
+ # If jadx was able to decompile the java archive, emit an event
59
+ if success:
60
+ await self.emit_event(
61
+ {"path": str(output_dir)},
62
+ "FILESYSTEM",
63
+ tags="folder",
64
+ parent=event,
65
+ context=f'extracted "{path}" to: {output_dir}',
66
+ )
67
+ else:
68
+ output_dir.rmdir()
69
+
70
+ async def decompile_apk(self, path, output_dir):
71
+ command = [
72
+ f"{self.scan.helpers.tools_dir}/jadx/bin/jadx",
73
+ "--threads-count",
74
+ self.threads,
75
+ "--output-dir",
76
+ str(output_dir),
77
+ str(path),
78
+ ]
79
+ try:
80
+ output = await self.run_process(command, check=True)
81
+ except CalledProcessError as e:
82
+ self.warning(f"Error decompiling {path}. STDOUT: {e.stdout} STDERR: {repr(e.stderr)}")
83
+ return False
84
+ if not (output_dir / "resources").exists() and not (output_dir / "sources").exists():
85
+ self.warning(f"JADX was unable to decompile {path}: (STDOUT: {output.stdout} STDERR: {output.stderr})")
86
+ return False
87
+ return True
bbot/modules/leakix.py CHANGED
@@ -15,31 +15,32 @@ class leakix(subdomain_enum_apikey):
15
15
  }
16
16
 
17
17
  base_url = "https://leakix.net"
18
+ ping_url = f"{base_url}/host/1.1.1.1"
18
19
 
19
20
  async def setup(self):
20
21
  ret = await super(subdomain_enum_apikey, self).setup()
21
- self.headers = {"Accept": "application/json"}
22
22
  self.api_key = self.config.get("api_key", "")
23
23
  if self.api_key:
24
- self.headers["api-key"] = self.api_key
25
24
  return await self.require_api_key()
26
25
  return ret
27
26
 
28
- async def ping(self):
29
- url = f"{self.base_url}/host/1.2.3.4.5"
30
- r = await self.helpers.request(url, headers=self.headers)
31
- resp_content = getattr(r, "text", "")
32
- assert getattr(r, "status_code", 0) != 401, resp_content
27
+ def prepare_api_request(self, url, kwargs):
28
+ if self.api_key:
29
+ kwargs["headers"]["api-key"] = self.api_key
30
+ kwargs["headers"]["Accept"] = "application/json"
31
+ return url, kwargs
33
32
 
34
33
  async def request_url(self, query):
35
34
  url = f"{self.base_url}/api/subdomains/{self.helpers.quote(query)}"
36
- response = await self.request_with_fail_count(url, headers=self.headers)
35
+ response = await self.api_request(url)
37
36
  return response
38
37
 
39
- def parse_results(self, r, query=None):
38
+ async def parse_results(self, r, query=None):
39
+ results = set()
40
40
  json = r.json()
41
41
  if json:
42
42
  for entry in json:
43
43
  subdomain = entry.get("subdomain", "")
44
44
  if subdomain:
45
- yield subdomain
45
+ results.add(subdomain)
46
+ return results
bbot/modules/myssl.py CHANGED
@@ -15,9 +15,9 @@ class myssl(subdomain_enum):
15
15
 
16
16
  async def request_url(self, query):
17
17
  url = f"{self.base_url}?domain={self.helpers.quote(query)}"
18
- return await self.request_with_fail_count(url)
18
+ return await self.api_request(url)
19
19
 
20
- def parse_results(self, r, query):
20
+ async def parse_results(self, r, query):
21
21
  results = set()
22
22
  json = r.json()
23
23
  if json and isinstance(json, dict):
@@ -46,11 +46,11 @@ class newsletters(BaseModule):
46
46
  body = _event.data["body"]
47
47
  soup = self.helpers.beautifulsoup(body, "html.parser")
48
48
  if soup is False:
49
- self.debug(f"BeautifulSoup returned False")
49
+ self.debug("BeautifulSoup returned False")
50
50
  return
51
51
  result = self.find_type(soup)
52
52
  if result:
53
- description = f"Found a Newsletter Submission Form that could be used for email bombing attacks"
53
+ description = "Found a Newsletter Submission Form that could be used for email bombing attacks"
54
54
  data = {"host": str(_event.host), "description": description, "url": _event.data["url"]}
55
55
  await self.emit_event(
56
56
  data,
bbot/modules/otx.py CHANGED
@@ -15,12 +15,14 @@ class otx(subdomain_enum):
15
15
 
16
16
  def request_url(self, query):
17
17
  url = f"{self.base_url}/api/v1/indicators/domain/{self.helpers.quote(query)}/passive_dns"
18
- return self.request_with_fail_count(url)
18
+ return self.api_request(url)
19
19
 
20
- def parse_results(self, r, query):
20
+ async def parse_results(self, r, query):
21
+ results = set()
21
22
  j = r.json()
22
23
  if isinstance(j, dict):
23
24
  for entry in j.get("passive_dns", []):
24
25
  subdomain = entry.get("hostname", "")
25
26
  if subdomain:
26
- yield subdomain
27
+ results.add(subdomain)
28
+ return results
@@ -91,15 +91,15 @@ class asset_inventory(CSV):
91
91
  self.assets[hostkey].absorb_event(event)
92
92
 
93
93
  async def report(self):
94
- stats = dict()
95
- totals = dict()
94
+ stats = {}
95
+ totals = {}
96
96
 
97
97
  def increment_stat(stat, value):
98
98
  try:
99
99
  totals[stat] += 1
100
100
  except KeyError:
101
101
  totals[stat] = 1
102
- if not stat in stats:
102
+ if stat not in stats:
103
103
  stats[stat] = {}
104
104
  try:
105
105
  stats[stat][value] += 1
@@ -259,17 +259,17 @@ class Asset:
259
259
  # ips
260
260
  self.ip_addresses = set(_make_ip_list(row.get("IP (External)", "")))
261
261
  self.ip_addresses.update(set(_make_ip_list(row.get("IP (Internal)", ""))))
262
- # If user reqests a recheck dont import the following fields to force them to be rechecked
262
+ # If user requests a recheck dont import the following fields to force them to be rechecked
263
263
  if not self.recheck:
264
264
  # ports
265
265
  ports = [i.strip() for i in row.get("Open Ports", "").split(",")]
266
- self.ports.update(set(i for i in ports if i and is_port(i)))
266
+ self.ports.update({i for i in ports if i and is_port(i)})
267
267
  # findings
268
268
  findings = [i.strip() for i in row.get("Findings", "").splitlines()]
269
- self.findings.update(set(i for i in findings if i))
269
+ self.findings.update({i for i in findings if i})
270
270
  # technologies
271
271
  technologies = [i.strip() for i in row.get("Technologies", "").splitlines()]
272
- self.technologies.update(set(i for i in technologies if i))
272
+ self.technologies.update({i for i in technologies if i})
273
273
  # risk rating
274
274
  risk_rating = row.get("Risk Rating", "").strip()
275
275
  if risk_rating and risk_rating.isdigit() and int(risk_rating) > self.risk_rating:
@@ -24,7 +24,7 @@ class BaseOutputModule(BaseModule):
24
24
  if event.type in ("FINISHED",):
25
25
  return True, "its type is FINISHED"
26
26
  if self.errored:
27
- return False, f"module is in error state"
27
+ return False, "module is in error state"
28
28
  # exclude non-watched types
29
29
  if not any(t in self.get_watched_events() for t in ("*", event.type)):
30
30
  return False, "its type is not in watched_events"
@@ -55,7 +55,6 @@ class CSV(BaseOutputModule):
55
55
  async def handle_event(self, event):
56
56
  # ["Event type", "Event data", "IP Address", "Source Module", "Scope Distance", "Event Tags"]
57
57
  discovery_path = getattr(event, "discovery_path", [])
58
- discovery_path = [e[-1] for e in discovery_path]
59
58
  self.writerow(
60
59
  {
61
60
  "Event type": getattr(event, "type", ""),
@@ -65,7 +64,7 @@ class CSV(BaseOutputModule):
65
64
  ),
66
65
  "Source Module": str(getattr(event, "module_sequence", "")),
67
66
  "Scope Distance": str(getattr(event, "scope_distance", "")),
68
- "Event Tags": ",".join(sorted(list(getattr(event, "tags", [])))),
67
+ "Event Tags": ",".join(sorted(getattr(event, "tags", []))),
69
68
  "Discovery Path": " --> ".join(discovery_path),
70
69
  }
71
70
  )
@@ -1,4 +1,3 @@
1
- from bbot.errors import WebError
2
1
  from bbot.modules.output.base import BaseOutputModule
3
2
 
4
3
 
@@ -52,16 +51,23 @@ class HTTP(BaseOutputModule):
52
51
 
53
52
  async def handle_event(self, event):
54
53
  while 1:
55
- try:
56
- await self.helpers.request(
57
- url=self.url,
58
- method=self.method,
59
- auth=self.auth,
60
- headers=self.headers,
61
- json=event.json(siem_friendly=self.siem_friendly),
62
- raise_error=True,
63
- )
64
- break
65
- except WebError as e:
66
- self.warning(f"Error sending {event}: {e}, retrying...")
67
- await self.helpers.sleep(1)
54
+ response = await self.helpers.request(
55
+ url=self.url,
56
+ method=self.method,
57
+ auth=self.auth,
58
+ headers=self.headers,
59
+ json=event.json(siem_friendly=self.siem_friendly),
60
+ )
61
+ is_success = False if response is None else response.is_success
62
+ if not is_success:
63
+ status_code = getattr(response, "status_code", 0)
64
+ self.warning(f"Error sending {event} (HTTP status code: {status_code}), retrying...")
65
+ body = getattr(response, "text", "")
66
+ self.debug(body)
67
+ if status_code == 429:
68
+ sleep_interval = 10
69
+ else:
70
+ sleep_interval = 1
71
+ await self.helpers.sleep(sleep_interval)
72
+ continue
73
+ break
@@ -0,0 +1,51 @@
1
+ from bbot.modules.templates.sql import SQLTemplate
2
+
3
+
4
+ class MySQL(SQLTemplate):
5
+ watched_events = ["*"]
6
+ meta = {"description": "Output scan data to a MySQL database"}
7
+ options = {
8
+ "username": "root",
9
+ "password": "bbotislife",
10
+ "host": "localhost",
11
+ "port": 3306,
12
+ "database": "bbot",
13
+ }
14
+ options_desc = {
15
+ "username": "The username to connect to MySQL",
16
+ "password": "The password to connect to MySQL",
17
+ "host": "The server running MySQL",
18
+ "port": "The port to connect to MySQL",
19
+ "database": "The database name to connect to",
20
+ }
21
+ deps_pip = ["sqlmodel", "aiomysql"]
22
+ protocol = "mysql+aiomysql"
23
+
24
+ async def create_database(self):
25
+ from sqlalchemy import text
26
+ from sqlalchemy.ext.asyncio import create_async_engine
27
+
28
+ # Create the engine for the initial connection to the server
29
+ initial_engine = create_async_engine(self.connection_string().rsplit("/", 1)[0])
30
+
31
+ async with initial_engine.connect() as conn:
32
+ # Check if the database exists
33
+ result = await conn.execute(text(f"SHOW DATABASES LIKE '{self.database}'"))
34
+ database_exists = result.scalar() is not None
35
+
36
+ # Create the database if it does not exist
37
+ if not database_exists:
38
+ # Use aiomysql directly to create the database
39
+ import aiomysql
40
+
41
+ raw_conn = await aiomysql.connect(
42
+ user=self.username,
43
+ password=self.password,
44
+ host=self.host,
45
+ port=self.port,
46
+ )
47
+ try:
48
+ async with raw_conn.cursor() as cursor:
49
+ await cursor.execute(f"CREATE DATABASE {self.database}")
50
+ finally:
51
+ await raw_conn.ensure_closed()
@@ -1,10 +1,15 @@
1
1
  import json
2
+ import logging
2
3
  from contextlib import suppress
3
4
  from neo4j import AsyncGraphDatabase
4
5
 
5
6
  from bbot.modules.output.base import BaseOutputModule
6
7
 
7
8
 
9
+ # silence annoying neo4j logger
10
+ logging.getLogger("neo4j").setLevel(logging.CRITICAL)
11
+
12
+
8
13
  class neo4j(BaseOutputModule):
9
14
  """
10
15
  # start Neo4j in the background with docker
@@ -48,7 +53,7 @@ class neo4j(BaseOutputModule):
48
53
  ),
49
54
  )
50
55
  self.session = self.driver.session()
51
- await self.handle_event(self.scan.root_event)
56
+ await self.session.run("Match () Return 1 Limit 1")
52
57
  except Exception as e:
53
58
  return False, f"Error setting up Neo4j: {e}"
54
59
  return True
@@ -110,7 +115,7 @@ class neo4j(BaseOutputModule):
110
115
 
111
116
  cypher = f"""UNWIND $events AS event
112
117
  MERGE (_:{event_type} {{ id: event.id }})
113
- SET _ += event
118
+ SET _ += properties(event)
114
119
  RETURN event.data as event_data, event.id as event_id, elementId(_) as neo4j_id"""
115
120
  neo4j_ids = {}
116
121
  # insert events
@@ -0,0 +1,49 @@
1
+ from bbot.modules.templates.sql import SQLTemplate
2
+
3
+
4
+ class Postgres(SQLTemplate):
5
+ watched_events = ["*"]
6
+ meta = {"description": "Output scan data to a SQLite database"}
7
+ options = {
8
+ "username": "postgres",
9
+ "password": "bbotislife",
10
+ "host": "localhost",
11
+ "port": 5432,
12
+ "database": "bbot",
13
+ }
14
+ options_desc = {
15
+ "username": "The username to connect to Postgres",
16
+ "password": "The password to connect to Postgres",
17
+ "host": "The server running Postgres",
18
+ "port": "The port to connect to Postgres",
19
+ "database": "The database name to connect to",
20
+ }
21
+ deps_pip = ["sqlmodel", "asyncpg"]
22
+ protocol = "postgresql+asyncpg"
23
+
24
+ async def create_database(self):
25
+ import asyncpg
26
+ from sqlalchemy import text
27
+ from sqlalchemy.ext.asyncio import create_async_engine
28
+
29
+ # Create the engine for the initial connection to the server
30
+ initial_engine = create_async_engine(self.connection_string().rsplit("/", 1)[0])
31
+
32
+ async with initial_engine.connect() as conn:
33
+ # Check if the database exists
34
+ result = await conn.execute(text(f"SELECT 1 FROM pg_database WHERE datname = '{self.database}'"))
35
+ database_exists = result.scalar() is not None
36
+
37
+ # Create the database if it does not exist
38
+ if not database_exists:
39
+ # Use asyncpg directly to create the database
40
+ raw_conn = await asyncpg.connect(
41
+ user=self.username,
42
+ password=self.password,
43
+ host=self.host,
44
+ port=self.port,
45
+ )
46
+ try:
47
+ await raw_conn.execute(f"CREATE DATABASE {self.database}")
48
+ finally:
49
+ await raw_conn.close()
@@ -16,7 +16,6 @@ class Slack(WebhookOutputModule):
16
16
  "event_types": "Types of events to send",
17
17
  "min_severity": "Only allow VULNERABILITY events of this severity or higher",
18
18
  }
19
- good_status_code = 200
20
19
  content_key = "text"
21
20
 
22
21
  def format_message_str(self, event):
@@ -0,0 +1,29 @@
1
+ from pathlib import Path
2
+
3
+ from bbot.modules.templates.sql import SQLTemplate
4
+
5
+
6
+ class SQLite(SQLTemplate):
7
+ watched_events = ["*"]
8
+ meta = {"description": "Output scan data to a SQLite database"}
9
+ options = {
10
+ "database": "",
11
+ }
12
+ options_desc = {
13
+ "database": "The path to the sqlite database file",
14
+ }
15
+ deps_pip = ["sqlmodel", "aiosqlite"]
16
+
17
+ async def setup(self):
18
+ db_file = self.config.get("database", "")
19
+ if not db_file:
20
+ db_file = self.scan.home / "output.sqlite"
21
+ db_file = Path(db_file)
22
+ if not db_file.is_absolute():
23
+ db_file = self.scan.home / db_file
24
+ self.db_file = db_file
25
+ self.db_file.parent.mkdir(parents=True, exist_ok=True)
26
+ return await super().setup()
27
+
28
+ def connection_string(self, mask_password=False):
29
+ return f"sqlite+aiosqlite:///{self.db_file}"
@@ -20,7 +20,7 @@ class Stdout(BaseOutputModule):
20
20
 
21
21
  async def setup(self):
22
22
  self.text_format = self.config.get("format", "text").strip().lower()
23
- if not self.text_format in self.format_choices:
23
+ if self.text_format not in self.format_choices:
24
24
  return (
25
25
  False,
26
26
  f'Invalid text format choice, "{self.text_format}" (choices: {",".join(self.format_choices)})',
@@ -33,7 +33,7 @@ class Stdout(BaseOutputModule):
33
33
 
34
34
  async def filter_event(self, event):
35
35
  if self.accept_event_types:
36
- if not event.type in self.accept_event_types:
36
+ if event.type not in self.accept_event_types:
37
37
  return False, f'Event type "{event.type}" is not in the allowed event_types'
38
38
  return True
39
39
 
@@ -10,14 +10,115 @@ class Teams(WebhookOutputModule):
10
10
  }
11
11
  options = {"webhook_url": "", "event_types": ["VULNERABILITY", "FINDING"], "min_severity": "LOW"}
12
12
  options_desc = {
13
- "webhook_url": "Discord webhook URL",
13
+ "webhook_url": "Teams webhook URL",
14
14
  "event_types": "Types of events to send",
15
15
  "min_severity": "Only allow VULNERABILITY events of this severity or higher",
16
16
  }
17
17
  _module_threads = 5
18
- good_status_code = 200
19
- content_key = "text"
20
18
 
21
- def evaluate_response(self, response):
22
- text = getattr(response, "text", "")
23
- return text == "1"
19
+ async def handle_event(self, event):
20
+ while 1:
21
+ data = self.format_message(event)
22
+
23
+ response = await self.helpers.request(
24
+ url=self.webhook_url,
25
+ method="POST",
26
+ json=data,
27
+ )
28
+ status_code = getattr(response, "status_code", 0)
29
+ if self.evaluate_response(response):
30
+ break
31
+ else:
32
+ response_data = getattr(response, "text", "")
33
+ try:
34
+ retry_after = response.json().get("retry_after", 1)
35
+ except Exception:
36
+ retry_after = 1
37
+ self.verbose(
38
+ f"Error sending {event}: status code {status_code}, response: {response_data}, retrying in {retry_after} seconds"
39
+ )
40
+ await self.helpers.sleep(retry_after)
41
+
42
+ def trim_message(self, message):
43
+ if len(message) > self.message_size_limit:
44
+ message = message[: self.message_size_limit - 3] + "..."
45
+ return message
46
+
47
+ def format_message_str(self, event):
48
+ items = []
49
+ msg = self.trim_message(event.data)
50
+ items.append({"type": "TextBlock", "text": f"{msg}", "wrap": True})
51
+ items.append({"type": "FactSet", "facts": [{"title": "Tags:", "value": ", ".join(event.tags)}]})
52
+ return items
53
+
54
+ def format_message_other(self, event):
55
+ items = [{"type": "FactSet", "facts": []}]
56
+ for key, value in event.data.items():
57
+ if key != "severity":
58
+ msg = self.trim_message(str(value))
59
+ items[0]["facts"].append({"title": f"{key}:", "value": msg})
60
+ return items
61
+
62
+ def get_severity_color(self, event):
63
+ color = "Accent"
64
+ if event.type == "VULNERABILITY":
65
+ severity = event.data.get("severity", "UNKNOWN")
66
+ if severity == "CRITICAL":
67
+ color = "Attention"
68
+ elif severity == "HIGH":
69
+ color = "Attention"
70
+ elif severity == "MEDIUM":
71
+ color = "Warning"
72
+ elif severity == "LOW":
73
+ color = "Good"
74
+ return color
75
+
76
+ def format_message(self, event):
77
+ adaptive_card = {
78
+ "type": "message",
79
+ "attachments": [
80
+ {
81
+ "contentType": "application/vnd.microsoft.card.adaptive",
82
+ "contentUrl": None,
83
+ "content": {
84
+ "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
85
+ "type": "AdaptiveCard",
86
+ "version": "1.2",
87
+ "msteams": {"width": "full"},
88
+ "body": [],
89
+ },
90
+ }
91
+ ],
92
+ }
93
+ heading = {"type": "TextBlock", "text": f"{event.type}", "wrap": True, "size": "Large", "style": "heading"}
94
+ body = adaptive_card["attachments"][0]["content"]["body"]
95
+ body.append(heading)
96
+ if event.type in ("VULNERABILITY", "FINDING"):
97
+ subheading = {
98
+ "type": "TextBlock",
99
+ "text": event.data.get("severity", "UNKNOWN"),
100
+ "spacing": "None",
101
+ "size": "Large",
102
+ "wrap": True,
103
+ }
104
+ subheading["color"] = self.get_severity_color(event)
105
+ body.append(subheading)
106
+ main_text = {
107
+ "type": "ColumnSet",
108
+ "separator": True,
109
+ "spacing": "Medium",
110
+ "columns": [
111
+ {
112
+ "type": "Column",
113
+ "width": "stretch",
114
+ "items": [],
115
+ }
116
+ ],
117
+ }
118
+ if isinstance(event.data, str):
119
+ items = self.format_message_str(event)
120
+ else:
121
+ items = self.format_message_other(event)
122
+ main_text["columns"][0]["items"] = items
123
+ body.append(main_text)
124
+ return adaptive_card
@@ -15,7 +15,7 @@ class paramminer_headers(BaseModule):
15
15
  meta = {
16
16
  "description": "Use smart brute-force to check for common HTTP header parameters",
17
17
  "created_date": "2022-04-15",
18
- "author": "@pmueller",
18
+ "author": "@liquidsec",
19
19
  }
20
20
  options = {
21
21
  "wordlist": "", # default is defined within setup function
@@ -82,7 +82,6 @@ class paramminer_headers(BaseModule):
82
82
  header_regex = re.compile(r"^[!#$%&\'*+\-.^_`|~0-9a-zA-Z]+: [^\r\n]+$")
83
83
 
84
84
  async def setup(self):
85
-
86
85
  self.recycle_words = self.config.get("recycle_words", True)
87
86
  self.event_dict = {}
88
87
  self.already_checked = set()
@@ -90,11 +89,11 @@ class paramminer_headers(BaseModule):
90
89
  if not wordlist:
91
90
  wordlist = f"{self.helpers.wordlist_dir}/{self.default_wordlist}"
92
91
  self.debug(f"Using wordlist: [{wordlist}]")
93
- self.wl = set(
92
+ self.wl = {
94
93
  h.strip().lower()
95
94
  for h in self.helpers.read_file(await self.helpers.wordlist(wordlist))
96
95
  if len(h) > 0 and "%" not in h
97
- )
96
+ }
98
97
 
99
98
  # check against the boring list (if the option is set)
100
99
  if self.config.get("skip_boring_words", True):
@@ -157,7 +156,6 @@ class paramminer_headers(BaseModule):
157
156
  )
158
157
 
159
158
  async def handle_event(self, event):
160
-
161
159
  # If recycle words is enabled, we will collect WEB_PARAMETERS we find to build our list in finish()
162
160
  # We also collect any parameters of type "SPECULATIVE"
163
161
  if event.type == "WEB_PARAMETER":
@@ -201,7 +199,7 @@ class paramminer_headers(BaseModule):
201
199
  return
202
200
  for count, args, kwargs in self.gen_count_args(url):
203
201
  r = await self.helpers.request(*args, **kwargs)
204
- if r is not None and not ((str(r.status_code)[0] in ("4", "5"))):
202
+ if r is not None and not (str(r.status_code)[0] in ("4", "5")):
205
203
  return count
206
204
 
207
205
  def gen_count_args(self, url):
@@ -240,8 +238,7 @@ class paramminer_headers(BaseModule):
240
238
  return await compare_helper.compare(url, headers=test_headers, check_reflection=(len(header_list) == 1))
241
239
 
242
240
  async def finish(self):
243
-
244
- untested_matches = sorted(list(self.extracted_words_master.copy()))
241
+ untested_matches = sorted(self.extracted_words_master.copy())
245
242
  for url, (event, batch_size) in list(self.event_dict.items()):
246
243
  try:
247
244
  compare_helper = self.helpers.http_compare(url)