#!/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);