#! @PERL@ # # Sanitise Linux password and group databases # # (c) 1998 Mark Wooding # use MdwOpt; use FileHandle; use POSIX; #----- Documentation -------------------------------------------------------- =head1 NAME shadowfix - fix password and group files =head1 SYNOPSIS B I... =head1 DESCRIPTION Shadowfix trundles through your various password files and makes sure that they're consistent with themselves. Currently, the checks Shadowfix makes, and their resolutions, are: =over 4 =item * Every user named in the password file should have a shadow password entry; create a shadow password entry if necessary. =item * Every password field in the password file indicates only presence or absence of a password; move a realistic-looking password to the shadow password file, and ensure that the password entry in the password file is either empty (signifying no password) or an `x' character (signifying a password). =item * The primary group of each user exists; warn about nonexistent primary groups. =item * Every user is a member of his primary group; add the user to the membership list of the group where necessary. =item * There are no entries in the shadow password file which don't match entries in the main password file; delete orphaned shadow password entries. =item * Check group and shadow group files for consistency, as for password and shadow password files: every group entry has a shadow entry, no passwords in the group file, no orphaned shadow group entries. =item * The lists of group members are consistent between group and shadow group files; edit the shadow group list to match the main group list where necessary. =item * Group administrators, listed in the shadow group file, are real users; warn about nonexistent group administrators. =back A lot of the checks above only make sense when shadow password and group files are used. When instructed not to create shadow files, Shadowfix will perform the password/group consistency checks as described above. Also, if given shadow files as input, and told not to create shadow files on output, Shadowfix will merge the password information back into the main files. Obviously, translating shadowed to non-shadowed files involved information loss: in particular, information about password expiry and group administration is lost. It's time to examine the command line options. =over 4 =item B<--passwd=>I Use I as the main password file. =item B<--group=>I Use I as the main group file. =item B<--shadow=>I Use I as the shadow password file. =item B<--gshadow=>I Use I as the shadow group file. =item B<--in-passwd=>I Read main password file entries from I. =item B<--in-group=>I Read main group file entries from I. =item B<--in-shadow=>I Read shadow password file entries from I. =item B<--in-gshadow=>I Read shadow group file entries from I. =item B<--quiet> Suppresses Shadowfix's informative messages about what it's doing to your passwored files. Enabling this option is not recommended. =back If the input files aren't specified explitly, Shadowfix defaults to trying to read the output files. These default sensibly to the system password and shadow files in F. Shadowfix knows about locking password files, so C and C will interact with it properly. =head1 FILES =over 4 =item F, F, F, F System default password and group files. =back =head1 BUGS Shadowfix doesn't understand how to cope with YP password files. Yellow Pages is a security hole; don't use it. =head1 AUTHOR Mark Wooding, =cut #----- Configuration section ------------------------------------------------ $passwd = "/etc/passwd"; $group = "/etc/group"; $shadow = "/etc/shadow"; $gshadow = "/etc/gshadow"; $passwd_in = $shadow_in = undef; $group_in = $gshadow_in = undef; $suyb = 0; #----- Subroutines ---------------------------------------------------------- sub hashify { map { $_, 1 } @_; } sub moan { print STDERR "shadowfix: @_\n"; } sub uidsort { if ($a eq $b) { return 0; } elsif ($a eq "+") { return +1; } elsif ($b eq "+") { return -1; } elsif (!exists($ubynam{$a})) { return +1; } elsif (!exists($ubynam{$b})) { return -1; } else { return $ubynam{$a}{uid} <=> $ubynam{$b}{uid} || $a cmp $b; } } sub gidsort { if ($a eq $b) { return 0; } elsif ($a eq "+") { return +1; } elsif ($b eq "+") { return -1; } else { return $gbynam{$a}{gid} <=> $gbynam{$b}{gid} || $a cmp $b; } } sub lockfile { my $file = shift; my $mode = shift or 0644; my $fh = new FileHandle; $fh->open("${file}.lock", O_WRONLY | O_EXCL | O_CREAT, 0600) or die "couldn't obtain lock file ${file}.lock"; $fh->print($$); $fh->close; $fh->open("${file}.edit", O_WRONLY | O_TRUNC | O_CREAT, $mode) or do { unlink "${file}.lock"; die "open(${file}.edit): $!"; }; return $fh; } sub unlockfile { my $file = shift; my $fh = shift; $fh->close; # --- See whether the file changed --- CHECK: { my ($ofh, $nfh); my ($obuf, $nbuf); my ($osz, $nsz); # --- Open the old and new versions for reading --- $ofh = new FileHandle $file, O_RDONLY; $nfh = new FileHandle "${file}.edit", O_RDONLY; last CHECK if !$ofh || !$nfh; # --- Read blocks from each and compare --- BLOCK: for (;;) { $osz = sysread($ofh, $obuf, 4096); $nsz = sysread($nfh, $nbuf, 4096); last CHECK if !defined($osz) || !defined($nsz); last CHECK if $obuf ne $nbuf; last BLOCK if $sz == 0; } # --- The files are identical --- # moan "file $file is unchanged"; unlink("${file}.edit"); unlink("${file}.lock"); return; } # --- Find the current owner --- # system("diff -u $file $file.edit"); if (-e $file) { my ($mode, $uid, $gid); (undef, undef, $mode, undef, $uid, $gid) = stat $file; chmod $mode, "${file}.edit"; chown $uid, $gid, "${file}.edit"; } # --- Move the old file out of the way --- !-e $file or rename("${file}", "${file}-") or do { unlink "${file}.lock"; unlink "${file}.edit"; die "couldn't save backup copy of $file: $!"; }; # --- Move the new one into place --- rename ("${file}.edit", "${file}") or do { rename("${file}-", "${file}"); # This shouldn't happen! unlink "${file}.lock"; unlink "${file}.edit"; die "HELP!!! couldn't save backup copy of $file: $!"; }; # --- Release the lock --- moan "updated $file" unless $suyb; unlink("${file}.lock"); } #----- Main code ------------------------------------------------------------ # --- Options parsing --- $longopts = { 'passwd' => { return => 'p', arg => 'opt' }, 'in-passwd' => { return => 'ip', arg => 'opt' }, 'shadow' => { return => 'ps', arg => 'opt' }, 'in-shadow' => { return => 'ips', arg => 'opt' }, 'group' => { return => 'g', arg => 'opt' }, 'in-group' => { return => 'ig', arg => 'opt' }, 'gshadow' => { return => 'gs', arg => 'opt' }, 'in-gshadow' => { return => 'igs', arg => 'opt' }, 'quiet' => { return => 'q', negate => 1 } }; $opts = MdwOpt->new("", $longopts, \@ARGV, ['negate', 'noshort']); OPT: while (($opt, $arg) = $opts->read, $opt) { $passwd = $arg, next OPT if $opt eq 'p'; $passwd_in = $arg, next OPT if $opt eq 'ip'; $shadow = $arg, next OPT if $opt eq 'ps'; $shadow_in = $arg, next OPT if $opt eq 'ips'; $group = $arg, next OPT if $opt eq 'g'; $group_in = $arg, next OPT if $opt eq 'ig'; $gshadow = $arg, next OPT if $opt eq 'gs'; $gshadow_in = $arg, next OPT if $opt eq 'igs'; $suyb = 1, next OPT if $opt eq 'q'; $suyb = 0, next OPT if $opt eq 'q+'; die "bad option"; } $passwd_in = $passwd unless $passwd_in; $shadow_in = $shadow unless $shadow_in; $group_in = $group unless $group_in; $gshadow_in = $gshadow unless $gshadow_in; # --- Initialise the user tables --- %ubynam = %ubyuid = %subynam = (); %gbynam = %gbygid = %sgbynam = (); # --- Slurp the user tables into memory --- $pw = new FileHandle $passwd_in, O_RDONLY or die "open($passwd_in): $!"; while ($line = $pw->getline) { chomp $line; my @f = split /:/, $line; $#f = 6; $a = { data => [ @f ], name => $f[0], uid => $f[2], gid => $f[3] }; $ubynam{$a->{name}} = $ubyuid{$a->{uid}} = $a; } $pw->close; $gr = new FileHandle $group_in, O_RDONLY or die "open($group_in): $!"; while ($line = $gr->getline) { chomp $line; my @f = split /:/, $line; $#f = 3; $a = { data => [ @f ], members => { hashify(split /,/, $f[3]) }, name => $f[0], gid => $f[2] }; $gbynam{$a->{name}} = $gbygid{$a->{gid}} = $a; } $gr->close; undef $have_shadow; if ($shadow_in) { if ($spw = new FileHandle $shadow_in, O_RDONLY) { while ($line = $spw->getline) { chomp $line; my @f = split /:/, $line; $#f = 8; $a = { data => [ @f ], name => $f[0] }; $subynam{$a->{name}} = $a; } $spw->close; $have_shadow = 1; } else { die "open($shadow_in): $!" unless $! == ENOENT; } } undef $have_gshadow; if ($gshadow_in) { if ($sgr = new FileHandle $gshadow_in, O_RDONLY) { while ($line = $sgr->getline) { chomp $line; my @f = split /:/, $line; $#f = 3; $a = { data => [ @f ], members => { hashify (split /,/, $f[3]) }, name => $f[0] }; $sgbynam{$a->{name}} = $a; } $sgr->close; $have_gshadow = 1; } else { die "open($gshadow_in): $!" unless $! == ENOENT; } } # --- Check primary group memberships --- for $u (values %ubynam) { $unam = $u->{name}; if (exists $gbygid{$u->{gid}}) { $g = $gbygid{$u->{gid}}; unless ($unam eq "+" || exists($g->{members}{$unam})) { moan "user $unam is not a member of his/her primary group" unless $suyb; $g->{members}{$unam} = 1; } } else { moan "user $unam seems to belong to a nonexistant group" unless $suyb; } } # --- Shadow password checks --- if ($shadow) { # --- Full shadowing checks --- for $u (values %ubynam) { $unam = $u->{name}; # --- Ensure there's a shadow password entry --- unless ($unam eq "+" || exists($subynam{$unam})) { moan "user $unam not in shadow password file: adding" unless $suyb; $subynam{$unam} = { name => $unam, data => [$unam, $u->{data}[1], 10205, 0, 99999, 7, "", "", ""] }; } # --- Mark unloginable shadow password entries --- $su = $subynam{$unam}; $p = $su->{data}[1]; if ($p ne "*" && length($p) > 0 && length($p) < 5) { moan "blanked user ${unam}'s password" unless $suyb; $su->{data}[1] = "*"; } # --- Blank out normal password entries --- if ($unam eq "+") { # Nothing doing } elsif ($p eq "") { $u->{data}[1] = ""; } else { $u->{data}[1] = "x"; } } # --- Remove shadow entries which don't make sense any more --- for $su (values %subynam) { $unam = $su->{name}; unless (exists($ubynam{$unam})) { moan "user $unam only in shadow password file: deleting" unless $suyb; delete $subynam{$su->{name}}; } } } elsif ($have_shadow) { # --- We have shadowing, but aren't writing out entries --- for $u (values %ubynam) { $unam = $u->{name}; $u->{data}[1] = $subynam{$unam}{data}[1] if exists($subynam{$unam}); } } # --- Shadow group checks --- for $g (values %gbynam) { $gnam = $g->{name}; # --- Ensure there's a shadow group entry --- unless (!$gshadow || $gnam eq "+" || exists($sgbynam{$gnam})) { moan "group $gnam not in shadow group file: adding" unless $suyb; $sgbynam{$gnam} = { name => $gnam, data => [$gnam, $g->{data}[1], "", $g->{data}[3]], members => { %{$g->{members}} } }; } # --- Play games with passwords --- if ($gshadow) { # --- Mark unloginable shadow group entries --- $sg = $sgbynam{$gnam}; $p = $sg->{data}[1]; if ($p ne "*" && length($p) > 0 && length($p) < 5) { moan "blanked group ${gnam}'s password" unless $suyb; $sg->{data}[1] = "*"; } # --- Blank out normal passwords --- $g->{data}[1] = "x" unless $gnam eq "+"; # --- Check that the group's administrators exist --- if ($sg->{data}[2] ne "" && !$suyb) { my @admins = my $admin; foreach $admin (split(/,/, $sg->{data}[2])) { exists $ubynam{$admin} or moan "user $admin owns group $gnam but doesn't seem to exist"; } } } elsif ($have_gshadow) { $g->{data}[1] = $sgbynam{$gnam}{data}[1] if exists($sgbynam{$gnam}); $sg = undef; } # --- The group members should be consistent across both files --- for $i (keys %{$g->{members}}) { exists $ubynam{$i} or $suyb or moan "user $i is a member of group $gnam but doesn't seem to exist"; unless (!$sg || exists($sg->{members}{$i})) { moan "group $gnam does not include $i in shadow group file: adding" unless $suyb; $sg->{members}{$i} = 1; } } if ($sg) { for $i (keys %{$sg->{members}}) { unless (exists($g->{members}{$i})) { moan "group $gnam does not include $i in main group file: deleting" unless $suyb; delete $sg->{members}{$i}; } } } } # --- Remove entries which are only in the shadow file --- if ($gshadow) { for $sg (values %sgbynam) { $gnam = $sg->{name}; unless (exists($gbynam{$gnam})) { moan "group $gnam only in shadow group file: deleting" unless $suyb; delete $sgbynam{$gnam}; } } } # --- Fix up the data blocks --- for $g (values %gbynam) { $g->{data}[3] = join(",", sort uidsort keys %{$g->{members}}); } if ($gshadow) { for $sg (values %sgbynam) { $sg->{data}[3] = join(",", sort uidsort keys %{$sg->{members}}); } } # --- Output the finished work of art --- $pw = lockfile($passwd, 0644); for $unam (sort uidsort keys %ubynam) { $pw->print(join(":", @{$ubynam{$unam}{data}}), "\n"); } unlockfile($passwd, $pw); if ($shadow) { $spw = lockfile($shadow, 0640); for $unam (sort uidsort keys %subynam) { $spw->print(join(":", @{$subynam{$unam}{data}}), "\n"); } unlockfile($shadow, $spw); } $gr = lockfile($group, 0644); for $gnam (sort gidsort keys %gbynam) { $gr->print(join(":", @{$gbynam{$gnam}{data}}), "\n"); } unlockfile($group, $gr); if ($gshadow) { $sgr = lockfile($gshadow, 0640); for $gnam (sort gidsort keys %sgbynam) { $sgr->print(join(":", @{$sgbynam{$gnam}{data}}), "\n"); } unlockfile($gshadow, $sgr); } #----- More subroutines ----------------------------------------------------- sub udump { my $u = shift; printf "name = %s\n", $u->{name}; printf "uid = %d, gid = %d\n", $u->{uid}, $u->{gid}; printf "data = %s\n", join(":", @{$u->{data}}); print "\n"; } sub sudump { my $u = shift; printf "name = %s\n", $u->{name}; printf "data = %s\n", join(":", @{$u->{data}}); print "\n"; } sub gdump { my $g = shift; printf "name = %s\n", $g->{name}; printf "gid = %d\n", $g->{gid}; printf "members = %s\n", join(",", sort uidsort keys %{$g->{members}}); printf "data = %s\n", join(":", @{$g->{data}}); print "\n"; } sub sgdump { my $g = shift; printf "name = %s\n", $g->{name}; printf "members = %s\n", join(",", sort uidsort keys %{$g->{members}}); printf "data = %s\n", join(":", @{$g->{data}}); print "\n"; } #----- That's all, folks ----------------------------------------------------