Files
webmin/bin/passwd
Ilia Ross 7e41f339a3
Some checks failed
Tests / prove (push) Has been cancelled
Package and upload artifacts / build (push) Has been cancelled
Close inactive / close-inactive (push) Has been cancelled
Fix password reset handling for Unix-backed Webmin users
ⓘ Warn when a Webmin user matches a Unix account and allow choosing Unix authentication or a separate Webmin-only password. Preserve explicit and legacy password behavior with regression coverage.

https://forum.virtualmin.com/t/resetting-root-admin-password/137615/4
2026-07-23 17:25:39 +02:00

413 lines
13 KiB
Perl
Executable File

#!/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 = <STDIN>;
system("stty echo");
print "\nRetype new password:";
system("stty -echo");
my $pass2 = <STDIN>;
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 = <STDIN>;
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</etc/webmin>
=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<passwd> 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<miniserv.users>, 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 <jcameron@webmin.com>
Joe Cooper <joe@virtualmin.com>
Ilia Ross <ilia@virtualmin.com>