Skip to content

Instantly share code, notes, and snippets.

@teddziuba
Last active August 8, 2025 01:25
Show Gist options
  • Save teddziuba/3ff08bdda120d1f7822f3baf52e606c2 to your computer and use it in GitHub Desktop.
Save teddziuba/3ff08bdda120d1f7822f3baf52e606c2 to your computer and use it in GitHub Desktop.
Extract a Mac OSX Catalina user's password hash as a hashcat-compatible string
#!/usr/bin/env python3
"""
Mac OSX Catalina User Password Hash Extractor
Extracts a user's password hash as a hashcat-compatible string.
Mac OSX Catalina (10.15) uses a salted SHA-512 PBKDF2 for storing user passwords
(hashcat type 7100), and it's saved in an annoying binary-plist-nested-inside-xml-plist
format, so previously reported methods for extracting the hash don't work.
** You must be root to do this. **
Example Usage:
sudo ./osx_hash_extract.py <username> > hash.txt
hashcat -a 0 -m 7100 --username hash.txt wordlist.dat
"""
import plistlib
import sys
def read_user_plist(username):
plist_path = f"/var/db/dslocal/nodes/Default/users/{username}.plist"
with open(plist_path, "rb") as f:
plist = plistlib.load(f)
return plist
def extract_shadow_hash(user_plist):
# Nested binary plist
nested_bplist = user_plist["ShadowHashData"]
shadow_hash_plist = plistlib.loads(nested_bplist[0])
shadow = shadow_hash_plist["SALTED-SHA512-PBKDF2"]
pbkdf2 = {"iterations": shadow["iterations"],
"entropy": shadow["entropy"][:64].hex(), # Only the first 512 bits
"salt": shadow["salt"].hex()}
return pbkdf2
def format_hashcat(username, pbkdf2):
hc_line = f"{username}:$ml${pbkdf2['iterations']}${pbkdf2['salt']}${pbkdf2['entropy']}"
return hc_line
def main(args):
username = args[1]
user_plist = read_user_plist(username)
shadow = extract_shadow_hash(user_plist)
hc_input = format_hashcat(username, shadow)
print(hc_input)
if __name__ == "__main__":
main(sys.argv)
@sunknudsen
Copy link

Thanks for the script @teddziuba!

Can you please expand on https://gist.github.com/teddziuba/3ff08bdda120d1f7822f3baf52e606c2#file-osx_extract_hash-py-L42.

Only the first 512 bits

Why?

@richwrightnyc
Copy link

this is amazing. been looking for something like this for the longest, thank you so much!!

@jvictors-tp
Copy link

jvictors-tp commented Nov 10, 2023

After considerable debugging, I found it necessary to manually specify the format.
If you get OverflowError, InvalidFileException, or ExpatError, try flipping the format manually. Sometimes you may need

shadow_hash_plist = plistlib.loads(nested_bplist[0], fmt=plistlib.FMT_XML)

and other times you may need:

shadow_hash_plist = plistlib.loads(nested_bplist[0], fmt=plistlib.FMT_BINARY)

@jvictors-tp
Copy link

Thanks for the script @teddziuba!

Can you please expand on https://gist.github.com/teddziuba/3ff08bdda120d1f7822f3baf52e606c2#file-osx_extract_hash-py-L42.

Only the first 512 bits

Why?

I don't know, but I tested it against my own credentials, with my own password in the wordlist, and hashcat cracked it right away.

@moddedBear
Copy link

Thanks for this! I was able to get the hash from an OSX 10.7 Lion install with a few small changes.
https://gist.github.com/moddedBear/ee67bd9a8d6f77a7158875ca36b0b2d6

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment