diff --git a/changelog.md b/changelog.md index 36e3245d..72b2fecc 100644 --- a/changelog.md +++ b/changelog.md @@ -5,6 +5,23 @@ 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 +- Fixed a bug in `MSSQL` where SID translation failed when using Kerberos authentication + + +### 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 @@ -14,6 +31,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 + ## [1.0.4] - 2024-05-28 ### Fixed 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), diff --git a/lib/attacks/find.py b/lib/attacks/find.py index 24f939b7..f2c73224 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() @@ -154,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] @@ -221,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() @@ -229,6 +234,54 @@ 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 = [] + 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: + for entry in self.ldap_session.entries: + hostname = str(entry['dNSHostname']) + cursor.execute(f'''insert into PXEDistributionPoints (Hostname, SigningStatus, SCCM, WDS) values (?,?,?,?)''', + (hostname, '', '' , '')) + if 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') + cursor.close() + def check_strings(self): #now search for anything related to "SCCM" yeet = '(|(samaccountname=*sccm*)(samaccountname=*mecm*)(description=*sccm*)(description=*mecm*)(name=*sccm*)(name=*mecm*))' @@ -255,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) @@ -267,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: @@ -291,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() @@ -337,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 = '' @@ -386,6 +439,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 +447,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") 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.") 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