Skip to content

Commit a447b9c

Browse files
authored
Merge branch 'dev' into docs-update
2 parents a3d83c5 + 82df903 commit a447b9c

35 files changed

Lines changed: 3344 additions & 145 deletions

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[![bbot_banner](https://github.com/user-attachments/assets/f02804ce-9478-4f1e-ac4d-9cf5620a3214)](https://github.com/blacklanternsecurity/bbot)
22

3-
[![Python Version](https://img.shields.io/badge/python-3.9+-FF8400)](https://www.python.org) [![License](https://img.shields.io/badge/license-AGPLv3-FF8400.svg)](https://github.com/blacklanternsecurity/bbot/blob/dev/LICENSE) [![DEF CON Recon Village 2024](https://img.shields.io/badge/DEF%20CON%20Demo%20Labs-2023-FF8400.svg)](https://www.reconvillage.org/talks) [![PyPi Downloads](https://static.pepy.tech/personalized-badge/bbot?right_color=orange&left_color=grey)](https://pepy.tech/project/bbot) [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) [![Tests](https://github.com/blacklanternsecurity/bbot/actions/workflows/tests.yml/badge.svg?branch=stable)](https://github.com/blacklanternsecurity/bbot/actions?query=workflow%3A"tests") [![Codecov](https://codecov.io/gh/blacklanternsecurity/bbot/branch/dev/graph/badge.svg?token=IR5AZBDM5K)](https://codecov.io/gh/blacklanternsecurity/bbot) [![Discord](https://img.shields.io/discord/859164869970362439)](https://discord.com/invite/PZqkgxu5SA)
3+
[![Python Version](https://img.shields.io/badge/python-3.10+-FF8400)](https://www.python.org) [![License](https://img.shields.io/badge/license-AGPLv3-FF8400.svg)](https://github.com/blacklanternsecurity/bbot/blob/dev/LICENSE) [![DEF CON Recon Village 2024](https://img.shields.io/badge/DEF%20CON%20Demo%20Labs-2023-FF8400.svg)](https://www.reconvillage.org/talks) [![PyPi Downloads](https://static.pepy.tech/personalized-badge/bbot?right_color=orange&left_color=grey)](https://pepy.tech/project/bbot) [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) [![Tests](https://github.com/blacklanternsecurity/bbot/actions/workflows/tests.yml/badge.svg?branch=stable)](https://github.com/blacklanternsecurity/bbot/actions?query=workflow%3A"tests") [![Codecov](https://codecov.io/gh/blacklanternsecurity/bbot/branch/dev/graph/badge.svg?token=IR5AZBDM5K)](https://codecov.io/gh/blacklanternsecurity/bbot) [![Discord](https://img.shields.io/discord/859164869970362439)](https://discord.com/invite/PZqkgxu5SA)
44

55
### **BEE·bot** is a multipurpose scanner inspired by [Spiderfoot](https://github.com/smicallef/spiderfoot), built to automate your **Recon**, **Bug Bounties**, and **ASM**!
66

bbot/core/event/base.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2005,6 +2005,31 @@ def _data_human(self):
20052005
return tech
20062006

20072007

2008+
class VIRTUAL_HOST(DictHostEvent):
2009+
class _data_validator(BaseModel):
2010+
host: str
2011+
virtual_host: str
2012+
url: Optional[str] = None
2013+
description: Optional[str] = None
2014+
ip: Optional[str] = None
2015+
_validate_url = field_validator("url")(validators.validate_url)
2016+
_validate_host = field_validator("host")(validators.validate_host)
2017+
2018+
def _data_id(self):
2019+
virtual_host = self.data.get("virtual_host", "")
2020+
return f"{self.host}:{virtual_host}"
2021+
2022+
def _pretty_string(self):
2023+
return self.data.get("virtual_host", "")
2024+
2025+
def _data_human(self):
2026+
virtual_host = self.data.get("virtual_host", "")
2027+
url = self.data.get("url", "")
2028+
if url:
2029+
return f"{virtual_host} ({url})"
2030+
return virtual_host
2031+
2032+
20082033
class PROTOCOL(DictHostEvent):
20092034
class _data_validator(BaseModel):
20102035
host: str

bbot/core/helpers/git.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,27 @@
11
from pathlib import Path
22

33

4+
_SAFE_GIT_CONFIG = """\
5+
[core]
6+
\trepositoryformatversion = 0
7+
\tfilemode = true
8+
\tbare = false
9+
\tlogallrefupdates = true
10+
\tfsmonitor = false
11+
\tsymlinks = false
12+
\tsshCommand = echo
13+
[transfer]
14+
\tfsckObjects = true
15+
"""
16+
17+
418
def sanitize_git_repo(repo_folder: Path):
5-
# sanitizing the git config is infeasible since there are too many different ways to do evil things
6-
# instead, we move it out of .git and into the repo folder, so we don't miss any secrets etc. inside
19+
# replace the git config with a safe one that neutralizes dangerous directives
20+
# the original is preserved in the repo folder so secret-scanning tools can still inspect it
721
config_file = repo_folder / ".git" / "config"
822
if config_file.exists():
923
config_file.rename(repo_folder / "git_config_original")
24+
config_file.write_text(_SAFE_GIT_CONFIG)
1025
# leave .git/index in place -- it's binary metadata (filename-to-SHA mappings),
1126
# not a security risk, and removing it breaks tools that need to clone the repo
1227
# move the hooks folder

bbot/core/helpers/simhash.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@ def __init__(self, bits=64):
1111
@staticmethod
1212
def compute_simhash(text, bits=64, truncate=True, normalization_filter=None):
1313
"""
14-
Static method for computing SimHash that can be used with multiprocessing.
14+
Static method for computing a SimHash fingerprint.
1515
16-
This method is designed to be used with run_in_executor_mp() for CPU-intensive
17-
SimHash computations across multiple processes.
16+
Designed to be called via run_in_executor_cpu(): the work is short and the
17+
input is truncated to ~3KB inside the helper, so a thread pool avoids the
18+
pickle/spawn overhead of a process pool.
1819
1920
Args:
2021
text (str): The text to hash

bbot/core/modules.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@
3030

3131
log = logging.getLogger("bbot.module_loader")
3232

33+
34+
class _SafeUnpickler(pickle.Unpickler):
35+
def find_class(self, module, name):
36+
raise pickle.UnpicklingError(f"Forbidden class: {module}.{name}")
37+
38+
3339
bbot_code_dir = Path(__file__).parent.parent
3440

3541

@@ -450,7 +456,7 @@ def preload_cache(self):
450456
if self.preload_cache_file.is_file():
451457
with suppress(Exception):
452458
with open(self.preload_cache_file, "rb") as f:
453-
self._preload_cache = pickle.load(f)
459+
self._preload_cache = _SafeUnpickler(f).load()
454460
return self._preload_cache
455461

456462
@preload_cache.setter

bbot/errors.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,6 @@ class WordlistError(BBOTError):
3838
pass
3939

4040

41-
class CurlError(BBOTError):
42-
pass
43-
44-
4541
class PresetNotFoundError(BBOTError):
4642
pass
4743

bbot/modules/apkpure.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ async def handle_event(self, event):
4949

5050
async def download_apk(self, app_id):
5151
path = None
52+
if "/" in app_id or "\\" in app_id or ".." in app_id:
53+
self.warning(f"Unsafe app_id, skipping: {app_id}")
54+
return path
5255
url = f"https://d.apkpure.com/b/XAPK/{app_id}?version=latest"
5356
self.helpers.mkdir(self.output_dir / app_id)
5457
response = await self.helpers.request(url, allow_redirects=True)

bbot/modules/aspnet_bin_exposure.py

Lines changed: 46 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,19 @@ class aspnet_bin_exposure(BaseModule):
1212
}
1313

1414
in_scope_only = True
15+
_module_threads = 2
16+
1517
test_dlls = [
1618
"Telerik.Web.UI.dll",
1719
"Newtonsoft.Json.dll",
1820
"System.Net.Http.dll",
1921
"EntityFramework.dll",
2022
"AjaxControlToolkit.dll",
2123
]
24+
_techniques = [
25+
"b/(S(X))in/###DLL_PLACEHOLDER###/(S(X))/",
26+
"(S(X))/b/(S(X))in/###DLL_PLACEHOLDER###",
27+
]
2228

2329
@staticmethod
2430
def normalize_url(url):
@@ -27,54 +33,52 @@ def normalize_url(url):
2733
def _incoming_dedup_hash(self, event):
2834
return hash(self.normalize_url(event.url))
2935

36+
@staticmethod
37+
def _is_dll_download(response):
38+
return (
39+
response is not None
40+
and response.status_code == 200
41+
and "content-type" in response.headers
42+
and "application/x-msdownload" in response.headers["content-type"]
43+
)
44+
3045
async def handle_event(self, event):
3146
normalized_url = self.normalize_url(event.url)
47+
kwargs = {"method": "GET", "allow_redirects": False, "timeout": 10}
48+
49+
probes = []
3250
for test_dll in self.test_dlls:
33-
for technique in ["b/(S(X))in/###DLL_PLACEHOLDER###/(S(X))/", "(S(X))/b/(S(X))in/###DLL_PLACEHOLDER###"]:
51+
for technique in self._techniques:
3452
test_url = f"{normalized_url}{technique.replace('###DLL_PLACEHOLDER###', test_dll)}"
35-
self.debug(f"Sending test URL: [{test_url}]")
36-
kwargs = {"method": "GET", "allow_redirects": False, "timeout": 10}
37-
test_result = await self.helpers.request(test_url, **kwargs)
38-
if test_result:
39-
if test_result.status_code == 200 and (
40-
"content-type" in test_result.headers
41-
and "application/x-msdownload" in test_result.headers["content-type"]
42-
):
43-
self.debug(
44-
f"Got positive result for probe with test url: [{test_url}]. Status Code: [{test_result.status_code}] Content Length: [{len(test_result.content)}]"
45-
)
53+
probes.append((test_url, kwargs, technique))
54+
55+
async for test_url, test_result, technique in self.helpers.request_batch_stream(probes, threads=10):
56+
if not self._is_dll_download(test_result):
57+
continue
58+
59+
self.debug(
60+
f"Got positive result for probe with test url: [{test_url}]. Status Code: [{test_result.status_code}] Content Length: [{len(test_result.content)}]"
61+
)
4662

47-
if test_result.status_code == 200 and (
48-
"content-type" in test_result.headers
49-
and "application/x-msdownload" in test_result.headers["content-type"]
50-
):
51-
confirm_url = (
52-
f"{normalized_url}{technique.replace('###DLL_PLACEHOLDER###', 'oopsnotarealdll.dll')}"
53-
)
54-
confirm_result = await self.helpers.request(confirm_url, **kwargs)
63+
confirm_url = f"{normalized_url}{technique.replace('###DLL_PLACEHOLDER###', 'oopsnotarealdll.dll')}"
64+
confirm_result = await self.helpers.request(confirm_url, **kwargs)
5565

56-
if confirm_result and (
57-
confirm_result.status_code != 200
58-
or not (
59-
"content-type" in confirm_result.headers
60-
and "application/x-msdownload" in confirm_result.headers["content-type"]
61-
)
62-
):
63-
description = f"IIS Bin Directory DLL Exposure. Detection Url: [{test_url}]"
64-
await self.emit_event(
65-
{
66-
"name": "IIS Bin Directory DLL Exposure",
67-
"severity": "HIGH",
68-
"confidence": "HIGH",
69-
"host": str(event.host),
70-
"url": normalized_url,
71-
"description": description,
72-
},
73-
"FINDING",
74-
event,
75-
context="{module} detected IIS Bin Directory DLL Exposure vulnerability",
76-
)
77-
return True
66+
if confirm_result and not self._is_dll_download(confirm_result):
67+
description = f"IIS Bin Directory DLL Exposure. Detection Url: [{test_url}]"
68+
await self.emit_event(
69+
{
70+
"name": "IIS Bin Directory DLL Exposure",
71+
"severity": "HIGH",
72+
"confidence": "HIGH",
73+
"host": str(event.host),
74+
"url": normalized_url,
75+
"description": description,
76+
},
77+
"FINDING",
78+
event,
79+
context="{module} detected IIS Bin Directory DLL Exposure vulnerability",
80+
)
81+
return True
7882

7983
async def filter_event(self, event):
8084
if "dir" in event.tags:

bbot/modules/docker_pull.py

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -83,14 +83,13 @@ async def docker_api_request(self, url: str):
8383
response = await self.helpers.request(url, headers=self.headers, follow_redirects=True)
8484
if response is not None and response.status_code != 401:
8585
return response
86-
try:
87-
www_authenticate_headers = response.headers.get("www-authenticate", "")
88-
realm = www_authenticate_headers.split('realm="')[1].split('"')[0]
89-
service = www_authenticate_headers.split('service="')[1].split('"')[0]
90-
scope = www_authenticate_headers.split('scope="')[1].split('"')[0]
91-
except (KeyError, IndexError):
86+
www_auth = response.headers.get("www-authenticate", "")
87+
realm, service, scope = self._parse_www_authenticate(www_auth)
88+
if not all([realm, service, scope]):
9289
self.log.warning(f"Could not obtain realm, service or scope from {url}")
9390
break
91+
if not self._validate_realm(url, realm):
92+
break
9493
auth_url = f"{realm}?service={service}&scope={scope}"
9594
auth_response = await self.helpers.request(auth_url)
9695
if not auth_response:
@@ -101,6 +100,29 @@ async def docker_api_request(self, url: str):
101100
self.headers.update({"Authorization": f"Bearer {token}"})
102101
return None
103102

103+
@staticmethod
104+
def _parse_www_authenticate(header):
105+
value = header
106+
if value.lower().startswith("bearer "):
107+
value = value[7:]
108+
params = {}
109+
for part in value.split(","):
110+
part = part.strip()
111+
if "=" in part:
112+
key, _, val = part.partition("=")
113+
params[key.strip()] = val.strip('"')
114+
return params.get("realm", ""), params.get("service", ""), params.get("scope", "")
115+
116+
def _validate_realm(self, registry_url, realm):
117+
registry_host = self.helpers.urlparse(registry_url).hostname or ""
118+
realm_host = self.helpers.urlparse(realm).hostname or ""
119+
_, registry_domain = self.helpers.split_domain(registry_host)
120+
_, realm_domain = self.helpers.split_domain(realm_host)
121+
if not realm_domain or realm_domain != registry_domain:
122+
self.warning(f"Auth realm TLD ({realm_domain}) does not match registry TLD ({registry_domain}), skipping")
123+
return False
124+
return True
125+
104126
async def get_tags(self, registry, repository):
105127
url = f"{registry}/v2/{repository}/tags/list"
106128
r = await self.docker_api_request(url)

bbot/modules/generic_ssrf.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,7 @@ async def test(self, event):
123123
post_data_list = [(subdomain_tag, post_data), (subdomain_tag_lower, post_data_lower)]
124124

125125
for tag, pd in post_data_list:
126-
# Send raw body (not URL-encoded) so payload URLs like http://... reach the
127-
# server literally — matching old curl -d behavior.
126+
# Send raw body (not URL-encoded) so payload URLs like http://... reach the server literally.
128127
raw_body = "&".join(f"{k}={v}" for k, v in pd.items())
129128
r = await self.generic_ssrf.helpers.request(url=test_url, method="POST", body=raw_body)
130129
if r:

0 commit comments

Comments
 (0)