2022-05-21 09:45:48 +00:00
|
|
|
#!/usr/bin/env python
|
2022-03-05 20:44:01 +00:00
|
|
|
"""Utility to extract Bitwarden hash for hashcat from Google Chrome / Firefox / Desktop local data"""
|
|
|
|
|
|
|
|
#
|
|
|
|
# Based on bitwarden2john.py https://github.com/willstruggle/john/blob/master/bitwarden2john.py
|
|
|
|
#
|
|
|
|
# Various data locations are documented here: https://bitwarden.com/help/data-storage/#on-your-local-machine
|
|
|
|
#
|
|
|
|
# Author: https://github.com/Greexter
|
|
|
|
# License: MIT
|
|
|
|
#
|
|
|
|
|
|
|
|
import os
|
|
|
|
import argparse
|
|
|
|
import sys
|
|
|
|
import base64
|
|
|
|
import traceback
|
|
|
|
|
|
|
|
try:
|
|
|
|
import json
|
|
|
|
assert json
|
|
|
|
except ImportError:
|
|
|
|
try:
|
|
|
|
import simplejson as json
|
|
|
|
except ImportError:
|
|
|
|
print("Please install json module which is currently not installed.\n", file=sys.stderr)
|
|
|
|
sys.exit(-1)
|
|
|
|
|
|
|
|
|
|
|
|
def process_sqlite(path):
|
|
|
|
try:
|
|
|
|
import snappy
|
|
|
|
except ImportError:
|
|
|
|
print("Please install python-snappy module.\n", file=sys.stderr)
|
|
|
|
sys.exit(-1)
|
|
|
|
try:
|
|
|
|
import sqlite3
|
|
|
|
except ImportError:
|
|
|
|
print("Please install sqlite3 module.\n", file=sys.stderr)
|
|
|
|
sys.exit(-1)
|
|
|
|
|
|
|
|
conn = sqlite3.connect(path)
|
|
|
|
cur = conn.cursor()
|
|
|
|
data = cur.execute('SELECT * FROM object_data')
|
|
|
|
fetched = data.fetchall()
|
|
|
|
|
|
|
|
# uses undocumented nonstandard data format
|
|
|
|
# probably can break in the future
|
|
|
|
dataValue = snappy.decompress(fetched[0][4])
|
|
|
|
|
|
|
|
key_hash = dataValue.split(b"keyHash")[1][9:53].decode()
|
|
|
|
email = dataValue.split(b"email")[1][11:].split(b'\x00')[0].decode()
|
|
|
|
iterations = int.from_bytes(dataValue.split(b"kdfIterations")[1][3:7], byteorder="little")
|
2022-03-06 09:50:04 +00:00
|
|
|
|
|
|
|
return [(email, key_hash, iterations)]
|
2022-03-05 20:44:01 +00:00
|
|
|
|
|
|
|
|
|
|
|
def process_leveldb(path):
|
|
|
|
try:
|
|
|
|
import leveldb
|
|
|
|
except ImportError:
|
2022-03-06 09:50:04 +00:00
|
|
|
print("Please install the leveldb module for full functionality!\n", file=sys.stderr)
|
|
|
|
sys.exit(-1)
|
2022-03-05 20:44:01 +00:00
|
|
|
|
|
|
|
db = leveldb.LevelDB(path, create_if_missing=False)
|
|
|
|
|
|
|
|
try:
|
2022-03-06 09:50:04 +00:00
|
|
|
out = []
|
|
|
|
accIds = db.Get(b'authenticatedAccounts')
|
|
|
|
accIds = json.loads(accIds)
|
|
|
|
|
|
|
|
for id in accIds:
|
|
|
|
authAccData = db.Get(id.strip('"').encode())
|
|
|
|
out.append(extract_json_profile(json.loads(authAccData)))
|
|
|
|
|
|
|
|
return out
|
2022-03-05 20:44:01 +00:00
|
|
|
except(KeyError):
|
|
|
|
# support for older Bitwarden versions (before account switch implementation)
|
|
|
|
# data is stored in different format
|
2022-03-06 09:50:04 +00:00
|
|
|
print("Failed to extract data, trying old format.", file=sys.stderr)
|
2022-03-05 20:44:01 +00:00
|
|
|
email = db.Get(b'userEmail')\
|
2022-03-06 09:50:04 +00:00
|
|
|
.decode('utf-8')\
|
|
|
|
.strip('"')
|
2022-03-05 20:44:01 +00:00
|
|
|
key_hash = db.Get(b'keyHash')\
|
2022-03-06 09:50:04 +00:00
|
|
|
.decode("ascii").strip('"')
|
2022-03-05 20:44:01 +00:00
|
|
|
iterations = int(db.Get(b'kdfIterations').decode("ascii"))
|
|
|
|
|
2022-03-06 09:50:04 +00:00
|
|
|
return [(email, key_hash, iterations)]
|
2022-03-05 20:44:01 +00:00
|
|
|
|
|
|
|
|
|
|
|
def process_json(data):
|
|
|
|
data = json.loads(data)
|
2022-03-06 09:50:04 +00:00
|
|
|
|
2022-03-05 20:44:01 +00:00
|
|
|
try:
|
2022-03-06 09:50:04 +00:00
|
|
|
out = []
|
|
|
|
accIds = data["authenticatedAccounts"]
|
|
|
|
for id in accIds:
|
|
|
|
authAccData = data[id.strip('"')]
|
|
|
|
out.append(extract_json_profile(authAccData))
|
|
|
|
|
|
|
|
return out
|
2022-03-05 20:44:01 +00:00
|
|
|
except(KeyError):
|
2022-03-06 09:50:04 +00:00
|
|
|
print("Failed to extract data, trying old format.", file=sys.stderr)
|
2022-03-05 20:44:01 +00:00
|
|
|
email = data["rememberedEmail"]
|
|
|
|
hash = data["keyHash"]
|
|
|
|
iterations = data["kdfIterations"]
|
|
|
|
|
2022-03-06 09:50:04 +00:00
|
|
|
return [(email, hash, iterations)]
|
|
|
|
|
|
|
|
|
|
|
|
def extract_json_profile(data):
|
|
|
|
profile = data["profile"]
|
|
|
|
email = profile["email"]
|
|
|
|
iterations = profile["kdfIterations"]
|
|
|
|
hash = profile["keyHash"]
|
2022-03-05 20:44:01 +00:00
|
|
|
return email, hash, iterations
|
|
|
|
|
|
|
|
|
|
|
|
def process_file(filename, legacy = False):
|
|
|
|
try:
|
|
|
|
if os.path.isdir(filename):
|
|
|
|
# Chromium based
|
2022-03-06 09:50:04 +00:00
|
|
|
data = process_leveldb(filename)
|
2022-03-05 20:44:01 +00:00
|
|
|
elif filename.endswith(".sqlite"):
|
|
|
|
# Firefox
|
2022-03-06 09:50:04 +00:00
|
|
|
data = process_sqlite(filename)
|
2022-03-05 20:44:01 +00:00
|
|
|
elif filename.endswith(".json"):
|
2022-06-20 13:19:01 +00:00
|
|
|
# json - Desktop
|
2022-03-05 20:44:01 +00:00
|
|
|
with open(filename, "rb") as f:
|
|
|
|
data = f.read()
|
2022-03-06 09:50:04 +00:00
|
|
|
data = process_json(data)
|
2022-03-05 20:44:01 +00:00
|
|
|
else:
|
|
|
|
print("Unknown storage. Don't know how to extract data.", file=sys.stderr)
|
|
|
|
sys.exit(-1)
|
|
|
|
|
|
|
|
except (ValueError, KeyError):
|
|
|
|
traceback.print_exc()
|
|
|
|
print("Missing values, user is probably logged out.", file=sys.stderr)
|
|
|
|
return
|
|
|
|
except:
|
|
|
|
traceback.print_exc()
|
|
|
|
return
|
|
|
|
|
|
|
|
iterations2 = 1 if legacy else 2
|
2022-03-06 09:50:04 +00:00
|
|
|
for entry in data:
|
|
|
|
if len(entry) != 3:
|
|
|
|
print("[error] %s could not be parsed properly!\nUser is probably logged out." % filename, file=sys.stderr)
|
|
|
|
continue
|
2022-03-05 20:44:01 +00:00
|
|
|
|
2022-03-06 09:50:04 +00:00
|
|
|
print("$bitwarden$2*%d*%d*%s*%s" %
|
|
|
|
(entry[2], iterations2, base64.b64encode(entry[0].encode("ascii")).decode("ascii"), entry[1]))
|
2022-03-05 20:44:01 +00:00
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
parser = argparse.ArgumentParser()
|
|
|
|
parser.add_argument("paths", type=str, nargs="+")
|
|
|
|
parser.add_argument("--legacy", action="store_true", help="Used for older versions of Bitwarden (before static iteration count had been changed).")
|
|
|
|
|
|
|
|
args = parser.parse_args()
|
|
|
|
|
|
|
|
for p in args.paths:
|
|
|
|
process_file(p, args.legacy)
|