#!/usr/bin/env perl # Program: add-batch-Unix-accounts.pl # Design: Add Unix accounts in batch (Linux, HP-UX, Solaris, Tru64 # and AIX tested) # Usage: add-batch-Unix-accounts.pl [-h] [-i] [-f accfile] [-u startUID] # [-s] [-p] [-x] # -f accfile Login names for new accounts # One per line, or full details # newuser:accpasswd:accuid:accgid:GECOS:acchome:accshell # -h Print this help message # -i Interactive passwords for Net::SSH # -p User pasword authentication for SSH to remote server # -s read array of remote servers to execute commands on # -u startUID Starting UID number for new accounts # -x Force password change at first login # # NOTE: The remote execution is currently 80% ready # I need to think about best method to pass the # commands for remote execution (scp, socket, or...) # # Last Update: 19 June 2015 # Designed by: Dusan U. Baljevic (dusan.baljevic@ieee.org) # Coded by: Dusan U. Baljevic (dusan.baljevic@ieee.org) # Special thanks go to Larson Caylan for many tests and # useful comments! His debugging efforts provided invaluable # help. # # Copyright 2011-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 . # # Define important environment variables # $ENV{'PATH'} = "/bin:/usr/sbin:/sbin:/usr/bin:/usr/ccs/bin:/opt/local/bin"; # Define Shell # $ENV{'SHELL'} = '/bin/sh' if $ENV{'SHELL'} ne ''; $ENV{'IFS'} = '' if $ENV{'IFS'} ne ''; # File to add log details of new accounts for auditing purposes # my $LOGFILE = "/var/log/add-accounts.log"; my @PASSENT = (); # Various log strings # my $ERRSTR = "ERROR"; my $WARNSTR = "WARN"; my $PASSSTR = "PASS"; my $SUMM = "SUMMARY"; my $TABSP = q{ }; my $INFOSTR = "INFO"; # Do not allow to run as unprivileged user # if ( $> != 0 ) { print "$TABSP$ERRSTR: The script must be run with root privileges\n"; exit(1); } # Make sure strict is used # if ( eval "require strict" ) { import strict; use strict; } else { print "$TABSP$WARNSTR: Perl strict not found\n"; } use vars qw ($Accfile $StartUID $VH $System $Hostname); # Variables with defaults. # I even considered an option to add all of them via various # command-line arguments, but decided against it. Not worth it. # Password lenght # my $Length = 8; # Array of remote servers and their root passwords to execute commands on # By default (unless flag "-s" is used, public key authentication # will be used. Password authetication (even with SSH) is not # recommended # # If flag "-s" is used and no password is provided # the script will attempt to use SSH key # # If "-p" flag is used, then passwords below will be # passed to remote SSH connection # my %SSHARRAY = ( "server1" => "pass1", "server2" => "pass2", "server3" => "pass2", "server4" => "", ); # SSH private key location (change to match setup at your site) # my $homessh = "$ENV{HOME}/.ssh/id_rsa"; # Username lenght # my $ULENGTH = 8; # Temporary file to save the commands for adding new accounts # The randomness for filename is good enough # srand (time ^ $$ ^ ( unpack "%L*", `uptime`) ^ ($$ <<15)); my $randno = int(rand() * 10e8); my $tempfile = "/var/tmp/.$$-useradd-${randno}"; # Array of commands to create new Unix accounts # my @PASSARR = (); # Array of records with new Unix accounts and their initial passwords # my @NOTIFY = (); # Number of days of warning before a password change is required # my $MINPWWRN = 7; # Maximum number of days during which a password is valid # my $MAXPWLIFE = 84; # Minimum number of days between password changes # my $MINPWCH = 0; # Default home directory # my $DEFHOME = "/home"; # Default Unix group ID (GID) # my $DEFGRP = "1"; # Default Login Shell # my $DEFSHELL = "/bin/ksh"; # Default random device # my $RandomDevice = "/dev/urandom"; # Default characters for passwords # (In this case uppercase, lowercase and digits only) # my $lowercase = q{abcdefghijklmnopqrstuvwxyz}; my $UPPERCASE = q{ABCDEFGHIJKLMNOPQRSTUVWXYZ}; my $Digits = q{0123456789}; my $Punctuation = q{`~!@#$%^&*()-=_+[]\\|;':",./<>?} . '{}'; #my $Matchlist = "${UPPERCASE}${lowercase}${Digits}${Punctuation}"; my $Matchlist = "${lowercase}${UPPERCASE}${Digits}"; my $Maj = q{}; my $Hardware = q{}; my $Version = q{}; my $VH = q{}; my $Major = q{}; my $Minor = q{}; my $Patch = q{}; # Prompt if Trusted Password database used and admnum set on HP-UX # my $PromptTCB = q{}; # Privileged account on remote servers # my $RemLogin = "root"; # For HP-UX and Tru64 # my $TCB = q{}; my $Shadow = "/etc/shadow"; my $PWEXPFLAG = 0; my $itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; sub Usage { if ( eval "require File::Basename" ) { import File::Basename; $CMD = basename( "$0", ".pl" ); Prusage(); } else { $CMD = `basename $0`; chomp($CMD); Prusage(); } } sub Prusage { print <&1`; ( $System, $Hostname, $Maj, undef, $Hardware, undef ) = split( /\s+/, $VH ); if ( "$System" eq "AIX" ) { $Version = `uname -v`; $Maj = `uname -r`; chomp($Version); chomp($Maj); } ( $Major, $Minor, $Patch ) = split( /\./, $Maj ); } if ( "$System" eq "Linux" ) { $ENV{'PATH'} = "$ENV{PATH}:/usr/local/bin:/usr/local/sbin"; $DEFGRP = "100"; $DEFSHELL = "/bin/bash"; } elsif ( "$System" eq "AIX" ) { $DEFGRP = "staff"; $DEFSHELL = "/bin/ksh"; } elsif ( "$System" eq "OSF1" ) { $DEFGRP = "15"; $DEFSHELL = "/bin/ksh"; $TCB = `rcmgr get SECURITY 2>/dev/null | egrep "ENHANCED"`; if ( "$TCB" ) { print "$TABSP$PASSSTR: Enhanced Password database used\n"; $PWEXPFLAG++; } else { print "$TABSP$WARNSTR: Enhanced Password database not used\n"; } } elsif ( "$System" eq "HP-UX" ) { $ENV{'PATH'} = "$ENV{PATH}:/usr/lbin:/usr/sam/lbin"; $DEFGRP = "1"; $DEFSHELL = "/bin/ksh"; $TCB = `getprdef -r 2>&1 | egrep "not trusted"`; if ( !"$TCB" ) { print "$TABSP$PASSSTR: Trusted System Password database used\n"; $PWEXPFLAG++; } else { print "$TABSP$WARNSTR: Trusted System Password database not used\n"; } if ( !-s "$Shadow" ) { print "$TABSP$WARNSTR: Shadow password database not used\n"; } else { print "$TABSP$PASSSTR: Shadow password database used\n"; $PWEXPFLAG++; } } elsif ( "$System" eq "SunOS" ) { $ENV{'PATH'} = "$ENV{PATH}:/usr/local/bin:/usr/local/sbin"; $DEFGRP = "1"; $DEFSHELL = "/bin/bash"; } else { print "$TABSP$ERRSTR: Unsupported O/S $System\n"; &Usage; } # Subroutine to check current time # sub datecheck { ($Csec,$Cmin,$Chour,$Cmday,$Cmon,$Cyear,$Cwday,$Cyday,$Cisdst) = localtime(time); $datestring = sprintf("%02d-%02d-%04d-%02d:%02d:%02d",$Cmday, ($Cmon+1), ($Cyear + 1900), $Chour, $Cmin, $Csec); } sub Randomgen { my $newrec = shift; my $newuser = q{}; my $GECOS = q{}; my $GCOMM = q{}; my $ulength = 0; my $Password = q{}; my $accpasswd = q{}; my $accuid = q{}; my $accgid = q{}; my $acchome = q{}; my $accshell = q{}; my @ckarr = split( /:/, $newrec ); my $zzz = scalar @ckarr; if ( $zzz == 1 ) { $newuser = $ckarr[0]; } elsif ( $zzz <= 7 ) { $newuser = $ckarr[0]; $accpasswd = $ckarr[1]; $accuid = $ckarr[2]; $accgid = $ckarr[3]; $GECOS = $ckarr[4]; $acchome = $ckarr[5]; $accshell = $ckarr[6]; } else { print "$TABSP$ERRSTR: Invalid number of fields in record \"@ckarr\"\n"; next; } if( ( "$accuid" ) && ( ! ( $accuid =~ /^[0-9]+$/ ) ) ) { print "$TABSP$ERRSTR: UID $accuid not numerical\n"; next; } if( ( "$accgid" ) && ! ( $accgid =~ /^[0-9]+$/ ) ) { print "$TABSP$ERRSTR: GID $accgid not numerical\n"; next; } $newuser =~ s/^\s+//g; $newuser =~ s/\s+$//g; $GECOS =~ s/^\s+//g; $GECOS =~ s/\s+$//g; # Checking valid characters in login names # Lower-case characters and digits recommended # if( ! ( $newuser =~ /^[a-zA-Z0-9]+$/ ) ) { print "$TABSP$WARNSTR: Invalid characters in login name \"$newuser\"\n"; next; } if( $newuser =~ /\p{IsUpper}/ ) { print "$TABSP$ERRSTR: Upper-case characters in login name \"$newuser\"\n"; next; } my @Userexist = getpwnam($newuser); if( "@Userexist" ) { print "$TABSP$ERRSTR: Login name \"$newuser\" already exists\n"; next; } $ulength = length($newuser); if ( "$System" eq "HP-UX" ) { if ( "$Minor$Patch" >= 1131 ) { my $lugadmin = `lugadmin -l 2>/dev/null`; chomp($lugadmin); if ( "$lugadmin" == 64 ) { print "$TABSP$INFOSTR: Login names are restricted to short user and group names\n"; next; } } else { if ( $ulength > $ULENGTH ) { print "$TABSP$ERRSTR: Login name \"$newuser\" has more than $ULENGTH characters\n"; next; } } } else { if ( $ulength > $ULENGTH ) { print "$TABSP$ERRSTR: Login name \"$newuser\" has more than $ULENGTH characters\n"; next; } } # One neat trick to generate passwords via Shell: # # head -n 10 /dev/urandom | tr -cd "[:alnum:]" | cut -c '1-8' # if ( ! "$accpasswd" ) { if ( ! open RANDOMDEVICE, $RandomDevice ) { print "$TABSP$ERRSTR: Cannot open $RandomDevice: Error $!\n"; print "$TABSP$INFOSTR: Recommended to fix your security setup!\n"; exit(1); } until ($Password =~ /^.{$Length}$/) { $_ = getc RANDOMDEVICE; $Password .= $_ if (m/[$Matchlist]/) } close RANDOMDEVICE; } else { $Password = $accpasswd; } $Password =~ s/\s+//g; # Now find the encrypted version of the password # my $salt; for (1..2) { $salt .= substr $itoa64, rand(length($itoa64)), 1; } my $encryptedpwd = crypt($Password, $salt); if ( "$System" eq "AIX" ) { if ( "$Version$Maj" >= 51 ) { if ( "$GECOS" ) { $GCOMM = " -c \"$GECOS\""; } else { $GCOMM = ""; } } else { if ( "$GECOS" ) { if ( "$System" eq "HP-UX" ) { $GCOMM = " gecos=\"$GECOS,,,\""; } else { $GCOMM = " gecos=\"$GECOS\""; } } else { $GCOMM = ""; } } } else { if ( "$GECOS" ) { $GCOMM = " -c \"$GECOS\""; } else { $GCOMM = ""; } } my $curUID = $accuid || $StartUID; if( ! "$curUID" ) { print "$TABSP$ERRSTR: Login UID not defined for $newuser\n"; next; } my $UIDexist = getpwuid($curUID); if( "$UIDexist" ) { print "$TABSP$ERRSTR: Login UID \"$curUID\" already exists\n"; next; } if ( "$System" eq "AIX" ) { if ( "$Version$Maj" >= 51 ) { if ( "$accuid" ) { $ACOM = " -u $accuid"; } else { $ACOM = " -u $StartUID"; } } else { if ( "$accuid" ) { $ACOM = " id=$accuid"; } else { $ACOM = " id=$StartUID"; } } } else { if ( "$accuid" ) { $ACOM = " -u $accuid"; } else { $ACOM = " -u $StartUID"; } } if ( "$System" eq "AIX" ) { if ( "$Version$Maj" >= 51 ) { if ( "$accgid" ) { $BCOM = " -g $accgid"; } else { $BCOM = " -g $DEFGRP"; } } else { if ( "$accgid" ) { my $gidno = getgrgid($accgid); if ( "$gidno" ) { $BCOM = " groups=$gidno"; } else { $BCOM = ""; } } else { $BCOM = ""; } } } else { if ( "$accgid" ) { if ( getgrgid($accgid) ) { $BCOM = " -g $accgid"; } else { print "$TABSP$ERRSTR: Group UID \"$accgid\" does not exist\n"; next; } } else { $BCOM = " -g $DEFGRP"; } } if ( "$System" eq "AIX" ) { if ( "$Version$Maj" >= 51 ) { if ( "$acchome" ) { $HCOM = " -d $acchome"; } else { $HCOM = " -d $DEFHOME/$newuser"; } } else { if ( "$acchome" ) { $HCOM = " home=$acchome"; } else { $HCOM = " home=$DEFHOME/$newuser"; } } } else { if ( "$acchome" ) { $HCOM = " -d $acchome"; } else { $HCOM = " -d $DEFHOME/$newuser"; } } if ( "$System" eq "AIX" ) { if ( "$Version$Maj" >= 51 ) { if ( "$accshell" ) { $SCOM = " -s $accshell"; } else { $SCOM = " -s $DEFSHELL"; } } else { if ( "$accshell" ) { $SCOM = " shell=$accshell"; } else { $SCOM = " shell=$DEFSHELL"; } } } else { if ( "$accshell" ) { $SCOM = " -s $accshell"; } else { $SCOM = " -s $DEFSHELL"; } } push(@NOTIFY, "Initial password for Unix account \"$newuser\" is $Password\n"); if ( "$System" eq "Linux" ) { # Linux itself offers some creative ways to add batch accounts # Example 1: # for newacct in user1 user2 user3 # do # echo secretpass | passwd --stdin $newacct # done # # Example 2: # cat newaccounts.txt # user1:secretpass:500:500:Blah:/home/user1:/bin/bash # user2:secretpw:751:751:Blah:/home/user2:/bin/bash # ...snip... # newusers newaccounts.txt # # If ever we need to you Expect to change password, # here are the prompts # my $Prompt = "New UNIX password:"; my $Prompt2 = "Retype new UNIX password:"; push(@PASSARR, "useradd${ACOM}${BCOM}${HCOM}${SCOM} -m -p \"${encryptedpwd}\"${GCOMM} $newuser\n"); push(@PASSARR, "chage -m $MINPWCH -M $MAXPWLIFE -W $MINPWWRN $newuser\n"); if ( "$opts{x}" == 1 ) { push(@PASSARR, "usermod -L $newuser\n"); push(@PASSARR, "chage -d 0 $newuser\n"); push(@PASSARR, "usermod -U $newuser\n"); } } elsif ( "$System" eq "HP-UX" ) { # If ever we need to use Expect to change password, # here are the prompts # if ( !"$TCB" ) { my $TCBadm = `getprpw -m admnum $newuser | awk -F= '{print \$2}'`; if ( "$TCBadm" ) { chomp($TCBadm); $PromptTCB = "Enter your user number here:"; } } my $Prompt = "New password:"; my $Prompt2 = "Re-enter new password:"; push(@PASSARR, "useradd${ACOM}${BCOM}${HCOM}${SCOM}${GCOMM} -m $newuser\n"); push(@PASSARR, "usermod -p \"$encryptedpwd\" $newuser\n"); if ( $PWEXPFLAG > 0 ) { push(@PASSARR, "passwd -n $MINPWCH -x $MAXPWLIFE -w $MINPWWRN $newuser\n"); } if ( "$opts{x}" == 1 ) { push(@PASSARR, "passwd -f $newuser\n"); } } elsif ( "$System" eq "AIX" ) { if ( "$Version$Maj" >= 51 ) { push(@PASSARR, "useradd${ACOM}${BCOM}${HCOM}${SCOM} -m ${GCOMM} $newuser\n"); push(@PASSARR, "echo \"$newuser:$Password\" | chpasswd\n"); } else { push(@PASSARR, "mkuser${ACOM}${BCOM}${HCOM}${SCOM} $newuser\n"); push(@PASSARR, "echo \"Please set the password for $newuser manually\"\n"); } push(@PASSARR, "chuser maxexpired=$MINPWCH maxage=$MAXPWLIFE pwdwarntime=$MINPWWRN $newuser\n"); if ( "$opts{x}" == 1 ) { push(@PASSARR, "pwdadm -f ADMCHG $newuser\n"); } } elsif ( "$System" eq "SunOS" ) { push(@PASSARR, "useradd${ACOM}${BCOM}${HCOM}${SCOM} -m ${GCOMM} $newuser\n"); if ( eval "require Expect" ) { import Expect; use Expect; } # Force autoflush right away and after every # write or print on the currently selected output channel $| = 1; $Expect::Log_Stdout = 0; # First we have to initialize STDIN in to an expect object. my $stdin = Expect->exp_init( \*STDIN ); my $stdout = Expect->exp_init( \*STDOUT ); # Here are the prompts # my $Prompt = "New Password:"; my $Prompt2 = "Re-enter new Password:"; $Command = "passwd $newuser"; my $sh = Expect->spawn("$Command") or die "ERROR: Spawn failed: $?"; $sh->log_stdout(0); # Log to STDOUT $sh->expect( 5, '-re', "$Prompt"); print $sh "$Password\n"; $sh->expect( 5, '-re', "$Prompt2"); print $sh "$Password\n"; # Close the command nicely # No need to be rude and use hard_close() # $sh->soft_close(); # Reset autoflush # $| = 0; push(@PASSARR, "passwd -n $MINPWCH -x $MAXPWLIFE -w $MINPWWRN $newuser\n"); if ( "$opts{x}" == 1 ) { push(@PASSARR, "passwd -f $newuser\n"); } } elsif ( "$System" eq "OSF1" ) { push(@PASSARR, "useradd${ACOM}${BCOM}${HCOM}${SCOM} -m ${GCOMM} $newuser\n"); push(@PASSARR, "echo \"Please set password for $newuser manually\""); if ( $PWEXPFLAG > 0 ) { push(@PASSARR, "usermod -x passwd_min_change_time=$MINPWCH passwd_expire_time=$MAXPWLIFE $newuser\n"); } else { if ( eval "require Expect" ) { import Expect; use Expect; } # Force autoflush right away and after every # write or print on the currently selected output channel. $| = 1; $Expect::Log_Stdout = 0; # First we have to initialize STDIN in to an expect object. my $stdin = Expect->exp_init( \*STDIN ); my $stdout = Expect->exp_init( \*STDOUT ); # Here are the prompts # my $Prompt = "New password:"; my $Prompt2 = "Retype new password:"; $Command = "passwd $newuser"; my $sh = Expect->spawn("$Command") or die "ERROR: Spawn failed: $?"; $sh->log_stdout(0); # Log to STDOUT $sh->expect( 5, '-re', "$Prompt"); print $sh "$Password\n"; $sh->expect( 5, '-re', "$Prompt2"); print $sh "$Password\n"; # Close the command nicely # No need to be rude and use hard_close() # $sh->soft_close(); # Reset autoflush # $| = 0; } if ( "$opts{x}" == 1 ) { push(@PASSARR, "usermod -x passwd_must_change=1 $newuser\n"); } } else { print "$TABSP$ERRSTR: Untested O/S\n"; } $StartUID++; datecheck(); push(@PASSENT, "Username \"$newuser\" created on $datestring\n"); } if ( -s "$Accfile" ) { if ( open( AC, "egrep -v ^# $Accfile 2>&1 |" ) ) { while () { next if ( grep(/^$/, $_ ) ); chomp($_); &Randomgen($_); } close(AC); } } else { print "$TABSP$ERRSTR: Cannot open file $Accfile\n"; exit(1); } print "\n"; print "$TABSP$INFOSTR: Generating ${Length}-character passwords with ${RandomDevice} and the following characters: ${Matchlist} Passwords are drawn from a keyspace of $RandomBits bits\n\n"; print "$TABSP$INFOSTR: Recommended Shell commands for account creation\n"; print "$TABSP$INFOSTR: Please ensure your Shell supports all characters below\n"; if ( "@PASSARR" ) { print @PASSARR; } print "\n"; print "$TABSP$SUMM:\n"; print @NOTIFY; print "\n"; umask((umask() & 077) | 077); if ( open( F, "> $tempfile" ) ) { print F @PASSARR or croak "$TABSP$ERRSTR: Cannot use $tempfile for writing: $OS_ERROR\n"; close F; } else { print "$TABSP$ERRSTR: Cannot open $tempfile for writing: $!"; exit(1); } # Execute a Shell script to add new accounts on the local server # if ( -s "$tempfile" ) { chmod 0700, $tempfile; # # Un-set CHLD signal handler # local $SIG{'CHLD'} = q{}; my @doit = ("$tempfile"); WIFEXITED(system @doit) or croak "$TABSP$ERRSTR: Cannot execute $doit: $OS_ERROR\n"; # If successful, remove the command file # unlink($tempfile); if ( open( F, ">> $LOGFILE" ) ) { print F @PASSENT or croak "$TABSP$ERRSTR: Cannot use $LOGFILE for writing: $OS_ERROR\n"; close F; } else { print "$TABSP$ERRSTR: Cannot open $LOGFILE for writing: $!"; exit(1); } } # Idea about executing commands via SSH remotely # if ( "$opts{s}" == 1 ) { use Net::SSH::Perl::Auth; use Net::SSH::Perl::Auth::PublicKey; # This is just a test until I decide how to # run commands on the remote server (either via scp, # or through a socket # # The additional problem is to decide how to simplify # detection of the O/S on each remote server before # sending correct commands for new account creation # my $cmd = "cat /etc/motd"; foreach my $RemSrv (keys %SSHARRAY) { my $RemPass = q{}; if ( "$opts{p}" == 1 ) { $RemPass = $SSHARRAY{$RemSrv}; } print "$TABSP$INFOSTR: Processing server \"$RemSrv\"\n"; my $ssh = q{}; if ( "$opts{i}" == 1 ) { $ssh = Net::SSH::Perl->new("$RemSrv", protocol => '2,1', interactive => 1, debug => 0); $ssh->login($RemLogin); } else { $ssh = Net::SSH::Perl->new("$RemSrv", identity_files, [ "$homessh" ], protocol => '2,1', debug => 0); $ssh->login($RemLogin, $RemPass); } my($stdout, $stderr, $exit) = $ssh->cmd($cmd); print "$stdout\n"; } } exit(0);