Skip to content

Commit e2a7d5c

Browse files
committed
Add module to find the entra-id sync server
1 parent e446d36 commit e2a7d5c

1 file changed

Lines changed: 78 additions & 0 deletions

File tree

nxc/modules/entra-id.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
2+
import re
3+
from nxc.parsers.ldap_results import parse_result_attributes
4+
5+
6+
class NXCModule:
7+
"""Module by @NeffIsBack"""
8+
9+
name = "entra-id"
10+
description = "Find the Entra ID sync server"
11+
supported_protocols = ["ldap"]
12+
opsec_safe = True
13+
multiple_hosts = True
14+
15+
def __init__(self):
16+
self.context = None
17+
self.module_options = None
18+
19+
def options(self, context, module_options):
20+
"""No options available."""
21+
22+
def on_login(self, context, connection):
23+
self.context = context
24+
25+
# For every Entra ID syncronization server, there is a corresponding MSOL_ account and likely an ADSyncMSA service account.
26+
msol_parsed = parse_result_attributes(connection.search(
27+
searchFilter="(sAMAccountName=MSOL_*)",
28+
attributes=["sAMAccountName", "cn", "description"],
29+
))
30+
adsync_parsed = parse_result_attributes(connection.search(
31+
searchFilter="(sAMAccountName=ADSyncMSA*)",
32+
attributes=["sAMAccountName", "cn", "msDS-HostServiceAccountBL"],
33+
))
34+
35+
hosts = []
36+
for acc in msol_parsed:
37+
host = re.search(r"computer (?P<host>.*) configured", acc["description"])
38+
if host:
39+
hostname = host.group("host")
40+
# Try to get the dNSHostName for the host, if not use the NetBIOS name from the description
41+
resp = parse_result_attributes(connection.search(f"(sAMAccountName={hostname}$)", ["dNSHostName"]))
42+
ip = connection.resolver(resp[0]["dNSHostName"] if resp else hostname)
43+
44+
hosts.append({
45+
"hostname": hostname,
46+
"ip": ip,
47+
"msol_account": acc["sAMAccountName"],
48+
})
49+
50+
for adsync in adsync_parsed:
51+
# The last 5 chars of the ADSyncMSA account name are the identifier for the corresponding MSOL account
52+
identifier = str(adsync["cn"]).removeprefix("ADSyncMSA")
53+
msol_acc = next((x for x in msol_parsed if str(x["sAMAccountName"]).startswith(f"MSOL_{identifier}")), None)
54+
self.context.log.debug(f"Found ADSyncMSA account: {adsync['sAMAccountName']}, corresponding MSOL account: {msol_acc['sAMAccountName'] if msol_acc else 'None'}")
55+
56+
# Get the computer object for the ADSyncMSA service account
57+
computer = parse_result_attributes(connection.search(
58+
searchFilter=f"(distinguishedName={adsync['msDS-HostServiceAccountBL']})",
59+
attributes=["dNSHostName", "cn", "sAMAccountName"],
60+
))[0]
61+
62+
# If we already found a host with its MSOL account, extend that info, otherwise create a new host entry
63+
host = next((x for x in hosts if x["hostname"] == computer["cn"]), None)
64+
if host and host["ip"]: # Skip if host and IP are already set
65+
self.context.log.debug(f"Host '{host['hostname']}' already exists with IP {host['ip'].get('host')}, skipping update.")
66+
continue
67+
elif host: # If host exists but IP is not set, update it
68+
host["ip"] = connection.resolver(computer["dNSHostName"])
69+
else: # If host does not exist, create a new entry
70+
hosts.append({
71+
"hostname": computer["cn"],
72+
"ip": connection.resolver(computer["dNSHostName"]),
73+
})
74+
75+
if hosts:
76+
self.context.log.success("Found Entra ID sync servers:")
77+
for host in hosts:
78+
self.context.log.highlight(f"{host['hostname']}: {host['ip'].get('host', '<not found>')} (MSOL Account: {host.get('msol_account', 'N/A')})")

0 commit comments

Comments
 (0)