#!/usr/bin/env perl # passwd - change Webmin users password use strict; use warnings; use 5.010; use File::Basename; use Getopt::Long; use Pod::Usage; use Term::ANSIColor qw(:constants); use lib (dirname(dirname($0))); use WebminCore; sub main { my %opt; GetOptions('help|h' => \$opt{'help'}, 'config|c=s' => \$opt{'config'}, 'user|u=s' => \$opt{'user'}, 'password|p=s' => \$opt{'password'}, 'unix' => \$opt{'unix'}, 'webmin-only|webmin' => \$opt{'webmin'}, 'stdout|o!' => \$opt{'stdout'}); # If username passed as regular param my $user = scalar(@ARGV) == 1 && $ARGV[0]; # Show usage pod2usage(0) if ($opt{'help'} || (!$opt{'user'} && !$user)); # Assign defaults $opt{'config'} ||= "/etc/webmin"; $opt{'user'} = $user if ($user && !$opt{'user'}); # Catch kill signal my $sigkill = sub { system("stty echo"); print "\n^C"; print "\n"; exit 1; }; $SIG{INT} = \&$sigkill; # Run change password command change_password(\%opt); return 0; } exit main(\@ARGV) if !caller(0); sub change_password { my ($optref) = @_; my ($minserv_uconf_file, %lusers, @users, %uinfos, %ulines); my $user = $optref->{'user'}; my $pass = $optref->{'password'}; my $confdif = $optref->{'config'}; my $conf = "$confdif/config"; my $mconf = "$confdif/miniserv.conf"; my $conf_check = sub { my ($configs) = @_; foreach my $config (@{$configs}) { if (!-r $config) { say BRIGHT_RED, "Error: ", RESET, "Failed to read Webmin essential config file: ", BRIGHT_YELLOW, $config, RESET, " doesn't exist"; exit 1; } } }; my $root = root($confdif, \&$conf_check); my $encrypt_password = sub { my ($pass, $gconfig, $config) = @_; my $root = root($confdif, \&$conf_check); # Use pre-defined encryption (forced by Webmin config) if (!$optref->{'stdout'} && ($gconfig->{'md5pass'} == 1 || $gconfig->{'md5pass'} == 2)) { do "$root/acl/md5-lib.pl"; # Use MD5 encryption return &encrypt_md5($pass) if ($gconfig->{'md5pass'}) == 1; # Use SHA512 encryption return &encrypt_sha512($pass) if ($gconfig->{'md5pass'}) == 2; } else { # Try detecting system default first my $module = 'useradmin'; if (-d "$root/$module") { $ENV{'PERLLIB'} = "$root"; $ENV{'WEBMIN_CONFIG'} = "$confdif"; $ENV{'FOREIGN_ROOT_DIRECTORY'} = "$root/$module"; $ENV{'FOREIGN_MODULE_NAME'} = "$module"; chdir("$root/$module"); require "$root/useradmin/user-lib.pl"; # We need to set third parameter to make sure useradmin's config # won't be used for hashing format, as we need to auto detect it return &encrypt_password($pass, undef, 'force_system_detection'); } else { # Use old Unix DES srand(time() ^ $$); return crypt($pass, chr(int(rand(26)) + 65) . chr(int(rand(26)) + 65)); } } }; # Check for main config and miniserv config files &$conf_check([$conf, $mconf]); # Read and parse configs my (%config, %gconfig, %uconfig); read_file($mconf, \%config); read_file($conf, \%gconfig); $minserv_uconf_file = $config{'userfile'}; # Check for main user file &$conf_check([$minserv_uconf_file]); # Read and parse `miniserv.users` config file read_file($minserv_uconf_file, \%lusers, undef, undef, ":"); @users = keys %lusers; map {my @uinfo = split(':', "$lusers{$_}"); $uinfos{$_} = \@uinfo} @users; # Check if user exists if (!defined($uinfos{$user})) { my $user_str = scalar(@users) > 1 ? 'users' : 'user'; my $user_str2 = scalar(@users) > 1 ? 'are' : 'is'; die(BRIGHT_RED, "Error: ", RESET . "Webmin user ", BRIGHT_YELLOW, $user, RESET, " doesn't exist. Existing Webmin $user_str on your system $user_str2 — ", BRIGHT_YELLOW, join(", ", sort(@users)), RESET, "\n"); } # A Webmin user can either authenticate as a Unix user or have a separate # password in miniserv.users. Make this distinction explicit whenever both # accounts exist, as silently setting the latter overrides Unix # authentication and leaves SSH and Webmin with different passwords. my @unix_info = getpwnam($user); my $target = choose_password_target( $optref, $user, scalar(@unix_info)); if ($target eq 'unix') { change_unix_password($user); # Ensure Webmin uses the newly changed Unix password. No restart is # needed when the account was already configured this way. if (($uinfos{$user}->[0] // '') ne 'x') { $uinfos{$user}->[0] = 'x'; map {$ulines{$_} = join(":", @{ $uinfos{$_} })} keys %uinfos; store_webmin_users( $confdif, $minserv_uconf_file, \%ulines); } say "Unix password for user ", BRIGHT_YELLOW, $user, RESET, " updated successfully; Webmin will use Unix authentication"; exit 0; } # Ask for password on stdin my $suc_pre_msg = ""; my $suc_msg = 'updated successfully'; if (!$pass) { print "Enter password for user ", BRIGHT_YELLOW, $user, RESET, ":"; system("stty -echo"); $pass = ; system("stty echo"); print "\nRetype new password:"; system("stty -echo"); my $pass2 = ; system("stty echo"); print "\n"; if ($pass ne $pass2) { say BRIGHT_RED, "Error: ", RESET, "Passwords do not match"; exit 1; } chomp $pass; if (!$pass) { $suc_pre_msg = BOLD BRIGHT_RED ON_WHITE . 'Warning:' . RESET . " "; $suc_msg = "has been removed, enabling anyone to login without authentication"; } } # Update with new password and store timestamp $uinfos{$user}->[0] = &$encrypt_password($pass, \%gconfig, \%config); # Print the hash and exit if ($optref->{'stdout'}) { say $uinfos{$user}->[0]; exit 0; } $uinfos{$user}->[5] = time() if ($uinfos{$user}->[5]); map {$ulines{$_} = join(":", @{ $uinfos{$_} })} keys %uinfos; # Write the new user config and restart Webmin store_webmin_users($confdif, $minserv_uconf_file, \%ulines); # Print user message say "${suc_pre_msg}Password for Webmin user ", BRIGHT_YELLOW, $user, RESET, " $suc_msg"; exit 0; } sub choose_password_target { my ($optref, $user, $unix_exists, $interactive) = @_; die BRIGHT_RED, "Error: ", RESET, "The --unix and --webmin-only options cannot be used together\n" if ($optref->{'unix'} && $optref->{'webmin'}); die BRIGHT_RED, "Error: ", RESET, "The --unix and --stdout options cannot be used together\n" if ($optref->{'unix'} && $optref->{'stdout'}); die BRIGHT_RED, "Error: ", RESET, "The --unix and --password options cannot be used together; ", "the system passwd command reads the password securely\n" if ($optref->{'unix'} && defined($optref->{'password'})); die BRIGHT_RED, "Error: ", RESET, "Unix user ", BRIGHT_YELLOW, $user, RESET, " doesn't exist\n" if ($optref->{'unix'} && !$unix_exists); return 'webmin' if ($optref->{'stdout'} || !$unix_exists); return 'unix' if ($optref->{'unix'}); return 'webmin' if ($optref->{'webmin'}); if (defined($optref->{'password'})) { print STDERR unix_password_warning($user), " The --password option explicitly sets a separate Webmin-only ", "password; use --unix without --password to change the Unix ", "password instead.\n"; return 'webmin'; } $interactive = -t STDIN if (!defined($interactive)); if ($interactive) { return prompt_password_target($user); } print STDERR unix_password_warning($user), " Non-interactive input prevents asking which password to change; ", "continuing with a separate Webmin-only password. Pass --unix or ", "--webmin-only to select explicitly.\n"; return 'webmin'; } sub prompt_password_target { my ($user) = @_; say unix_password_warning($user); say " 1. Change the Unix password and use Unix authentication in Webmin", " (recommended)"; say " 2. Set a separate Webmin-only password in miniserv.users"; while (1) { print "Select password type [1]: "; my $choice = ; die BRIGHT_RED, "\nError: ", RESET, "No password type was selected\n" if (!defined($choice)); chomp $choice; $choice = lc($choice); return 'unix' if ($choice eq '' || $choice eq '1' || $choice eq 'u' || $choice eq 'unix'); return 'webmin' if ($choice eq '2' || $choice eq 'w' || $choice eq 'webmin'); say BRIGHT_RED, "Invalid selection.", RESET, " Enter 1 for Unix or 2 for Webmin-only."; } } sub unix_password_warning { my ($user) = @_; return BRIGHT_YELLOW . "Warning: " . RESET . "Webmin user " . BRIGHT_YELLOW . $user . RESET . " is also a Unix user. A separate Webmin password overrides Unix " . "authentication, so Webmin and SSH can have different passwords."; } sub change_unix_password { my ($user) = @_; my $passwd = has_command('passwd'); die BRIGHT_RED, "Error: ", RESET, "The system passwd command could not be found\n" if (!$passwd); die BRIGHT_RED, "Error: ", RESET, "Changing a Unix password requires an interactive terminal. ", "Run ", BRIGHT_YELLOW, "$passwd $user", RESET, " directly instead.\n" if (!-t STDIN); my $status = system { $passwd } $passwd, $user; if ($status == -1) { die BRIGHT_RED, "Error: ", RESET, "Failed to run $passwd: $!\n"; } elsif ($status & 127) { die BRIGHT_RED, "Error: ", RESET, "The system passwd command was interrupted\n"; } elsif ($status >> 8) { die BRIGHT_RED, "Error: ", RESET, "The system passwd command failed\n"; } } sub store_webmin_users { my ($confdif, $minserv_uconf_file, $ulines) = @_; # Store original file first copy_source_dest($minserv_uconf_file, "$minserv_uconf_file-"); # Restart Webmin and write new user config file system("$confdif/stop >/dev/null 2>&1"); write_file($minserv_uconf_file, $ulines, ":"); system("$confdif/start >/dev/null 2>&1"); } sub root { my ($config, $conf_check) = @_; my $mconf = "$config/miniserv.conf"; $conf_check->([$mconf]); open(my $CONF, "<", $mconf); my $root; while (<$CONF>) { if (/^root=(.*)/) { $root = $1; } } close($CONF); # Does the Webmin root exist? if ($root) { die BRIGHT_RED, "Error: ", BRIGHT_YELLOW, $root, RESET, " is not a directory\n" unless (-d $root); } else { # Try to guess where Webmin lives, since config file didn't know. die BRIGHT_RED, "Error: ", RESET, "Unable to determine Webmin installation directory\n"; } return $root; } 1; =pod =head1 NAME passwd =head1 DESCRIPTION This program allows you to change the password used by a Webmin user. When a matching Unix user exists, it can change the Unix password or set a separate password in the Webmin password file. =head1 SYNOPSIS webmin passwd [options] =head1 OPTIONS =over =item --help, -h Print this usage summary and exit. Examples of usage: - webmin passwd root - webmin passwd --user root - webmin passwd --user root --unix - webmin passwd --user admin --webmin-only - webmin passwd --user admin --webmin-only --password ycwyMQRVAZY - webmin passwd --config /usr/local/etc/webmin --user admin --webmin-only --password ycwyMQRVAZY - webmin passwd --config /usr/local/etc/webmin --user root --password ycwyMQRVAZY --stdout =item --config, -c Specify the full path to the Webmin configuration directory. Defaults to C =item --user, -u Existing Webmin user to change password for =item --password, -p Set a new Webmin-only password. Using this option may be insecure because the password can be exposed in the process list. =item --unix Change the matching Unix user's password with the system C command, and configure Webmin to use Unix authentication. This is the recommended mode when the Webmin username is also a Unix username. =item --webmin-only, --webmin Set a separate password in C, even if a matching Unix user exists. This password overrides Unix authentication for the Webmin user. =back =head1 LICENSE AND COPYRIGHT Copyright 2018 Jamie Cameron Joe Cooper Ilia Ross