Skip to content

Commit bbca423

Browse files
authored
Merge pull request Pennyw0rth#763 from Pennyw0rth/neff-add-entry-id-finder
Add module to find the entra-id sync server
2 parents f935807 + 20d6e31 commit bbca423

1 file changed

Lines changed: 83 additions & 0 deletions

File tree

nxc/modules/entra-id.py

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

0 commit comments

Comments
 (0)