Add lastpass2hashcat.py

This commit is contained in:
hansvh 2022-10-21 14:49:59 +02:00 committed by Lars Sætaberget
parent f40dc401bc
commit b2cd1d1afb
1 changed files with 166 additions and 0 deletions

166
tools/lastpass2hashcat.py Executable file
View File

@ -0,0 +1,166 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Author: hansvh <6390369+hans-vh@users.noreply.github.com>
# Version: 0.0.6
# License: MIT
"""
Files can be found here:
Android: /data/data/com.lastpass.lpandroid/files
Others: See https://support.lastpass.com/help/where-is-my-lastpass-data-stored-on-my-computer-lp070008
Tested OK with:
- LastPass for Android (com.lastpass.lpandroid) v5.12.0.10004
- LastPass for Chrome v4.101.1
- LastPass for Opera v4.101.1
- LastPass for Firefox v4.101.0
"""
import sys
import os
import sqlite3
from base64 import b64decode
from re import search
def parse_encu(data):
"""Parse ENCU and return IV and AES-256-CBC encrypted email to compare against"""
data = data.decode("utf-8")
initialization_vector = None
encrypted_email = None
try:
# Format: ![B64]|[B64]
result = search(r"^!(.*)\|(.*)$", data)
initialization_vector = result.group(1)
encrypted_email = result.group(2)
initialization_vector = b64decode(initialization_vector).hex()
encrypted_email = b64decode(encrypted_email).hex()
except:
# B64 Only. This implies EBC, not CBC, mode and IV is found elsewhere, e.g., in database
encrypted_email = b64decode(data).hex()
return initialization_vector, encrypted_email
def open_file(file_name):
"""Open file and return contents"""
with open(file_name, "rb") as file_handle:
return file_handle.read()
def parse_vault(xml):
"""Parse Vault according to format: 4 bytes ASCII identifier, 4 bytes size, size bytes data"""
magic_bytes = xml[:4].decode("utf-8")
if magic_bytes != "LPAV":
sys.exit(f"Expected LPAV in base 64 decoded XML, but found {magic_bytes}")
offset = 0
while offset < len(xml):
identifier = xml[offset:offset + 4].decode("utf-8")
offset = offset + 4
size = int.from_bytes(xml[offset:offset + 4], byteorder='big')
offset = offset + 4
data = xml[offset:offset + size]
if identifier == 'ENCU':
initialization_vector, encrypted_email = parse_encu(data)
return initialization_vector, encrypted_email
offset = offset + size
return None, None
def sqlite_parse_chromium(cur):
"""Chrome and Opera"""
iterations = -1
xml = ""
try:
res = cur.execute("SELECT data FROM LastPassData WHERE type='accts'")
(xml,) = res.fetchone()
result = search(r"^iterations=(\d+);(.*)$", xml)
iterations = result.group(1)
xml = result.group(2)
xml = b64decode(xml)
except:
return None, None
return iterations, xml
def sqlite_parse_firefox(cur):
"""Firefox"""
iterations = -1
encu = ""
try:
res = cur.execute("SELECT value FROM data WHERE key LIKE '%sch'")
encu, = res.fetchone()
encu = encu.decode("utf-8")
encu = encu[encu.find("!"):]
encu = encu[:encu.find("\n")]
encu = bytes(encu, "utf-8")
res = cur.execute("SELECT value FROM data WHERE key LIKE '%key_iter'")
iterations, = res.fetchone()
iterations = int(iterations)
except:
return None, None
return iterations, encu
def main():
"""Entry point"""
if len(sys.argv) < 3:
sys.exit(f"Usage: {sys.argv[0]} <xml or sqlite file> <username (email)>")
file_name = sys.argv[1]
if not os.path.exists(file_name):
sys.exit(f"File {file_name} does not exist")
file_content = open_file(file_name)
magic_bytes = file_content[:5].decode("utf-8")
# Output will contain the following fields (in order), colon separated
encrypted_email = ""
iterations = -1
email = sys.argv[2].lower()
initialization_vector = ""
if magic_bytes == "LPB64":
# Android App
iterations = 100100
xml = b64decode(file_content[5:])
initialization_vector, encrypted_email = parse_vault(xml)
elif magic_bytes == "SQLit":
# Browser Extension
con = sqlite3.connect(file_name)
cur = con.cursor()
# First try Chromium based browsers
iterations, xml = sqlite_parse_chromium(cur)
if iterations and xml:
initialization_vector, encrypted_email = parse_vault(xml)
# Then try Firefox
if not encrypted_email or not iterations or not initialization_vector:
iterations, encu = sqlite_parse_firefox(cur)
initialization_vector, encrypted_email = parse_encu(encu)
# Finally give up
if not encrypted_email or not iterations or not initialization_vector:
sys.exit("Unexpected behaviour in SQLite database parsing")
con.close()
else:
sys.exit(f"Expected LPB64 or SQLit in file, but found {magic_bytes}")
print(f"{encrypted_email}:{iterations}:{email}:{initialization_vector}")
if __name__ == "__main__":
main()