#!/usr/bin/perl -w # # XPWDUMP - Mac OS X password hash dumping tool # Copyright (c) 2008 by Jen Linkova aka Furry # Copyright (c) 2008 by Solar Designer # Redistribution and use in source and binary forms, with or without # modification, are permitted. # There's ABSOLUTELY NO WARRANTY, express or implied. # (This is a heavily cut-down "BSD license".) # use strict; ################ ### DSCL command line options, output formats, etc. my $DSCL = "/usr/bin/dscl"; my $HASH_DIR = "/private/var/db/shadow/hash"; my @DSCL_GET_USERS = qw(. -list /Users); my @DSCL_GET = qw(. -read /Users); my $DSCL_GET_USER = 2; my $DSCL_ARGS_AUTH = "AuthenticationAuthority"; my $DSCL_ARGS_AUTH_ERR = "No such key: $DSCL_ARGS_AUTH"; my @DSCL_ARGS_USERINFO = qw(GeneratedUID UniqueID PrimaryGroupID RealName NFSHomeDirectory UserShell); my $DISABLED_USER = "DisabledUser"; my $AUTH_HINT = "AuthenticationHint"; ################ ### Supported hash types my $DEFAULT_HASH = "SALTED-SHA1"; ### Password hints are added to Real Name field. ### ' - ' is used as a delimiter my $HINT_DELIMITER = ' - '; ### UID = GID = 1 should be used if the actual values are unknown ### (in case of dumping from hash files provided) my $ID_PLACEHOLDER = 1; ### The offset in hash file, hash length, and CLI option for each supported ### hash type. "0", "0" means "not supported yet". my %SUPPORTED_HASHES = ( "SALTED-SHA1" => [ "168", "48", "-salted" ], "SHA1" => [ "104", "40", "-sha" ], "SMB-NT" => [ "0", "32", "-nt" ], "SMB-LAN-MANAGER" => [ "32", "32", "-lan" ], "CRAM-MD5" => [ "0", "0" ], "RECOVERABLE" => [ "0", "0" ], "SECURE" => [ "0", "0" ] ); ### To add/change CLI options for new hash types easily my %OPTIONS = ( "-salted" => "SALTED-SHA1", "-sha" => "SHA1", "-nt" => "SMB-NT", "-lan" => "SMB-LAN-MANAGER", ); ################ ### A few global vars my @USERNAMES; my $REQ_HASH; my $REAL_USERS; my $DISABLED = 0; my $HINT = 1; my $SANITY = 0; my $DUMP_FROM_FILE = 0; my $user = 0; ################ Subroutines ### ### Print usage instructions and exit ### sub usage { my $exitcode = shift; my $opt; print STDERR <) { s/\s*$//; ### dscl output is in "Attribute: value" format if (/^\s*(.+):\s+(.+)$/) { $userinfo{$1} = $2; ### In some cases the output could contain new-line ### Attribute: ### value } elsif (/^\s*(.+):$/) { $attr = $1; } elsif ($attr) { s/^\s*//; $userinfo{$attr} = $_; undef $attr; } } close(PIPE); ### Sanitize an password hint, if any $userinfo{$AUTH_HINT} = sanitize($userinfo{$AUTH_HINT}) if ($HINT && defined($userinfo{$AUTH_HINT})); return \%userinfo; } ### In case of dumping from files, ### filename is used as username and realname for the pseudo-user sub get_pseudouser_info { my $user = shift; my %userinfo; #UID & GID $userinfo{$DSCL_ARGS_USERINFO[1]} = $userinfo{$DSCL_ARGS_USERINFO[2]} = $ID_PLACEHOLDER; #Real Name $userinfo{$DSCL_ARGS_USERINFO[3]} = sanitize($user); #Homedir and shell are empty $userinfo{$DSCL_ARGS_USERINFO[4]} = $userinfo{$DSCL_ARGS_USERINFO[5]} = ""; return \%userinfo; } ### Get a list of users sub get_users_list { my $user; my @all_users; my %real_users; open(PIPE, "-|", $DSCL, @DSCL_GET_USERS) || die "Cannot run $DSCL program: $!"; @all_users = ; close(PIPE); ### Get a list of users who can log in (they have AuthenticationAuthority ### attribute set to list of hashes). foreach $user (@all_users) { my @dscl_get_loc = @DSCL_GET; my @auth_auth; my $p_to_hashtypes; $user =~ s/\s*$//; $dscl_get_loc[$DSCL_GET_USER] .= "/$user"; open(PIPE, "-|", $DSCL, @dscl_get_loc, $DSCL_ARGS_AUTH) || die "Cannot run $DSCL program: $!"; @auth_auth = ; close(PIPE); next unless $p_to_hashtypes = get_hashtypes($user, @auth_auth); $real_users{$user} = $p_to_hashtypes; } return \%real_users; } ### ### Return a pointer to a list of hash types specified for a user ### in Open Directory ### sub get_hashtypes { my $user = shift; my $auth_auth; my %hashtypes; my @hashlist; my $hash; my $i; while ($i = shift) { chomp $i; $auth_auth = $i; return undef if $auth_auth =~ $DSCL_ARGS_AUTH_ERR; next unless $auth_auth =~ /^\s*$DSCL_ARGS_AUTH:\s*(.+)\s*$/; $auth_auth = $1; } ### Actually, users that do not have an authentication authority attribute ### can be authenticated using Basic password authentication. ### Basic auth will be supported in future releases. return undef unless $auth_auth; ### Hashes for disabled user are not dumped by default if ($auth_auth =~ /.*;$DISABLED_USER;.*/) { return undef unless $DISABLED; } ### AuthenticationAuthority attribute can be customized with ### a list of hashes. ### For example: ### ;ShadowHash;HASHLIST: ### No hashlist means "SALTED-SHA1" aka $DEFAULT_HASH type. ### Open Directory documentation mentions "SALTED-SHA-1", ### although actual systems show up "SALTED-SHA1" as a hash type. ### Therefore, a small dirty hack is used to replace all "SALTED-SHA-1" ### strings by "SALTED-SHA1" (remove "-" sign) if ($auth_auth =~ /.*;ShadowHash;(.*)/) { $hash = $1; $hash =~ s/SHA-1/SHA1/g; if ($hash =~ /\s*HASHLIST:<(.+)>/) { $hash = $1; } else { $hash = $DEFAULT_HASH; } @hashlist = split(/,/, $hash); ### If the user has more than one hash type and no specific hash type ### has been requested - a warning message is printed about ### any hash types other than SALTED-SHA-1. foreach $i (@hashlist) { if (!exists ($SUPPORTED_HASHES{$i}) || !$SUPPORTED_HASHES{$i}[1]) { print STDERR "Hash type $i is not supported\n"; next; } $hashtypes{$i} = 1; next if ($REQ_HASH || ($i eq $DEFAULT_HASH)); print STDERR <{$DEFAULT_HASH}; ### In case SALTED-SHA-1 is not available for the user - let's take any other ### hash type. @hashes = keys %{$p2hashtypes}; return $hashes[0]; } ### Get a hash file name for the given user sub get_filename { my $uid = shift; my $hashfile; $hashfile = $HASH_DIR . "/" . $uid; return $hashfile; } ### Extract all possible hash types from the hash file sub extract_allhashes { my $hashfile = shift; my $hashtype; my %all_hashes; my $hash; my $offset; my $size; my $got; ### Open the hash file open(F, "<$hashfile") || do { print STDERR "Cannot open $hashfile: $!\n"; return; }; ### Get all available hashes from this hashfile foreach $hashtype (keys %SUPPORTED_HASHES) { next unless (exists ($SUPPORTED_HASHES{$hashtype}) && $SUPPORTED_HASHES{$hashtype}[1]); $offset = $SUPPORTED_HASHES{$hashtype}[0]; $size = $SUPPORTED_HASHES{$hashtype}[1]; seek(F, $offset, 0) || next; $got = read(F, $hash, $size); next if ($hash =~ /^0*$/); next if ($got != $size); $hash .= "00000000" if ($hashtype =~ /^SHA1$/); $all_hashes{$hashtype} = $hash; } close F; return %all_hashes; } ### ### Generate last 5 fields of /etc/passwd-like record ### sub gen_pw_entry { my $pointer = shift; my $entry; #UID $entry = $pointer->{$DSCL_ARGS_USERINFO[1]}; #GID $entry .= ":" . $pointer->{$DSCL_ARGS_USERINFO[2]}; #Real Name (and password hint as an option) $entry .= ":" . $pointer->{$DSCL_ARGS_USERINFO[3]}; $entry .= $HINT_DELIMITER . $pointer->{$AUTH_HINT} if (($HINT) && (defined($pointer->{$AUTH_HINT}))); #Home directory $entry .= ":" . $pointer->{$DSCL_ARGS_USERINFO[4]}; #Command/shell $entry .= ":" . $pointer->{$DSCL_ARGS_USERINFO[5]}; return $entry; } ### ### Sanitize a string (deal with colons, spaces, special chars etc) ### Replace all special characters with spaces, ### remove repeated spaces then. ### sub sanitize { my $string = shift; $string =~ s/[:\x7f\x01-\x1f]/ /g; $string =~ s/^\s+//; $string =~ s/\s{2,}/ /g; return $string; } ### ### Convert a filename to username (leave only \w pattern) ### sub filename2username { my $name = shift; $name =~ s/\W//g; return $name; } ### Parse the command-line sub parse_cli { while (@ARGV) { my $arg = shift @ARGV; usage(0) if ($arg =~ /^-h$/); if ($arg =~ /^-d$/) { $DISABLED = 1; next; } if ($arg =~ /^-nohint$/) { $HINT = 0; next; } if ($arg =~ /^-sanity$/) { $SANITY = 1; next; } if ($arg =~ /^-f$/) { $DUMP_FROM_FILE = 1; next; } if ($OPTIONS{$arg}) { if ($REQ_HASH) { print STDERR < to dump password hash(es) from file(s). EOF ; } push (@unused_opts, "-sanity") if $SANITY; push (@unused_opts, "-nohint") unless $HINT; print STDERR "-f option is specified. @unused_opts option(s) will be ignored!\n" if @unused_opts; } } ############################################################################ ################################## Main ################################## ############################################################################ parse_cli(); ### Check EUID if ($> != 0) { print STDERR <{$DSCL_ARGS_USERINFO[0]}); } ### Read all non-zero hashes for this user from the hash file (if any) unless (%all_hashes = extract_allhashes($filename)) { print STDERR "There is no hashes available for the user $user\n"; next; } ### The optional consistency check. If the hashfile contains any hashes ### which are not shown up in AuthenticationAuthority attribute, ### the warning will be produced. This behaviour is controlled by ### "-sanity" CLI option if ($SANITY) { foreach $hashtype (keys %all_hashes) { next if exists $REAL_USERS->{$user}{$hashtype}; print STDERR <