From e0042d3d9aba710163100e16177166f7a53a21dc Mon Sep 17 00:00:00 2001 From: Garrett Foster Date: Thu, 15 Aug 2024 18:14:34 -0400 Subject: [PATCH 1/8] updated find.py to query for PXE distribution points --- lib/attacks/find.py | 62 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/lib/attacks/find.py b/lib/attacks/find.py index 24f939b7..08fd2762 100644 --- a/lib/attacks/find.py +++ b/lib/attacks/find.py @@ -38,7 +38,7 @@ def run(self): return True def validate_tables(self): - table_names = ["CAS", "SiteServers", "ManagementPoints", "Users", "Groups", "Computers", "Creds"] + table_names = ["CAS", "SiteServers", "ManagementPoints", "DistributionPoints", "Users", "Groups", "Computers", "Creds"] try: for table_name in table_names: validated = self.conn.execute(f'''select name FROM sqlite_master WHERE type=\'table\' and name =\'{table_name}\' @@ -56,6 +56,7 @@ def build_tables(self): self.conn.execute('''CREATE TABLE CAS(SiteCode)''') self.conn.execute('''CREATE TABLE SiteServers(Hostname, SiteCode, CAS, SigningStatus, SiteServer, SMSProvider, Config, MSSQL)''') self.conn.execute('''CREATE TABLE ManagementPoints(Hostname, SiteCode, SigningStatus)''') + self.conn.execute('''CREATE TABLE PXEDistributionPoints(Hostname, SigningStatus, SCCM, WDS)''') self.conn.execute('''CREATE TABLE Users(cn, name, sAMAAccontName, servicePrincipalName, description)''') self.conn.execute('''CREATE TABLE Groups(cn, name, sAMAAccontName, member, description)''') self.conn.execute('''CREATE TABLE Computers(Hostname, SiteCode, SigningStatus, SiteServer, ManagementPoint, DistributionPoint, SMSProvider, WSUS, MSSQL)''') @@ -117,6 +118,8 @@ def run(self): self.search_base = get_dn(self.domain) #check for AD extension info self.check_schema() + #check for potential DPs + self.check_dps() #if they're using DNS only: thoughts and prayers self.check_strings() @@ -229,6 +232,60 @@ def check_mps(self): except ldap3.core.exceptions.LDAPObjectClassError as e: logger.info(f'[-] Could not find any Management Points published in LDAP') + + def check_dps(self): + #query for PXE enabled distribution points that are using Windows Deployment Services + #if the DP is using WDS a child intellimirror-scp class will be published under the computer object + #find all cases, parse the DN, then resolve the DN's hostname + logger.info(f'[*] Querying LDAP for potential PXE enabled distribution points') + cursor = self.conn.cursor() + potential_dps = [] + resolved_dps = [] + try: + self.ldap_session.extend.standard.paged_search(self.search_base, + "(cn=*-Remote-Installation-Services)", + attributes="distinguishedName", + controls=self.controls, + paged_size=500, + generator=False) + if self.ldap_session.entries: + logger.info(f"[+] Found {len(self.ldap_session.entries)} potential Distribution Points in LDAP.") + for entry in self.ldap_session.entries: + dn = str(entry['distinguishedName']) + if dn: + trim = dn.find(",") + trimmed = dn[trim + 1:] + potential_dps.append(trimmed) + + except ldap3.core.exceptions.LDAPObjectClassError as e: + logger.info(f'[-] Could not find any Distribution Points published in LDAP') + if potential_dps: + for dn in potential_dps: + try: + self.ldap_session.extend.standard.paged_search(self.search_base, + f"(distinguishedName={dn})", + attributes="dNSHostName", + controls=self.controls, + paged_size=500, + generator=False) + if self.ldap_session.entries: + logger.info(f"[+] Found {len(self.ldap_session.entries)} potential Distribution Points in LDAP.") + for entry in self.ldap_session.entries: + hostname = str(entry['dNSHostname']) + cursor.execute(f'''insert into PXEDistributionPoints (Hostname, SigningStatus, SCCM, WDS) values (?,?,?,?)''', + (hostname, '', '' , '')) + except ldap3.core.exceptions.LDAPObjectClassError as e: + logger.info(f'[-] Could not find any Distribution Points published in LDAP') + + + # self.add_computer_to_db(hostname) + # self.conn.commit() + # cursor.close() + # self.check_sites() + print(potential_dps) + print(resolved_dps) + + def check_strings(self): #now search for anything related to "SCCM" yeet = '(|(samaccountname=*sccm*)(samaccountname=*mecm*)(description=*sccm*)(description=*mecm*)(name=*sccm*)(name=*mecm*))' @@ -386,6 +443,7 @@ def recursive_resolution(self, dn): def results(self): tb_ss = dp.read_sql("SELECT * FROM SiteServers WHERE Hostname IS NOT 'Unknown' ", self.conn) tb_mp = dp.read_sql("SELECT * FROM ManagementPoints WHERE Hostname IS NOT 'Unknown' ", self.conn) + tb_dp = dp.read_sql("SELECT * FROM PXEDistributionPoints WHERE Hostname IS NOT 'Unknown' ", self.conn) tb_c = dp.read_sql("SELECT * FROM Computers WHERE Hostname IS NOT 'Unknown' ", self.conn) tb_u = dp.read_sql("SELECT * FROM Users", self.conn) tb_g = dp.read_sql("SELECT * FROM Groups", self.conn) @@ -393,6 +451,8 @@ def results(self): logger.info(tabulate(tb_ss, showindex=False, headers=tb_ss.columns, tablefmt='grid')) logger.info("Management Points Table") logger.info(tabulate(tb_mp, showindex=False, headers=tb_mp.columns, tablefmt='grid')) + logger.info("Potential PXE Distribution Points") + logger.info(tabulate(tb_dp, showindex=False, headers=tb_dp.columns, tablefmt='grid')) logger.info('Computers Table') logger.info(tabulate(tb_c, showindex=False, headers=tb_c.columns, tablefmt='grid')) logger.info("Users Table") From eb58e3754a7bd2423c49d6b38d57d9ae8b5537a1 Mon Sep 17 00:00:00 2001 From: Garrett Foster Date: Thu, 15 Aug 2024 18:18:04 -0400 Subject: [PATCH 2/8] removed junk print statements from check_dps function --- lib/attacks/find.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/lib/attacks/find.py b/lib/attacks/find.py index 08fd2762..d40b9f59 100644 --- a/lib/attacks/find.py +++ b/lib/attacks/find.py @@ -240,7 +240,6 @@ def check_dps(self): logger.info(f'[*] Querying LDAP for potential PXE enabled distribution points') cursor = self.conn.cursor() potential_dps = [] - resolved_dps = [] try: self.ldap_session.extend.standard.paged_search(self.search_base, "(cn=*-Remote-Installation-Services)", @@ -269,22 +268,16 @@ def check_dps(self): paged_size=500, generator=False) if self.ldap_session.entries: - logger.info(f"[+] Found {len(self.ldap_session.entries)} potential Distribution Points in LDAP.") for entry in self.ldap_session.entries: hostname = str(entry['dNSHostname']) cursor.execute(f'''insert into PXEDistributionPoints (Hostname, SigningStatus, SCCM, WDS) values (?,?,?,?)''', (hostname, '', '' , '')) + self.add_computer_to_db(hostname) + self.conn.commit() + except ldap3.core.exceptions.LDAPObjectClassError as e: logger.info(f'[-] Could not find any Distribution Points published in LDAP') - - - # self.add_computer_to_db(hostname) - # self.conn.commit() - # cursor.close() - # self.check_sites() - print(potential_dps) - print(resolved_dps) - + cursor.close() def check_strings(self): #now search for anything related to "SCCM" From e24fd397f93f6f976b8f8de1b78e9364ec6f94af Mon Sep 17 00:00:00 2001 From: Garrett Foster Date: Thu, 15 Aug 2024 18:26:05 -0400 Subject: [PATCH 3/8] fixed issue where site servers weren't being added to the computers table --- lib/attacks/find.py | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/lib/attacks/find.py b/lib/attacks/find.py index d40b9f59..f2c73224 100644 --- a/lib/attacks/find.py +++ b/lib/attacks/find.py @@ -157,9 +157,10 @@ def check_schema(self): if self.resolved_sids: cursor = self.conn.cursor() for result in set(self.resolved_sids): + print(type(result)) cursor.execute(f'''insert into SiteServers (Hostname, SiteCode, CAS, SigningStatus, SiteServer, Config, MSSQL) values (?,?,?,?,?,?,?)''', (result, '', '', '', 'True', '', '')) - #self.add_computer_to_db(result) + self.add_computer_to_db(result) self.conn.commit() cursor.execute('''SELECT COUNT (Hostname) FROM SiteServers''') count = cursor.fetchone()[0] @@ -224,7 +225,8 @@ def check_mps(self): self.mp_sitecodes.append(sitecode) cursor.execute(f'''insert into ManagementPoints (Hostname, SiteCode, SigningStatus) values (?,?,?)''', (hostname, sitecode, '')) - self.add_computer_to_db(hostname) + if hostname: + self.add_computer_to_db(hostname) self.conn.commit() cursor.close() self.check_sites() @@ -272,7 +274,8 @@ def check_dps(self): hostname = str(entry['dNSHostname']) cursor.execute(f'''insert into PXEDistributionPoints (Hostname, SigningStatus, SCCM, WDS) values (?,?,?,?)''', (hostname, '', '' , '')) - self.add_computer_to_db(hostname) + if hostname: + self.add_computer_to_db(hostname) self.conn.commit() except ldap3.core.exceptions.LDAPObjectClassError as e: @@ -305,7 +308,9 @@ def check_strings(self): self.add_user_to_db(entry) #add computer to db if (entry['sAMAccountType']) == 805306369: - self.add_computer_to_db(entry) + hostname = str(entry['dNSHostname']) + if hostname: + self.add_computer_to_db(hostname) #add group to db and then resolve members if (entry['sAMAccountType']) == 268435456: self.add_group_to_db(entry) @@ -317,7 +322,9 @@ def check_strings(self): if (result['sAMAccountType']) == 805306368: self.add_user_to_db(result) if (result['sAMAccountType']) == 805306369: - self.add_computer_to_db(result) + hostname = str(result['dNSHostname']) + if hostname: + self.add_computer_to_db(result) if (result['sAMAccountType']) == 268435456: self.add_group_to_db(result) except ldap3.core.exceptions.LDAPAttributeError as e: @@ -341,7 +348,9 @@ def check_all_computers(self): if self.ldap_session.entries: logger.info(f"[+] Found {len(self.ldap_session.entries)} computers in LDAP.") for entry in self.ldap_session.entries: - self.add_computer_to_db(entry) + hostname = str(entry['dNSHostname']) + if hostname: + self.add_computer_to_db(hostname) self.conn.commit() cursor.close() @@ -387,13 +396,7 @@ def add_user_to_db(self, entry): def add_computer_to_db(self, entry): cursor = self.conn.cursor() - if 'dNSHostName' in entry: - hostname = str(entry['dNSHostName']).lower() - elif ldap3.core.exceptions.LDAPKeyError: - # if no dnshostname attribute, skip it - return - else: - entry = entry + hostname = entry sitecode = '' signing = '' siteserver = '' From 6450d9be44ea1a3f5d5940fd083482841d654f8a Mon Sep 17 00:00:00 2001 From: Garrett Foster Date: Thu, 15 Aug 2024 18:42:51 -0400 Subject: [PATCH 4/8] added profiling for pxe enabled distribution points to smb.py --- lib/attacks/smb.py | 43 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/lib/attacks/smb.py b/lib/attacks/smb.py index 9cd0f8c5..f4ca963d 100644 --- a/lib/attacks/smb.py +++ b/lib/attacks/smb.py @@ -45,6 +45,7 @@ def run(self): #TODO add check to be sure FIND module was run self.check_siteservers() self.check_managementpoints() + self.check_distributionpoints() self.check_computers() self.conn.close() @@ -67,7 +68,7 @@ def check_siteservers(self): #only enumerate if the host is reachable conn = self.smb_connection(hostname) if conn: - signing, site_code, siteserv, distp, wsus = self.smb_hunter(hostname, conn) + signing, site_code, siteserv, distp, wsus, wdspxe, sccmpxe = self.smb_hunter(hostname, conn) #check if mssql is self hosted mssql = self.mssql_check(hostname) #check for SMS provider roles @@ -109,7 +110,7 @@ def check_managementpoints(self): hostname = i[0] conn = self.smb_connection(hostname) if conn: - signing, site_code, siteserv, distp, wsus = self.smb_hunter(hostname, conn) + signing, site_code, siteserv, distp, wsus, wdspxe, sccmpxe = self.smb_hunter(hostname, conn) cursor.execute(f'''Update ManagementPoints SET SigningStatus=? WHERE Hostname=?''', (str(signing), hostname)) self.conn.commit() @@ -121,6 +122,30 @@ def check_managementpoints(self): return else: logger.info("[-] No Management Points found in database.") + + def check_distributionpoints(self): + cursor = self.conn.cursor() + cursor.execute("SELECT Hostname FROM PXEDistributionPoints WHERE Hostname IS NOT 'Unknown'") + hostnames = cursor.fetchall() + if hostnames: + logger.info (f"Profiling {len(hostnames)} distribution points.") + for i in hostnames: + hostname = i[0] + conn = self.smb_connection(hostname) + if conn: + signing, site_code, siteserv, distp, wsus, wdspxe, sccmpxe = self.smb_hunter(hostname, conn) + #Hostname, SigningStatus, SCCM, WDS + cursor.execute(f'''Update PXEDistributionPoints SET SigningStatus=?, SCCM=?, WDS=? WHERE Hostname=?''', + (str(signing), str(sccmpxe), str(wdspxe), hostname)) + self.conn.commit() + + logger.info("[+] Finished profiling Distribution Points.") + cursor.close() + tb_dp = dp.read_sql("SELECT * FROM PXEDistributionPoints WHERE Hostname IS NOT 'Unknown' ", self.conn) + logger.info(tabulate(tb_dp, showindex=False, headers=tb_dp.columns, tablefmt='grid')) + return + else: + logger.info("[-] No Management Points found in database.") #read from computers table created from strings check in LDAP module def check_computers(self): @@ -136,7 +161,7 @@ def check_computers(self): mssql = self.mssql_check(hostname) mp = self.http_check(hostname) provider = self.provider_check(hostname) - signing, site_code, siteserv, distp, wsus = self.smb_hunter(hostname, conn) + signing, site_code, siteserv, distp, wsus, wdspxe, sccmpxe = self.smb_hunter(hostname, conn) if site_code == 'None': try: cursor.execute(f"SELECT SiteCode FROM ManagementPoints WHERE Hostname IS '{hostname}'") @@ -185,6 +210,8 @@ def smb_hunter(self, server, conn): siteserv = False distp = False wsus = False + wdspxe = False + sccmpxe = False signing = conn.isSigningRequired() shares = conn.listShares() @@ -212,16 +239,24 @@ def smb_hunter(self, server, conn): distp = False if "REMINST" in shares_dict: #list REMINST contents to check if the SMSTemp dir actually exists + remark = shares_dict.get("REMINST", '') + if "Windows Deployment Services Share" in remark: + wdspxe = True + if "RemoteInstallation" in remark: + sccmpxe = True check = conn.listPath(shareName="REMINST", path="//*") for i in check: if i.get_longname() == "SMSTemp": pxe_boot_servers.append(server) + if "WsusContent" in shares_dict: wsus = True + + if pxe_boot_servers: self.smb_spider(conn, pxe_boot_servers) - return signing, site_code, siteserv, distp, wsus + return signing, site_code, siteserv, distp, wsus, wdspxe, sccmpxe except socket.error: logger.info(socket.error) return From 23fc95d12a74c1499c5c04d685c53c944af40b14 Mon Sep 17 00:00:00 2001 From: Garrett Foster Date: Thu, 15 Aug 2024 18:51:25 -0400 Subject: [PATCH 5/8] added approver user check to fix #42 --- lib/attacks/cmpivot.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/attacks/cmpivot.py b/lib/attacks/cmpivot.py index 816d7aa3..071dfd91 100644 --- a/lib/attacks/cmpivot.py +++ b/lib/attacks/cmpivot.py @@ -296,6 +296,14 @@ def __init__(self, username=None, password=None, ip=None, debug=False, logs_dir= def run(self): try: endpoint = f"https://{self.url}/AdminService/wmi/" + if self.approve_user: + r = requests.request("GET", + endpoint, + auth=HttpNtlmAuth(self.approve_user, self.approve_password), + verify=False) + if r.status_code == 401: + logger.info("Got error code 401: Access Denied. Check your approver credentials.") + logger.info("Script execution will fail if approval is required.") r = requests.request("GET", endpoint, auth=HttpNtlmAuth(self.username, self.password), From 08c756012174f81f24b664ec59664d4ae76ed783 Mon Sep 17 00:00:00 2001 From: Garrett Foster Date: Thu, 15 Aug 2024 18:57:47 -0400 Subject: [PATCH 6/8] updated changelog --- changelog.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/changelog.md b/changelog.md index 36e3245d..68f4a07b 100644 --- a/changelog.md +++ b/changelog.md @@ -5,6 +5,22 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.0.6] - 2024-08-15 + +### Fixed + +- Fixed a bug where site servers weren't being added to the computers table causing further profiling to fail + + +### Added +- Find module + - Added distribution point check in LDAP +- SMB module + - Added distribution point profiling to determine if the found host is SCCM or WDS related +- Admin module + - Added "approver credentials" check to ensure credentials are valid when script approval is required for the hierarchy + ## [1.0.5] - 2024-06-9 ### Fixed From 92878406701e1a406dd2ed08279857247d826ef3 Mon Sep 17 00:00:00 2001 From: Garrett Foster Date: Fri, 23 Aug 2024 15:26:18 -0400 Subject: [PATCH 7/8] fixed bug for sid translation when using Kerbeos auth --- lib/attacks/mssql.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/lib/attacks/mssql.py b/lib/attacks/mssql.py index b95a909a..97d2311c 100644 --- a/lib/attacks/mssql.py +++ b/lib/attacks/mssql.py @@ -3,6 +3,8 @@ from impacket.ldap import ldaptypes import ldap3 from getpass import getpass +import json +from ldap3.protocol.formatters.formatters import format_sid """ @@ -83,14 +85,19 @@ def run(self): exit() if self.ldap_session.entries: for entry in self.ldap_session.entries: - sid = str(entry['objectsid']) - logger.debug(f"[+] Found {self.target_user} SID: {sid}") - #abusing MSSQL requires the hex SID of the owned account - #REF: https://thehacker.recipes/ad/movement/sccm-mecm#1.-retreive-the-controlled-user-sid - hexsid = ldaptypes.LDAP_SID() - hexsid.fromCanonical(sid) - self.querysid = ('0x' + ''.join('{:02X}'.format(b) for b in hexsid.getData())) - logger.info(f'[*] Converted {self.target_user} SID to {self.querysid}') + json_entry = json.loads(entry.entry_to_json()) + attributes = json_entry['attributes'].keys() + for attr in attributes: + if attr == "objectSid": + sid = format_sid(entry[attr].value) + print(sid) + logger.debug(f"[+] Found {self.target_user} SID: {sid}") + #abusing MSSQL requires the hex SID of the owned account + #REF: https://thehacker.recipes/ad/movement/sccm-mecm#1.-retreive-the-controlled-user-sid + hexsid = ldaptypes.LDAP_SID() + hexsid.fromCanonical(sid) + self.querysid = ('0x' + ''.join('{:02X}'.format(b) for b in hexsid.getData())) + logger.info(f'[*] Converted {self.target_user} SID to {self.querysid}') else: print("[-] Failed to resolve target SID.") From 60f4eb717477f767a1ad4db6089678cde7906955 Mon Sep 17 00:00:00 2001 From: Garrett Foster Date: Sat, 24 Aug 2024 22:10:55 -0400 Subject: [PATCH 8/8] fixed changelog --- changelog.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/changelog.md b/changelog.md index 68f4a07b..72b2fecc 100644 --- a/changelog.md +++ b/changelog.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Fixed a bug where site servers weren't being added to the computers table causing further profiling to fail +- Fixed a bug in `MSSQL` where SID translation failed when using Kerberos authentication ### Added @@ -30,6 +31,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 + ## [1.0.4] - 2024-05-28 ### Fixed