Skip to content

Commit d6dad27

Browse files
Merge pull request #2888 from blacklanternsecurity/dev
Dev -> Stable 2.8.1
2 parents 5148c75 + e269141 commit d6dad27

12 files changed

Lines changed: 992 additions & 377 deletions

File tree

bbot/core/event/base.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1247,6 +1247,10 @@ def _words(self):
12471247
return set()
12481248

12491249

1250+
class OPEN_UDP_PORT(OPEN_TCP_PORT):
1251+
pass
1252+
1253+
12501254
class URL_UNVERIFIED(BaseEvent):
12511255
_status_code_regex = re.compile(r"^status-(\d{1,3})$")
12521256

bbot/modules/azure_tenant.py

Lines changed: 47 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
import regex as re
2-
from contextlib import suppress
3-
41
from bbot.modules.base import BaseModule
52

63

@@ -9,116 +6,90 @@ class azure_tenant(BaseModule):
96
produced_events = ["DNS_NAME"]
107
flags = ["affiliates", "subdomain-enum", "cloud-enum", "passive", "safe"]
118
meta = {
12-
"description": "Query Azure for tenant sister domains",
9+
"description": "Query Azure via azmap.dev for tenant sister domains",
1310
"created_date": "2024-07-04",
1411
"author": "@TheTechromancer",
1512
}
1613

17-
base_url = "https://autodiscover-s.outlook.com"
14+
base_url = "https://azmap.dev/api/tenant"
1815
in_scope_only = True
1916
per_domain_only = True
2017

2118
async def setup(self):
2219
self.processed = set()
23-
self.d_xml_regex = re.compile(r"<Domain>([^<>/]*)</Domain>", re.I)
2420
return True
2521

2622
async def handle_event(self, event):
2723
_, query = self.helpers.split_domain(event.data)
28-
domains, openid_config = await self.query(query)
29-
30-
tenant_id = None
31-
authorization_endpoint = openid_config.get("authorization_endpoint", "")
32-
matches = await self.helpers.re.findall(self.helpers.regexes.uuid_regex, authorization_endpoint)
33-
if matches:
34-
tenant_id = matches[0]
35-
36-
tenant_names = set()
37-
if domains:
38-
self.verbose(f'Found {len(domains):,} domains under tenant for "{query}": {", ".join(sorted(domains))}')
39-
for domain in domains:
24+
tenant_data = await self.query(query)
25+
26+
if not tenant_data:
27+
return
28+
29+
tenant_id = tenant_data.get("tenant_id")
30+
tenant_name = tenant_data.get("tenant_name")
31+
email_domains = tenant_data.get("email_domains", [])
32+
33+
if email_domains:
34+
self.verbose(
35+
f'Found {len(email_domains):,} domains under tenant for "{query}": {", ".join(sorted(email_domains))}'
36+
)
37+
for domain in email_domains:
4038
if domain != query:
4139
await self.emit_event(
4240
domain,
4341
"DNS_NAME",
4442
parent=event,
4543
tags=["affiliate", "azure-tenant"],
46-
context=f'{{module}} queried Outlook autodiscover for "{query}" and found {{event.type}}: {{event.data}}',
44+
context=f'{{module}} queried azmap.dev for "{query}" and found {{event.type}}: {{event.data}}',
4745
)
48-
# tenant names
49-
if domain.lower().endswith(".onmicrosoft.com"):
50-
tenantname = domain.split(".")[0].lower()
51-
if tenantname:
52-
tenant_names.add(tenantname)
53-
54-
tenant_names = sorted(tenant_names)
55-
event_data = {"tenant-names": tenant_names, "domains": sorted(domains)}
46+
47+
# Build tenant names list (include the tenant name from the API)
48+
tenant_names = []
49+
if tenant_name:
50+
tenant_names.append(tenant_name)
51+
52+
# Also extract tenant names from .onmicrosoft.com domains
53+
for domain in email_domains:
54+
if domain.lower().endswith(".onmicrosoft.com"):
55+
tenantname = domain.split(".")[0].lower()
56+
if tenantname and tenantname not in tenant_names:
57+
tenant_names.append(tenantname)
58+
59+
event_data = {"tenant-names": tenant_names, "domains": sorted(email_domains)}
5660
tenant_names_str = ",".join(tenant_names)
57-
if tenant_id is not None:
61+
if tenant_id:
5862
event_data["tenant-id"] = tenant_id
5963
await self.emit_event(
6064
event_data,
6165
"AZURE_TENANT",
6266
parent=event,
63-
context=f'{{module}} queried Outlook autodiscover for "{query}" and found {{event.type}}: {tenant_names_str}',
67+
context=f'{{module}} queried azmap.dev for "{query}" and found {{event.type}}: {tenant_names_str}',
6468
)
6569

