Skip to content

Commit e269141

Browse files
Merge pull request #2887 from blacklanternsecurity/fix-tenant-recon
Fix Azure tenant recon
2 parents 782c39d + 147d8df commit e269141

3 files changed

Lines changed: 60 additions & 160 deletions

File tree

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/test/test_step_2/module_tests/test_module_azure_tenant.py

Lines changed: 11 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -2,93 +2,22 @@
22

33

44
class TestAzure_Tenant(ModuleTestBase):
5-
tenant_response = """
6-
<?xml version="1.0"?>
7-
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" xmlns:a="http://www.w3.org/2005/08/addressing">
8-
<s:Header>
9-
<a:Action s:mustUnderstand="1">http://schemas.microsoft.com/exchange/2010/Autodiscover/Autodiscover/GetFederationInformationResponse</a:Action>
10-
<h:ServerVersionInfo xmlns:h="http://schemas.microsoft.com/exchange/2010/Autodiscover" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
11-
<h:MajorVersion>15</h:MajorVersion>
12-
<h:MinorVersion>20</h:MinorVersion>
13-
<h:MajorBuildNumber>6411</h:MajorBuildNumber>
14-
<h:MinorBuildNumber>14</h:MinorBuildNumber>
15-
<h:Version>Exchange2015</h:Version>
16-
</h:ServerVersionInfo>
17-
</s:Header>
18-
<s:Body>
19-
<GetFederationInformationResponseMessage xmlns="http://schemas.microsoft.com/exchange/2010/Autodiscover">
20-
<Response xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
21-
<ErrorCode>NoError</ErrorCode>
22-
<ErrorMessage/>
23-
<ApplicationUri>outlook.com</ApplicationUri>
24-
<Domains>
25-
<Domain>blacklanternsecurity.onmicrosoft.com</Domain>
26-
</Domains>
27-
<TokenIssuers>
28-
<TokenIssuer>
29-
<Endpoint>https://login.microsoftonline.com/extSTS.srf</Endpoint>
30-
<Uri>urn:federation:MicrosoftOnline</Uri>
31-
</TokenIssuer>
32-
</TokenIssuers>
33-
</Response>
34-
</GetFederationInformationResponseMessage>
35-
</s:Body>
36-
</s:Envelope>"""
37-
38-
openid_config_azure = {
39-
"token_endpoint": "https://login.windows.net/cc74fc12-4142-400e-a653-f98bdeadbeef/oauth2/token",
40-
"token_endpoint_auth_methods_supported": ["client_secret_post", "private_key_jwt", "client_secret_basic"],
41-
"jwks_uri": "https://login.windows.net/common/discovery/keys",
42-
"response_modes_supported": ["query", "fragment", "form_post"],
43-
"subject_types_supported": ["pairwise"],
44-
"id_token_signing_alg_values_supported": ["RS256"],
45-
"response_types_supported": ["code", "id_token", "code id_token", "token id_token", "token"],
46-
"scopes_supported": ["openid"],
47-
"issuer": "https://sts.windows.net/cc74fc12-4142-400e-a653-f98bdeadbeef/",
48-
"microsoft_multi_refresh_token": True,
49-
"authorization_endpoint": "https://login.windows.net/cc74fc12-4142-400e-a653-f98bdeadbeef/oauth2/authorize",
50-
"device_authorization_endpoint": "https://login.windows.net/cc74fc12-4142-400e-a653-f98bdeadbeef/oauth2/devicecode",
51-
"http_logout_supported": True,
52-
"frontchannel_logout_supported": True,
53-
"end_session_endpoint": "https://login.windows.net/cc74fc12-4142-400e-a653-f98bdeadbeef/oauth2/logout",
54-
"claims_supported": [
55-
"sub",
56-
"iss",
57-
"cloud_instance_name",
58-
"cloud_instance_host_name",
59-
"cloud_graph_host_name",
60-
"msgraph_host",
61-
"aud",
62-
"exp",
63-
"iat",
64-
"auth_time",
65-
"acr",
66-
"amr",
67-
"nonce",
68-
"email",
69-
"given_name",
70-
"family_name",
71-
"nickname",
5+
tenant_response = {
6+
"tenant_id": "cc74fc12-4142-400e-a653-f98bdeadbeef",
7+
"tenant_name": "blacklanternsecurity",
8+
"domain": "blacklanternsecurity.com",
9+
"email_domains": [
10+
"blacklanternsecurity.com",
11+
"blacklanternsecurity.onmicrosoft.com",
12+
"blsgvt.com",
13+
"o365.blacklanternsecurity.com",
7214
],
73-
"check_session_iframe": "https://login.windows.net/cc74fc12-4142-400e-a653-f98bdeadbeef/oauth2/checksession",
74-
"userinfo_endpoint": "https://login.windows.net/cc74fc12-4142-400e-a653-f98bdeadbeef/openid/userinfo",
75-
"kerberos_endpoint": "https://login.windows.net/cc74fc12-4142-400e-a653-f98bdeadbeef/kerberos",
76-
"tenant_region_scope": "NA",
77-
"cloud_instance_name": "microsoftonline.com",
78-
"cloud_graph_host_name": "graph.windows.net",
79-
"msgraph_host": "graph.microsoft.com",
80-
"rbac_url": "https://pas.windows.net",
8115
}
8216

8317
async def setup_after_prep(self, module_test):
8418
module_test.httpx_mock.add_response(
85-
method="POST",
86-
url="https://autodiscover-s.outlook.com/autodiscover/autodiscover.svc",
87-
text=self.tenant_response,
88-
)
89-
module_test.httpx_mock.add_response(
90-
url="https://login.windows.net/blacklanternsecurity.com/.well-known/openid-configuration",
91-
json=self.openid_config_azure,
19+
url="https://azmap.dev/api/tenant?domain=blacklanternsecurity.com&extract=true",
20+
json=self.tenant_response,
9221
)
9322

9423
def check(self, module_test, events):

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "bbot"
3-
version = "2.8.0"
3+
version = "2.8.1"
44
description = "OSINT automation for hackers."
55
authors = [
66
"TheTechromancer",
@@ -121,7 +121,7 @@ lint.ignore = ["E402", "E711", "E713", "E721", "E741", "F403", "F405", "E501"]
121121
[tool.poetry-dynamic-versioning]
122122
enable = true
123123
metadata = false
124-
format-jinja = 'v2.8.0{% if branch == "dev" %}.{{ distance }}rc{% endif %}'
124+
format-jinja = 'v2.8.1{% if branch == "dev" %}.{{ distance }}rc{% endif %}'
125125

126126
[tool.poetry-dynamic-versioning.substitution]
127127
files = ["*/__init__.py"]

0 commit comments

Comments
 (0)