From 393dfc3987b29eb84f1fa168044e37d0fe97ac7a Mon Sep 17 00:00:00 2001 From: zblurx Date: Wed, 22 Feb 2023 12:56:24 +0100 Subject: [PATCH 1/6] store domain backup key --- cme/protocols/smb.py | 57 ++++++++++++++++++++--------------- cme/protocols/smb/database.py | 46 ++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 25 deletions(-) diff --git a/cme/protocols/smb.py b/cme/protocols/smb.py index 320045ad..3e9dc90b 100755 --- a/cme/protocols/smb.py +++ b/cme/protocols/smb.py @@ -1180,6 +1180,7 @@ class smb(connection): if self.args.pvk is not None: try: self.pvkbytes = open(self.args.pvk, 'rb').read() + self.logger.success("Loading domain backupkey from {}".format(self.args.pvk)) except Exception as e: logging.error(str(e)) @@ -1191,31 +1192,37 @@ class smb(connection): self.logger.error(str(e)) if self.pvkbytes is None and self.no_da == None: - try: - dc_target = Target.create( - domain = self.domain, - username = self.username, - password = self.password, - target = self.domain, - lmhash = self.lmhash, - nthash = self.nthash, - do_kerberos = self.kerberos, - aesKey = self.aesKey, - no_pass = True, - use_kcache = self.use_kcache, - ) - dc_conn = DPLootSMBConnection(dc_target) - dc_conn.connect() # Connect to DC - if dc_conn.is_admin: - self.logger.success("User is Domain Administrator, exporting domain backupkey...") - backupkey_triage = BackupkeyTriage(target=dc_target, conn=dc_conn) - backupkey = backupkey_triage.triage_backupkey() - self.pvkbytes = backupkey.backupkey_v2 - else: - self.no_da = False - except Exception as e: - self.logger.debug("Could not get domain backupkey: {}".format(e)) - pass + results = self.db.get_domain_backupkey(self.domain) + if len(results) > 0: + self.logger.success("Loading domain backupkey from cmedb...") + self.pvkbytes = results[0][2] + else: + try: + dc_target = Target.create( + domain = self.domain, + username = self.username, + password = self.password, + target = self.domain, + lmhash = self.lmhash, + nthash = self.nthash, + do_kerberos = self.kerberos, + aesKey = self.aesKey, + no_pass = True, + use_kcache = self.use_kcache, + ) + dc_conn = DPLootSMBConnection(dc_target) + dc_conn.connect() # Connect to DC + if dc_conn.is_admin: + self.logger.success("User is Domain Administrator, exporting domain backupkey...") + backupkey_triage = BackupkeyTriage(target=dc_target, conn=dc_conn) + backupkey = backupkey_triage.triage_backupkey() + self.pvkbytes = backupkey.backupkey_v2 + self.db.add_domain_backupkey(self.domain, self.pvkbytes) + else: + self.no_da = False + except Exception as e: + self.logger.debug("Could not get domain backupkey: {}".format(e)) + pass target = Target.create( domain = self.domain, diff --git a/cme/protocols/smb/database.py b/cme/protocols/smb/database.py index 8d2fa275..780f9f33 100755 --- a/cme/protocols/smb/database.py +++ b/cme/protocols/smb/database.py @@ -76,6 +76,13 @@ class database: UNIQUE(computerid, userid, name) )''') + db_conn.execute('''CREATE TABLE "dpapi_backupkey" ( + "id" integer PRIMARY KEY, + "domain" text, + "pvk" text, + UNIQUE(domain) + )''') + #db_conn.execute('''CREATE TABLE "ntds_dumps" ( # "id" integer PRIMARY KEY, # "computerid", integer, @@ -537,3 +544,42 @@ class database: cur.close() logging.debug('get_groups(filterTerm={}, groupName={}, groupDomain={}) => {}'.format(filterTerm, groupName, groupDomain, results)) return results + + def add_domain_backupkey(self, domain:str, pvk:bytes): + """ + Add domain backupkey + :domain is the domain fqdn + :pvk is the domain backupkey + """ + cur = self.conn.cursor() + + cur.execute("SELECT * FROM dpapi_backupkey WHERE LOWER(domain)=LOWER(?)", [domain]) + results = cur.fetchall() + + if not len(results): + import base64 + pvk_encoded = base64.b64encode(pvk) + cur.execute("INSERT INTO dpapi_backupkey (domain, pvk) VALUES (?,?)", [domain, pvk_encoded]) + + cur.close() + + logging.debug('add_domain_backupkey(domain={}, pvk={}[...]) => {}'.format(domain, pvk_encoded[:20], cur.lastrowid)) + + def get_domain_backupkey(self, domain:str = None): + """ + Get domain backupkey + :domain is the domain fqdn + """ + cur = self.conn.cursor() + + if domain is not None: + cur.execute("SELECT * FROM dpapi_backupkey WHERE LOWER(domain)=LOWER(?)", [domain]) + else: + cur.execute("SELECT * FROM dpapi_backupkey", [domain]) + results = cur.fetchall() + cur.close() + logging.debug('get_domain_backupkey(domain={}) => {}'.format(domain, results)) + if len(results) >0: + import base64 + results = [(idkey, domain, base64.b64decode(pvk)) for idkey, domain, pvk in results] + return results \ No newline at end of file From 7bfdd0a75d8f19586aa6420467a6cd170d060557 Mon Sep 17 00:00:00 2001 From: zblurx Date: Wed, 22 Feb 2023 13:58:53 +0100 Subject: [PATCH 2/6] store dpapi secrets in cmedb --- cme/protocols/smb.py | 8 +++-- cme/protocols/smb/database.py | 60 ++++++++++++++++++++++++++++++- cme/protocols/smb/db_navigator.py | 46 ++++++++++++++++++++++++ 3 files changed, 110 insertions(+), 4 deletions(-) diff --git a/cme/protocols/smb.py b/cme/protocols/smb.py index 3e9dc90b..d622872e 100755 --- a/cme/protocols/smb.py +++ b/cme/protocols/smb.py @@ -1274,9 +1274,10 @@ class smb(connection): self.logger.debug("Error while looting credentials: {}".format(e)) for credential in credentials: self.logger.highlight("[%s][CREDENTIAL] %s - %s:%s" % (credential.winuser, credential.target, credential.username, credential.password)) + self.db.add_dpapi_secrets(target.address, 'CREDENTIAL', credential.winuser, credential.username, credential.password, credential.target) for credential in system_credentials: self.logger.highlight("[SYSTEM][CREDENTIAL] %s - %s:%s" % (credential.target, credential.username, credential.password)) - + self.db.add_dpapi_secrets(target.address, 'CREDENTIAL', 'SYSTEM', credential.username, credential.password, credential.target) try: # Collect Chrome Based Browser stored secrets @@ -1286,6 +1287,7 @@ class smb(connection): self.logger.debug("Error while looting browsers: {}".format(e)) for credential in browser_credentials: self.logger.highlight("[%s][%s] %s %s:%s" % (credential.winuser, credential.browser.upper(), credential.url+' -' if credential.url!= '' else '-', credential.username, credential.password)) + self.db.add_dpapi_secrets(target.address, credential.browser.upper(), credential.winuser, credential.username, credential.password, credential.url) try: # Collect User Internet Explorer stored secrets @@ -1296,7 +1298,7 @@ class smb(connection): for vault in vaults: if vault.type == 'Internet Explorer': self.logger.highlight("[%s][IEX] %s - %s:%s" % (vault.winuser, vault.resource+' -' if vault.resource!= '' else '-', vault.username, vault.password)) - + self.db.add_dpapi_secrets(target.address, 'IEX', vault.winuser, vault.username, vault.password, vault.resource) try: # Collect Firefox stored secrets @@ -1306,7 +1308,7 @@ class smb(connection): self.logger.debug("Error while looting firefox: {}".format(e)) for credential in firefox_credentials: self.logger.highlight("[%s][FIREFOX] %s %s:%s" % (credential.winuser, credential.url+' -' if credential.url!= '' else '-', credential.username, credential.password)) - + self.db.add_dpapi_secrets(target.address, 'FIREFOX', credential.winuser, credential.username, credential.password, credential.url) @requires_admin def lsa(self): diff --git a/cme/protocols/smb/database.py b/cme/protocols/smb/database.py index 780f9f33..e3c9e8e8 100755 --- a/cme/protocols/smb/database.py +++ b/cme/protocols/smb/database.py @@ -76,6 +76,17 @@ class database: UNIQUE(computerid, userid, name) )''') + db_conn.execute('''CREATE TABLE "dpapi_secrets" ( + "id" integer PRIMARY KEY, + "computer" text, + "dpapi_type" text, + "windows_user" text, + "username" text, + "password" text, + "url" text, + UNIQUE(computer, dpapi_type, windows_user, username, password, url) + )''') + db_conn.execute('''CREATE TABLE "dpapi_backupkey" ( "id" integer PRIMARY KEY, "domain" text, @@ -563,7 +574,7 @@ class database: cur.close() - logging.debug('add_domain_backupkey(domain={}, pvk={}[...]) => {}'.format(domain, pvk_encoded[:20], cur.lastrowid)) + logging.debug('add_domain_backupkey(domain={}, pvk={}) => {}'.format(domain, pvk_encoded, cur.lastrowid)) def get_domain_backupkey(self, domain:str = None): """ @@ -582,4 +593,51 @@ class database: if len(results) >0: import base64 results = [(idkey, domain, base64.b64decode(pvk)) for idkey, domain, pvk in results] + return results + + def is_dpapi_secret_valid(self, dpapiSecretID): + """ + Check if this group ID is valid. + :dpapiSecretID is a primary id + """ + cur = self.conn.cursor() + cur.execute('SELECT * FROM dpapi_secrets WHERE id=? LIMIT 1', [dpapiSecretID]) + results = cur.fetchall() + cur.close() + + logging.debug('is_dpapi_secret_valid(groupID={}) => {}'.format(dpapiSecretID, True if len(results) else False)) + return len(results) > 0 + + def add_dpapi_secrets(self, computer:str, dpapi_type:str, windows_user:str, username:str, password:str, url:str=''): + """ + Add dpapi secrets to cmedb + """ + cur = self.conn.cursor() + cur.execute("INSERT OR IGNORE INTO dpapi_secrets (computer, dpapi_type, windows_user, username, password, url) VALUES (?,?,?,?,?,?)", [computer, dpapi_type, windows_user, username, password, url]) + cur.close() + + logging.debug('add_dpapi_secrets(computer={}, dpapi_type={}, windows_user={}, username={}, password={}, url={}) => {}'.format(computer, dpapi_type, windows_user, username, password, url, cur.lastrowid)) + + def get_dpapi_secrets(self, filterTerm=None, computer:str=None, dpapi_type:str=None, windows_user:str=None, username:str=None, url:str=None): + """ + Get dpapi secrets from cmedb + """ + cur = self.conn.cursor() + if self.is_dpapi_secret_valid(filterTerm): + cur.execute("SELECT * FROM dpapi_secrets WHERE id=? LIMIT 1", [filterTerm]) + elif computer: + cur.execute("SELECT * FROM dpapi_secrets WHERE computer=? LIMIT 1", [computer]) + elif dpapi_type: + cur.execute('SELECT * FROM dpapi_secrets WHERE LOWER(dpapi_type)=LOWER(?)', [dpapi_type]) + elif windows_user: + cur.execute('SELECT * FROM dpapi_secrets WHERE LOWER(windows_user) LIKE LOWER(?)', [windows_user]) + elif username: + cur.execute('SELECT * FROM dpapi_secrets WHERE LOWER(windows_user) LIKE LOWER(?)', [username]) + elif url: + cur.execute('SELECT * FROM dpapi_secrets WHERE LOWER(url)=LOWER(?)', [url]) + else: + cur.execute("SELECT * FROM dpapi_secrets") + results = cur.fetchall() + cur.close() + logging.debug('get_dpapi_secrets(filterTerm={}, computer={}, dpapi_type={}, windows_user={}, username={}, url={}) => {}'.format(filterTerm, computer, dpapi_type, windows_user, username, url, results)) return results \ No newline at end of file diff --git a/cme/protocols/smb/db_navigator.py b/cme/protocols/smb/db_navigator.py index 60d066a7..9d6470fc 100644 --- a/cme/protocols/smb/db_navigator.py +++ b/cme/protocols/smb/db_navigator.py @@ -276,6 +276,52 @@ class navigator(DatabaseNavigator): self.print_table(data, title='Credential(s) with Admin Access') + def do_dpapi(self, line): + filterTerm = line.strip() + + if filterTerm == "": + secrets = self.db.get_dpapi_secrets() + secrets.insert(0,["ID","Host", "DPAPI Type", "Windows User", "Username", "Password", "URL"]) + self.print_table(secrets, title='DPAPI Secrets') + elif filterTerm.split()[0].lower() == "browser": + secrets = self.db.get_dpapi_secrets(dpapi_type="MSEDGE") + secrets += self.db.get_dpapi_secrets(dpapi_type="GOOGLE CHROME") + secrets += self.db.get_dpapi_secrets(dpapi_type="IEX") + secrets += self.db.get_dpapi_secrets(dpapi_type="FIREFOX") + if len(secrets) > 0: + secrets.insert(0,["Host", "DPAPI Type", "Windows User", "Username", "Password", "URL"]) + self.print_table(secrets, title='DPAPI Secrets') + elif filterTerm.split()[0].lower() == "chrome": + secrets = self.db.get_dpapi_secrets(dpapi_type="GOOGLE CHROME") + if len(secrets) > 0: + secrets.insert(0,["ID","Host", "DPAPI Type", "Windows User", "Username", "Password", "URL"]) + self.print_table(secrets, title='DPAPI Secrets') + elif filterTerm.split()[0].lower() == "msedge": + secrets = self.db.get_dpapi_secrets(dpapi_type="MSEDGE") + if len(secrets) > 0: + secrets.insert(0,["ID","Host", "DPAPI Type", "Windows User", "Username", "Password", "URL"]) + self.print_table(secrets, title='DPAPI Secrets') + elif filterTerm.split()[0].lower() == "credentials": + secrets = self.db.get_dpapi_secrets(dpapi_type="CREDENTIAL") + if len(secrets) > 0: + secrets.insert(0,["ID","Host", "DPAPI Type", "Windows User", "Username", "Password", "URL"]) + self.print_table(secrets, title='DPAPI Secrets') + elif filterTerm.split()[0].lower() == "iex": + secrets = self.db.get_dpapi_secrets(dpapi_type="IEX") + if len(secrets) > 0: + secrets.insert(0,["ID","Host", "DPAPI Type", "Windows User", "Username", "Password", "URL"]) + self.print_table(secrets, title='DPAPI Secrets') + elif filterTerm.split()[0].lower() == "firefox": + secrets = self.db.get_dpapi_secrets(dpapi_type="FIREFOX") + if len(secrets) > 0: + secrets.insert(0,["ID","Host", "DPAPI Type", "Windows User", "Username", "Password", "URL"]) + self.print_table(secrets, title='DPAPI Secrets') + else: + secrets = self.db.get_dpapi_secrets(filterTerm=filterTerm) + if len(secrets) > 0: + secrets.insert(0,["ID","Host", "DPAPI Type", "Windows User", "Username", "Password", "URL"]) + self.print_table(secrets, title='DPAPI Secrets') + def do_creds(self, line): filterTerm = line.strip() From bf254506d017ba1d3fb5b24f65786ff39e244983 Mon Sep 17 00:00:00 2001 From: zblurx Date: Wed, 22 Feb 2023 14:02:36 +0100 Subject: [PATCH 3/6] fix column for cme dpapi browser --- cme/protocols/smb/db_navigator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cme/protocols/smb/db_navigator.py b/cme/protocols/smb/db_navigator.py index 9d6470fc..e8fd751a 100644 --- a/cme/protocols/smb/db_navigator.py +++ b/cme/protocols/smb/db_navigator.py @@ -289,7 +289,7 @@ class navigator(DatabaseNavigator): secrets += self.db.get_dpapi_secrets(dpapi_type="IEX") secrets += self.db.get_dpapi_secrets(dpapi_type="FIREFOX") if len(secrets) > 0: - secrets.insert(0,["Host", "DPAPI Type", "Windows User", "Username", "Password", "URL"]) + secrets.insert(0,["ID","Host", "DPAPI Type", "Windows User", "Username", "Password", "URL"]) self.print_table(secrets, title='DPAPI Secrets') elif filterTerm.split()[0].lower() == "chrome": secrets = self.db.get_dpapi_secrets(dpapi_type="GOOGLE CHROME") From 26e0393a31b2ed0c0e4e163910a5aa2a67ebb6cc Mon Sep 17 00:00:00 2001 From: mpgn Date: Wed, 22 Feb 2023 14:41:58 -0500 Subject: [PATCH 4/6] Add check to verify if latest version of cmedb is used --- cme/protocols/smb.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cme/protocols/smb.py b/cme/protocols/smb.py index d622872e..52fb6f9a 100755 --- a/cme/protocols/smb.py +++ b/cme/protocols/smb.py @@ -1192,7 +1192,11 @@ class smb(connection): self.logger.error(str(e)) if self.pvkbytes is None and self.no_da == None: - results = self.db.get_domain_backupkey(self.domain) + try: + results = self.db.get_domain_backupkey(self.domain) + except: + self.logger.error("Your version of CMEDB is not up to date, run cmedb and create a new workspace: 'workspace create dpapi' then re-run the dpapi option") + return False if len(results) > 0: self.logger.success("Loading domain backupkey from cmedb...") self.pvkbytes = results[0][2] From 9ca90fceccb49a6ad769fafce7a240f7a5f6cd09 Mon Sep 17 00:00:00 2001 From: mpgn Date: Thu, 23 Feb 2023 04:38:44 -0500 Subject: [PATCH 5/6] is_admin check wrong --- cme/protocols/smb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cme/protocols/smb.py b/cme/protocols/smb.py index 5d429018..caad8f3b 100755 --- a/cme/protocols/smb.py +++ b/cme/protocols/smb.py @@ -1216,7 +1216,7 @@ class smb(connection): ) dc_conn = DPLootSMBConnection(dc_target) dc_conn.connect() # Connect to DC - if dc_conn.is_admin: + if dc_conn.is_admin(): self.logger.success("User is Domain Administrator, exporting domain backupkey...") backupkey_triage = BackupkeyTriage(target=dc_target, conn=dc_conn) backupkey = backupkey_triage.triage_backupkey() From 868a5a509e047d47cc5acc22efc6912c8f59d392 Mon Sep 17 00:00:00 2001 From: zblurx Date: Fri, 24 Feb 2023 13:44:04 +0100 Subject: [PATCH 6/6] upgrade dploot --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 2cb82e32..160dbdbd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,7 @@ aioconsole = "^0.3.3" pywerview = "^0.3.3" minikerberos = "0.3.5" aardwolf = "0.2.5" -dploot = "^2.1.14" +dploot = "^2.1.16" bloodhound = "^1.6.1" asyauth = "^0.0.12" masky = "^0.2.0"