6670
async def query(self, domain):
67-
url = f"{self.base_url}/autodiscover/autodiscover.svc"
68-
data = f"""<?xml version="1.0" encoding="utf-8"?>
69-
<soap:Envelope xmlns:exm="http://schemas.microsoft.com/exchange/services/2006/messages" xmlns:ext="http://schemas.microsoft.com/exchange/services/2006/types" xmlns:a="http://www.w3.org/2005/08/addressing" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
70-
<soap:Header>
71-
<a:Action soap:mustUnderstand="1">http://schemas.microsoft.com/exchange/2010/Autodiscover/Autodiscover/GetFederationInformation</a:Action>
72-
<a:To soap:mustUnderstand="1">https://autodiscover-s.outlook.com/autodiscover/autodiscover.svc</a:To>
73-
<a:ReplyTo>
74-
<a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>
75-
</a:ReplyTo>
76-
</soap:Header>
77-
<soap:Body>
78-
<GetFederationInformationRequestMessage xmlns="http://schemas.microsoft.com/exchange/2010/Autodiscover">
79-
<Request>
80-
<Domain>{domain}</Domain>
81-
</Request>
82-
</GetFederationInformationRequestMessage>
83-
</soap:Body>
84-
</soap:Envelope>"""
85-
86-
headers = {
87-
"Content-Type": "text/xml; charset=utf-8",
88-
"SOAPAction": '"http://schemas.microsoft.com/exchange/2010/Autodiscover/Autodiscover/GetFederationInformation"',
89-
"User-Agent": "AutodiscoverClient",
90-
"Accept-Encoding": "identity",
91-
}
71+
url = f"{self.base_url}?domain={domain}&extract=true"
9272

9373
self.debug(f"Retrieving tenant domains at {url}")
9474

95-
autodiscover_task = self.helpers.create_task(
96-
self.helpers.request(url, method="POST", headers=headers, content=data)
97-
)
98-
openid_url = f"https://login.windows.net/{domain}/.well-known/openid-configuration"
99-
openid_task = self.helpers.create_task(self.helpers.request(openid_url))
100-
101-
r = await autodiscover_task
75+
r = await self.helpers.request(url)
10276
status_code = getattr(r, "status_code", 0)
103-
if status_code not in (200, 421):
77+
if status_code != 200:
10478
self.verbose(f'Error retrieving azure_tenant domains for "{domain}" (status code: {status_code})')
105-
return set(), {}
106-
found_domains = list(set(await self.helpers.re.findall(self.d_xml_regex, r.text)))
107-
domains = set()
79+
return {}
10880

109-
for d in found_domains:
110-
# make sure we don't make any unnecessary api calls
81+
try:
82+
tenant_data = r.json()
83+
except Exception as e:
84+
self.warning(f'Error parsing JSON response for "{domain}": {e}')
85+
return {}
86+
87+
# Absorb domains into word cloud
88+
email_domains = tenant_data.get("email_domains", [])
89+
for d in email_domains:
11190
d = str(d).lower()
11291
_, query = self.helpers.split_domain(d)
11392
self.processed.add(hash(query))
114-
domains.add(d)
115-
# absorb into word cloud
11693
self.scan.word_cloud.absorb_word(d)
11794

118-
r = await openid_task
119-
openid_config = {}
120-
with suppress(Exception):
121-
openid_config = r.json()
122-
123-
domains = sorted(domains)
124-
return domains, openid_config
95+
return tenant_data

bbot/modules/censys_dns.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
from bbot.modules.templates.censys import censys
2+
3+
4+
class censys_dns(censys):
5+
"""
6+
Query the Censys certificates API for subdomains.
7+
Thanks to https://github.com/owasp-amass/amass/blob/master/resources/scripts/cert/censys.ads
8+
"""
9+
10+
watched_events = ["DNS_NAME"]
11+
produced_events = ["DNS_NAME"]
12+
flags = ["subdomain-enum", "passive", "safe"]
13+
meta = {
14+
"description": "Query the Censys API for subdomains",
15+
"created_date": "2022-08-04",
16+
"author": "@TheTechromancer",
17+
"auth_required": True,
18+
}
19+
options = {"api_key": "", "max_pages": 5}
20+
options_desc = {
21+
"api_key": "Censys.io API Key in the format of 'key:secret'",
22+
"max_pages": "Maximum number of pages to fetch (100 results per page)",
23+
}
24+
25+
async def setup(self):
26+
self.max_pages = self.config.get("max_pages", 5)
27+
return await super().setup()
28+
29+
async def query(self, query):
30+
results = set()
31+
cursor = ""
32+
for i in range(self.max_pages):
33+
url = f"{self.base_url}/v2/certificates/search"
34+
json_data = {
35+
"q": f"names: {query}",
36+
"per_page": 100,
37+
}
38+
if cursor:
39+
json_data.update({"cursor": cursor})
40+
resp = await self.api_request(
41+
url,
42+
method="POST",
43+
json=json_data,
44+
)
45+
46+
if resp is None:
47+
break
48+
49+
try:
50+
d = resp.json()
51+
except Exception as e:
52+
self.warning(f"Failed to parse JSON from {url} (response: {resp}): {e}")
53+
54+
if resp.status_code < 200 or resp.status_code >= 400:
55+
if isinstance(d, dict):
56+
error = d.get("error", "")
57+
if error:
58+
self.warning(error)
59+
self.verbose(f'Non-200 Status code: {resp.status_code} for query "{query}", page #{i + 1}')
60+
self.debug(f"Response: {resp.text}")
61+
break
62+
else:
63+
if d is None:
64+
break
65+
elif not isinstance(d, dict):
66+
break
67+
status = d.get("status", "").lower()
68+
result = d.get("result", {})
69+
hits = result.get("hits", [])
70+
if status != "ok" or not hits:
71+
break
72+
73+
for h in hits:
74+
names = h.get("names", [])
75+
for n in names:
76+
results.add(n.strip(".*").lower())
77+
78+
cursor = result.get("links", {}).get("next", "")
79+
if not cursor:
80+
break
81+
82+
return results

0 commit comments

Comments
 (0)