#!/usr/bin/env perl # Program: Linux-audit-account-password-hashing.pl # # Description: Linux account password hashing checks # Results are displayed on stdout or can be redirected to a file # # If you obtain this script via Web, convert it to Unix format. For example: # dos2unix -n Linux-audit-account-password-hashing.pl.txt Linux-audit-account-password-hashing.pl # # Last Update: 30 May 2014 # Designed by: Dusan U. Baljevic (dusan.baljevic@ieee.org) # Coded by: Dusan U. Baljevic (dusan.baljevic@ieee.org) # # Copyright 2015 Dusan Baljevic # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # The script has been developed over several hectic days, so errors # (although not planned) might exist. Please use with care. # Contrary to popular belief, the account password entries in /etc/shadow # can have more than three "$" separators (hint: when one uses SHA256|512 # hashing and non-default number of rounds). # Here are some examples of valid accounts on my CentOS 6.5 server: # # SHA256 hashing # # user1:$5$Y4HhzEPz$mXSHm95E/4MQPp.3X4Km5R/ysct0WT45FzdX2mPkon.:16156:::::: # # SHA512 hashing account with non-default rounds # # user2:$6$rounds=85000$pA/kjrZS$wo0980kwEuE28ER6moiaHzuDqO/VZMoxfvbXK1i/cW2BdJjI8xH/1WgD7RH7UaxM1SDLYsPtPgiMF9orb1Iwi.:16156:0:99999:7::: # # SHA512 hashing account # # user3:$6$zgpfWfGc$ACfCZLTLeJzLhiC1gyO0Bj5JlD337zAW.L25FpYz07QalwRQJYAJ8AIFL69PxK2XwoDehTLzPT64AsrMUsL1o0:15955:0:99999:7::: # # MD5 hashing account # # user4:$1$6tAaCsfx$E2amS8ko4ks1lxz7izSL//:16156:::::: # # Blowfish hashing account on Suse 11 # # user5:$2y$05$Z4taSkam70Vc9mMqtrAby25ixpstvJUf49gqzPtjhkscGgu4Zvd6c:15894:0:120:7::: # # I acknowledge contributions for testing on Suse servers by Ralph Roth # of cfg2html fame. # # Define important environment variables # $ENV{'PATH'} = "/bin:/usr/sbin:/sbin:/usr/bin:/usr/local/bin"; $ENV{'PATH'} = "$ENV{PATH}:/usr/local/sbin"; # Define Shell # $ENV{'SHELL'} = '/bin/sh' if $ENV{'SHELL'} ne ''; $ENV{'IFS'} = '' if $ENV{'IFS'} ne ''; use strict; # Hashing algorithms # my %PWHASHARR = ( "1", "hashing-algorithm=MD5", "2a", "hashing-algorithm=Blowfish-system-specific-handling-8bit-chars", "2y", "hashing-algorithm=Blowfish-with-correct-handling-8bit-chars", "5", "hashing-algorithm=SHA-256", "6", "hashing-algorithm=SHA-512", ); # String lengths for encrypted part of the pasword string # my %PWLEN = ( "1", "22", "2a", "53", "2y", "53", "5", "43", "6", "86", ); my @entry = (); my $SHADOW = "/etc/shadow"; my $SHADOWBCK = "/etc/shadow-"; my @SHADOWDIFF = (); my $passwdarr = q{}; my $pwdhash = q{}; my $LOGINDEFS = '/etc/login.defs'; my $SUSEDEFPASSWD = '/etc/default/passwd'; my $PAMDIR = '/etc/pam.d'; my $UBUNTUDEFPASSWD = '${PAMDIR}/common-password'; my $AUDCONF = '/etc/auditd.conf'; my $AUDCONF2 = '/etc/audit/auditd.conf'; if ( ! -s "$AUDCONF" ) { $AUDCONF = $AUDCONF2; } # String lengths for DES-encrypted pasword string # my $DESLENGTH = 13; if ( -s $SUSEDEFPASSWD ) { if ( open( sauthc, "awk NF $SUSEDEFPASSWD 2>/dev/null |" ) ) { print "\nINFO: Enabled features in configuration file $SUSEDEFPASSWD\n\n"; while () { print $_; chomp($_); if ( grep( /^CRYPT=/, $_ ) ) { $_ =~ s/^\s+//g; $_ =~ s/\s+$//g; my @pwdhash = split(/=/, $_); $pwdhash = $pwdhash[1]; } } close(sauthc); } } if ( -s $UBUNTUDEFPASSWD ) { if ( open( uauthc, "awk NF $UBUNTUDEFPASSWD 2>/dev/null |" ) ) { print "\nINFO: Enabled features in configuration file $UBUNTUDEFPASSWD\n\n"; while () { print $_; chomp($_); } close(uauthc); } } if ( -s $LOGINDEFS ) { if ( open( lauthc, "awk NF $LOGINDEFS 2>/dev/null |" ) ) { print "\nINFO: Enabled features in configuration file $LOGINDEFS\n\n"; while () { print $_; chomp($_); if ( grep( /^ENCRYPT_METHOD/, $_ ) ) { $_ =~ s/^\s+//g; $_ =~ s/\s+$//g; my @pwdhash = split(/\s+/, $_); $pwdhash = $pwdhash[1]; } } close(lauthc); } } if ( open( authc, "authconfig --test 2>/dev/null |" ) ) { print "\nINFO: Global system authentication resources\n"; while () { print $_; if ( grep( /hashing/, $_ ) ) { $_ =~ s/^\s+//g; $pwdhash = $_; } } close(authc); } else { print "WARN: System authentication resources status unknown or command \"authconfig\" missing in this distribution\n"; } if ( "$pwdhash" ) { print "\nINFO: Default password hashing\n"; print "INFO: $pwdhash\n"; print "NOTE: Minimum recommended password hashing is SHA512\n"; print "NOTE: For different Linux distributions, one of following methods are used: run \"authconfig --passalgo=sha512 --update\"\n"; print "Set \"CRYPT=SHA512\" in \"$SUSEDEFPASSWD\"\n"; print "Modify \"password\" line in \"$UBUNTUDEFPASSWD\" Set \"ENCRYPT_METHOD SHA512\" in \"$LOGINDEFS\"\n"; } print "\nINFO Status of configuration files in $PAMDIR\n"; my @pamls = `ls ${PAMDIR}/* 2>/dev/null`; foreach my $pcfg (@pamls) { chomp($pcfg); if ( -s $pcfg ) { print "\nINFO Configuration file $pcfg\n\n"; my @psfg = `grep -v ^# $pcfg | awk NF`; print @psfg; } } my @pamcfg = `pam-config --list-modules 2>/dev/null`; if ( "@pamcfg" ) { print "\nINFO List of supported PAM modules\n\n"; print @pamcfg; } if ( -s $AUDCONF ) { my @audc = `awk NF $AUDCONF 2>/dev/null | grep -v ^#`; if ( "@audc" ) { print "\nINFO Auditing configuration file $AUDCONF\n\n"; print @audc; } } my @aurep = `aureport 2>/dev/null | awk NF`; if ( "@aurep" ) { print "\nINFO Audit daemon logs\n\n"; print @aurep; } my @pwckr = `pwck -r 2>/dev/null`; if ( "@pwckr" ) { print "\nINFO Password file report in read-only mode (\"pwck -r\")\n\n"; print @pwckr; } my %shadiff; if ( -s "$SHADOW" ) { open my $afile, "$SHADOW" or die "Couldn't open $SHADOW: $!"; while (my $link = <$afile>) { chomp $link; $shadiff{$link} = undef; } close $afile; } if ( -s "$SHADOWBCK" ) { open my $bfile, "$SHADOWBCK" or die "Couldn't open $SHADOWBCK: $!"; while (my $link = <$bfile>) { chomp $link; next if exists $shadiff{$link}; push(@SHADOWDIFF, "$link\n"); } close $bfile; } if ( "@SHADOWDIFF" ) { print "\nINFO: $SHADOW differs from backup file $SHADOWBCK"; print "\nINFO: Offending entries in $SHADOW\n\n"; print @SHADOWDIFF; } my @PASA = (); print "\nINFO: Hashing algorithm per username:\n"; while ( @entry = getpwent ) { my @pwx = `passwd -S $entry[0] 2>&1 | awk NF`; push(@PASA, "@pwx"); my $pwhash = q{}; if ( grep(/^\$/, $entry[1]) ) { my @passwdarr = split(/\$/, $entry[1]); $pwhash = $passwdarr[$#passwdarr]; if ( $#passwdarr eq 3 ) { print "\n$entry[0]: $PWHASHARR{$passwdarr[1]}, salt=$passwdarr[2], hashed-password-and-salt=$passwdarr[3]\n"; } elsif ( $#passwdarr eq 4 ) { if ( $passwdarr[2] =~ /rounds=/ ) { print "\nINFO Username $entry[0]: $PWHASHARR{$passwdarr[1]}, $passwdarr[2], salt=$passwdarr[3], hashed-password-and-salt=$passwdarr[4]\n"; } elsif ( "$passwdarr[3]" eq "" ) { print "\nINFO Username $entry[0]: $PWHASHARR{$passwdarr[1]}, salt=$passwdarr[2], hashed-password-and-salt=$passwdarr[4]\n"; } else { print "\nINFO Username $entry[0]: $PWHASHARR{$passwdarr[1]}, salt=$passwdarr[2], hashed-password-and-salt=$passwdarr[4]\n"; } } else { print "\n$entry[0]:\n"; foreach my $passent ( @passwdarr) { print "$passent "; } print "\n"; } if ( length($passwdarr[$#passwdarr]) ne $PWLEN{$passwdarr[1]} ) { print "ERROR: Incorrect length of encrypted password string for user \"$entry[0]\" (length($passwdarr[$#passwdarr]) versus $PWLEN{$passwdarr[1]})\n"; } else { print "PASS: Correct length of encrypted password string for user \"$entry[0]\" ($PWLEN{$passwdarr[1]} for $PWHASHARR{$passwdarr[1]})\n"; } if ( ! ( $pwhash =~ /^[a-zA-Z0-9\.\/]+$/ ) ) { print "ERROR: Invalid characters in hashed password string \"$pwhash\"\n"; } } else { if ( $entry[1] eq "x" ) { print "\n$entry[0]: hashing-algorithm=UNDEFINED\n"; my @pw2 = `passwd -S $entry[0] 2>&1 | awk NF`; if ( "@pw2" ) { print "INFO Full password entry status for \"$entry[0]\" via \"passwd -S\" command \n"; print @pw2; } } else { if ( ! grep(/!|\*/, $entry[1]) ) { print "\n$entry[0]: hashing-algorithm=DES\n"; if ( length($entry[1]) ne $DESLENGTH ) { print "ERROR: Incorrect length of encrypted password string for user $entry[0] (length($entry[1]) versus $DESLENGTH)\n"; } else { print "PASS: Correct length of encrypted password string for user $entry[0] ($DESLENGTH)\n"; } } $pwhash = $entry[1]; } if ( $pwhash =~ /^[a-zA-Z0-9\.\/]/ ) { if ( ! ( $pwhash =~ /^[a-zA-Z0-9\.\/]+$/ ) ) { print "ERROR: Invalid characters in hashed password string \"$pwhash\"\n"; } else { print "PASS: Valid characters in hashed password string\n"; } } } } my @pwdsa = `passwd -Sa 2>/dev/null`; if ( "@pwdsa" ) { print "\nINFO Password status (\"passwd -Sa\")\n\n"; print @pwdsa; } else { if ( "@PASA" ) { print "\nINFO Password status (\"passwd -S\")\n\n"; print @PASA; } } exit(0);