1#!/usr/local/bin/perl
2# password_change.cgi
3# Actually update a user's password by directly modifying /etc/shadow
4
5BEGIN { push(@INC, "."); };
6use WebminCore;
7
8$ENV{'MINISERV_INTERNAL'} || die "Can only be called by miniserv.pl";
9&init_config();
10&ReadParse();
11&get_miniserv_config(\%miniserv);
12$miniserv{'passwd_mode'} == 2 || die "Password changing is not enabled!";
13
14# Validate inputs
15$in{'new1'} ne '' || &pass_error($text{'password_enew1'});
16$in{'new1'} eq $in{'new2'} || &pass_error($text{'password_enew2'});
17
18# Is this a Webmin user?
19if (&foreign_check("acl")) {
20	&foreign_require("acl", "acl-lib.pl");
21	($wuser) = grep { $_->{'name'} eq $in{'user'} } &acl::list_users();
22	if ($wuser) {
23		if ($wuser->{'pass'} eq 'x') {
24			# A Webmin user, but using Unix authentication
25			$wuser = undef;
26			}
27		elsif ($wuser->{'pass'} eq '*LK*' ||
28		       $wuser->{'pass'} =~ /^\!/) {
29			&pass_error("Webmin users with locked accounts cannot change ".
30				    "their passwords!");
31			}
32		}
33	}
34if (!$in{'pam'} && !$wuser) {
35	$miniserv{'passwd_cindex'} ne '' && $miniserv{'passwd_mindex'} ne '' ||
36		die "Missing password file configuration";
37	}
38
39if ($wuser) {
40	# Update Webmin user's password
41	$ok = &acl::validate_password($in{'old'}, $wuser->{'pass'});
42	$ok || &pass_error($text{'password_eold'});
43	$perr = &acl::check_password_restrictions($in{'user'}, $in{'new1'});
44	$perr && &pass_error(&text('password_enewpass', $perr));
45	$wuser->{'pass'} = &acl::encrypt_password($in{'new1'});
46	$wuser->{'temppass'} = 0;
47	&acl::modify_user($wuser->{'name'}, $wuser);
48	&reload_miniserv();
49	}
50elsif ($gconfig{'passwd_cmd'}) {
51	# Use some configured command
52	$passwd_cmd = &has_command($gconfig{'passwd_cmd'});
53	$passwd_cmd || &pass_error("The password change command <tt>$gconfig{'passwd_cmd'}</tt> was not found");
54
55	&foreign_require("proc", "proc-lib.pl");
56	&clean_environment();
57	$ENV{'REMOTE_USER'} = $in{'user'};	# some programs need this
58	$passwd_cmd .= " ".quotemeta($in{'user'});
59	($fh, $fpid) = &proc::pty_process_exec($passwd_cmd, 0, 0);
60	&reset_environment();
61	while(1) {
62		local $rv = &wait_for($fh,
63			   '(new|re-enter).*:',
64			   '(old|current|login).*:',
65			   'pick a password',
66			   'too\s+many\s+failures',
67			   'attributes\s+changed\s+on|successfully\s+changed',
68			   'pick your passwords');
69		$out .= $wait_for_input;
70		sleep(1);
71		if ($rv == 0) {
72			# Prompt for the new password
73			syswrite($fh, $in{'new1'}."\n", length($in{'new1'})+1);
74			}
75		elsif ($rv == 1) {
76			# Prompt for the old password
77			syswrite($fh, $in{'old'}."\n", length($in{'old'})+1);
78			}
79		elsif ($rv == 2) {
80			# Request for a menu option (SCO?)
81			syswrite($fh, "1\n", 2);
82			}
83		elsif ($rv == 3) {
84			# Failed too many times
85			last;
86			}
87		elsif ($rv == 4) {
88			# All done
89			last;
90			}
91		elsif ($rv == 5) {
92			# Request for a menu option (HP/UX)
93			syswrite($fh, "p\n", 2);
94			}
95		else {
96			last;
97			}
98		last if (++$count > 10);
99		}
100	$crv = close($fh);
101	sleep(1);
102	waitpid($fpid, 1);
103	if ($? || $count > 10 ||
104	    $out =~ /error|failed/i || $out =~ /bad\s+password/i) {
105		&pass_error("<tt>".&html_escape($out)."</tt>");
106		}
107	}
108elsif ($in{'pam'}) {
109	# Use PAM to make the change..
110	eval "use Authen::PAM;";
111	if ($@) {
112		&pass_error(&text('password_emodpam', $@));
113		}
114
115	# Check if the old password is correct
116	$service = $miniserv{'pam'} ? $miniserv{'pam'} : "webmin";
117	$pamh = new Authen::PAM($service, $in{'user'}, \&pam_check_func);
118	$rv = $pamh->pam_authenticate();
119	$rv == PAM_SUCCESS() ||
120		&pass_error($text{'password_eold'});
121	$pamh = undef;
122
123	# Change the password with PAM, in a sub-process. This is needed because
124	# the UID must be changed to properly signal to the PAM libraries that
125	# the password change is not being done by the root user.
126	$temp = &transname();
127	$pid = fork();
128	@uinfo = getpwnam($in{'user'});
129	if (!$pid) {
130		($>, $<) = (0, $uinfo[2]);
131		$pamh = new Authen::PAM("passwd", $in{'user'}, \&pam_change_func);
132		$rv = $pamh->pam_chauthtok();
133		open(TEMP, ">$temp");
134		print TEMP "$rv\n";
135		print TEMP ($messages || $pamh->pam_strerror($rv)),"\n";
136		close(TEMP);
137		exit(0);
138		}
139	waitpid($pid, 0);
140	open(TEMP, "<$temp");
141	chop($rv = <TEMP>);
142	chop($messages = <TEMP>);
143	close(TEMP);
144	unlink($temp);
145	$rv == PAM_SUCCESS || &pass_error(&text('password_epam', $messages));
146	$pamh = undef;
147	}
148else {
149	# Directly update password file
150
151	# Read shadow file and find user
152	&lock_file($miniserv{'passwd_file'});
153	$lref = &read_file_lines($miniserv{'passwd_file'});
154	for($i=0; $i<@$lref; $i++) {
155		@line = split(/:/, $lref->[$i], -1);
156		local $u = $line[$miniserv{'passwd_uindex'}];
157		if ($u eq $in{'user'}) {
158			$idx = $i;
159			last;
160			}
161		}
162	defined($idx) || &pass_error($text{'password_euser'});
163
164	# Validate old password
165	&unix_crypt($in{'old'}, $line[$miniserv{'passwd_pindex'}]) eq
166		$line[$miniserv{'passwd_pindex'}] ||
167			&pass_error($text{'password_eold'});
168
169	# Make sure new password meets restrictions
170	if (&foreign_check("changepass")) {
171		&foreign_require("changepass", "changepass-lib.pl");
172		$err = &changepass::check_password($in{'new1'}, $in{'user'});
173		&pass_error($err) if ($err);
174		}
175	elsif (&foreign_check("useradmin")) {
176		&foreign_require("useradmin", "user-lib.pl");
177		$err = &useradmin::check_password_restrictions(
178				$in{'new1'}, $in{'user'});
179		&pass_error($err) if ($err);
180		}
181
182	# Set new password and save file
183	$salt = chr(int(rand(26))+65) . chr(int(rand(26))+65);
184	$line[$miniserv{'passwd_pindex'}] = &unix_crypt($in{'new1'}, $salt);
185	$days = int(time()/(24*60*60));
186	$line[$miniserv{'passwd_cindex'}] = $days;
187	$lref->[$idx] = join(":", @line);
188	&flush_file_lines();
189	&unlock_file($miniserv{'passwd_file'});
190	}
191
192# Change password in Usermin too
193if (&get_product_name() eq 'usermin' &&
194    &foreign_check("changepass")) {
195	&foreign_require("changepass", "changepass-lib.pl");
196	&changepass::change_mailbox_passwords(
197		$in{'user'}, $in{'old'}, $in{'new1'});
198	&changepass::change_samba_password(
199		$in{'user'}, $in{'old'}, $in{'new1'});
200	}
201
202# Show ok page
203&header(undef, undef, undef, undef, 1, 1);
204print &ui_alert_box(&text('password_done', "/"), "success");
205&footer();
206
207sub pass_error
208{
209&header(undef, undef, undef, undef, 1, 1, undef, undef);
210print &ui_alert_box("$text{'password_err'}: @_.", "danger");
211&footer();
212exit;
213}
214
215sub pam_check_func
216{
217my @res;
218while ( @_ ) {
219	my $code = shift;
220	my $msg = shift;
221	my $ans = "";
222
223	$ans = $in{'user'} if ($code == PAM_PROMPT_ECHO_ON());
224	$ans = $in{'old'} if ($code == PAM_PROMPT_ECHO_OFF());
225
226	push @res, PAM_SUCCESS();
227	push @res, $ans;
228	}
229push @res, PAM_SUCCESS();
230return @res;
231}
232
233sub pam_change_func
234{
235my @res;
236while ( @_ ) {
237	my $code = shift;
238	my $msg = shift;
239	my $ans = "";
240	$messages = $msg;
241
242	if ($code == PAM_PROMPT_ECHO_ON()) {
243		# Assume asking for username
244		push @res, PAM_SUCCESS();
245		push @res, $in{'user'};
246		}
247	elsif ($code == PAM_PROMPT_ECHO_OFF()) {
248		# Assume asking for a password (old first, then new)
249		push @res, PAM_SUCCESS();
250		if ($msg =~ /old|current|login/i) {
251			push @res, $in{'old'};
252			}
253		else {
254			push @res, $in{'new1'};
255			}
256		}
257	else {
258		# Some message .. ignore it
259		push @res, PAM_SUCCESS();
260		push @res, undef;
261		}
262	}
263push @res, PAM_SUCCESS();
264return @res;
265}
266
267