Skip to content

Commit

Permalink
Merge pull request #63 from garrettfoster13/dev
Browse files Browse the repository at this point in the history
Dev
  • Loading branch information
garrettfoster13 authored Sep 14, 2024
2 parents b7a795e + 60f4eb7 commit bc4bd7c
Show file tree
Hide file tree
Showing 5 changed files with 149 additions and 25 deletions.
18 changes: 18 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
8 changes: 8 additions & 0 deletions lib/attacks/cmpivot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
82 changes: 69 additions & 13 deletions lib/attacks/find.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}\'
Expand All @@ -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)''')
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -221,14 +225,63 @@ 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()

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*))'
Expand All @@ -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)
Expand All @@ -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:
Expand All @@ -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()

Expand Down Expand Up @@ -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 = ''
Expand Down Expand Up @@ -386,13 +439,16 @@ 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)
logger.info("Site Servers Table")
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")
Expand Down
23 changes: 15 additions & 8 deletions lib/attacks/mssql.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


"""
Expand Down Expand Up @@ -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.")
Expand Down
43 changes: 39 additions & 4 deletions lib/attacks/smb.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -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
Expand Down Expand Up @@ -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()
Expand All @@ -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):
Expand All @@ -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}'")
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit bc4bd7c

Please sign in to comment.