#!/usr/bin/perl use strict; use Getopt::Std; my $ID = '$Id: sysaudit.linux,v 1.13 2004/10/25 20:28:19 spangla Exp $'; my $VERSION = '$Revision: 1.13 $'; $VERSION=(split / /,$VERSION)[1]; # pick off raw version number $ENV{PATH}="/sbin:/bin:/usr/sbin:/usr/bin"; # parse command line options my %opt; getopts('visdt',\%opt) or die usage(); die "only one of [ -i | -s | -t ] are allowed together\n" if 1< $opt{i} + $opt{s} + $opt{t}; # behavior of system my $verbose =$opt{v} && !$opt{i} && !$opt{t}; my $interactive =$opt{i}; my $silent =$opt{s}; my $debug =$opt{d}; my $terse =$opt{t}; sub usage{ print; <<_ Usage: $0 [-v] Audit system read only $0 -i Interactively make changes $0 -s Silently make all changes $0 -t Terse listing of non-compliant sections _ } # sanity check die "This script must be run as root\n" if $>; die "This script is only for Linux\n" if `uname` ne "Linux\n"; # this is based upon the Linux Security Template v1.0 7/13/2004 # Note that there are several versions of Template v1.0 my $standard=[ {sect=>1, name=>"System Services and Processes", report=> \&checkInetSvcs, fix=> \&disableInetSvcs }, {sect=>"1a", name=>"System Services and Processes File Perms", report=> sub{ check_perms("/etc/sysconfig/network 600 root")}, fix=> sub{ fix_perms("/etc/sysconfig/network 600 root")}, }, # section 2 - Service Documentation - no action {sect=>2, name=>"Service Documentation - Record a machine's remote accessible services (include associated ports) and processes.", report=> sub{ check_perms("/etc/services 644 root")}, fix=> sub{ fix_perms("/etc/services 644 root")}, }, # section 3 - Naming System - no action {sect=>4, name=>"IP - Turn off IP Forwarding", report=> sub{ check_ndd("net.ipv4.ip_forward=0") }, fix=> sub{ fix_ndd("net.ipv4.ip_forward=0") }, }, {sect=>5, name=> "IP - Enable reverse path filtering.", report=> sub{ check_ndd( "net.ipv4.conf.all.rp_filter=1", "net.ipv4.conf.lo.rp_filter=1", "net.ipv4.conf.eth0.rp_filter=1", "net.ipv4.conf.default.rp_filter=1", )}, fix=> sub{ fix_ndd( "net.ipv4.conf.all.rp_filter=1", "net.ipv4.conf.lo.rp_filter=1", "net.ipv4.conf.eth0.rp_filter=1", "net.ipv4.conf.default.rp_filter=1", )}, }, {sect=>6, name=> "IP - Configure system to disable the ability to respond to source routed packets.", report=> sub{ check_ndd( "net.ipv4.conf.all.accept_source_route=0", "net.ipv4.conf.lo.accept_source_route=0", "net.ipv4.conf.eth0.accept_source_route=0", "net.ipv4.conf.default.accept_source_route=0", )}, fix=> sub{ fix_ndd( "net.ipv4.conf.all.accept_source_route=0", "net.ipv4.conf.lo.accept_source_route=0", "net.ipv4.conf.eth0.accept_source_route=0", "net.ipv4.conf.default.accept_source_route=0", )}, }, {sect=>7, name=> "TCP Timestamps - Disable the ability to respond to timestamp requests.", report=> sub{ check_ndd("net.ipv4.tcp_timestamps=0") }, fix => sub{ fix_ndd("net.ipv4.tcp_timestamps=0") }, }, {sect=>8, name=> "ICMP Receiving Redirect errors - Disable the ability to accept ICMP redirect errors.", report=> sub{ check_ndd( "net.ipv4.conf.all.accept_redirects=0", "net.ipv4.conf.lo.accept_redirects=0", "net.ipv4.conf.eth0.accept_redirects=0", "net.ipv4.conf.default.accept_redirects=0", )}, fix => sub{ fix_ndd( "net.ipv4.conf.all.accept_redirects=0", "net.ipv4.conf.lo.accept_redirects=0", "net.ipv4.conf.eth0.accept_redirects=0", "net.ipv4.conf.default.accept_redirects=0", )}, }, {sect=>9, name=> "ICMP Sending Redirect errors - Disable the ability to send ICMP redirect errors.", report=> sub{ check_ndd( "net.ipv4.conf.all.send_redirects=0", "net.ipv4.conf.lo.send_redirects=0", "net.ipv4.conf.eth0.send_redirects=0", "net.ipv4.conf.default.send_redirects=0", )}, fix => sub{ fix_ndd( "net.ipv4.conf.all.send_redirects=0", "net.ipv4.conf.lo.send_redirects=0", "net.ipv4.conf.eth0.send_redirects=0", "net.ipv4.conf.default.send_redirects=0", )}, }, {sect=>10, name=> "ICMP Echo Request - Disable the ability to respond to echo request broadcasts.", report=> sub{ check_ndd("net.ipv4.icmp_echo_ignore_broadcasts=1") }, fix => sub{ fix_ndd("net.ipv4.icmp_echo_ignore_broadcasts=1") }, }, # Sect 11: SNMP is required for the Proliant Support Pack required for HW RAID. {sect=>18, name=>"File System Access - Ensure that NFS filesystems have correct contros", report=> \&check_am, fix=> \&fix_am, }, {sect=>19, name=>"NFS Servers - Only designated fileservers should be allowed to export file systems.", report=> \&check_nfs, fix=> \&fix_nfs, }, {sect=>37, name=>"Kernel Parameters - Limit the maximum number of process per user.", report=> \&check_limits, fix=> \&fix_limits, }, {sect=>34, name=>"Sendmail - Ensure that /etc/aliases are writable by root only", report=> sub{ check_perms("/etc/aliases 644 root")}, fix=> sub{ fix_perms("/etc/aliases 644 root")}, }, {sect=>40, name=>"System File Permissions - Ensure system directories are only writable by root or privileged ID's.", report=> sub{ check_perms( "/ 755 root root", "/bin 755 root root", "/sbin 755 root root", "/usr 755 root root", "/usr/bin 755 root root", "/usr/lib 755 root root", "/usr/sbin 755 root root", "/var/spool 755 root root", "/dev 755 root root", "/etc/security 755 root root", "/etc/inittab 644 root", "/etc/rc.d/init.d 755 root root", "/etc/rc.d/rc0.d 755 root root", "/etc/rc.d/rc1.d 755 root root", "/etc/rc.d/rc2.d 755 root root", "/etc/rc.d/rc3.d 755 root root", "/etc/rc.d/rc4.d 755 root root", "/etc/rc.d/rc5.d 755 root root", "/etc/rc.d/rc6.d 755 root root", "/root 750 root root", "/etc/shadow 400 root root", )}, fix=> sub{ fix_perms( "/ 755 root root", "/bin 755 root root", "/sbin 755 root root", "/usr 755 root root", "/usr/bin 755 root root", "/usr/lib 755 root root", "/usr/sbin 755 root root", "/var/spool 755 root root", "/dev 755 root root", "/etc/security 755 root root", "/etc/inittab 644 root", "/etc/rc.d/init.d 755 root root", "/etc/rc.d/rc0.d 755 root root", "/etc/rc.d/rc1.d 755 root root", "/etc/rc.d/rc2.d 755 root root", "/etc/rc.d/rc3.d 755 root root", "/etc/rc.d/rc4.d 755 root root", "/etc/rc.d/rc5.d 755 root root", "/etc/rc.d/rc6.d 755 root root", "/root 750 root root", "/etc/shadow 400 root root", )}, }, {sect=>43, name=>"Crontab Configuration - Ensure that the crontab configuration files can only be modified by the administrative root account.", report=> sub{ check_perms( "/etc/cron.d 700 root", "/var/spool/cron 700 root", "/var/spool/at 700 ", "/var/spool/at/spool 700 ", )}, fix=> sub{ fix_perms( "/etc/cron.d 700 root", "/var/spool/cron 700 root", "/var/spool/at 700 ", "/var/spool/at/spool 700 ", )}, }, {sect=>47, name=>"Startup Files - Ensure that startup files are not world writable", report=> sub{ checkfix_perms_init() }, fix=> sub{ checkfix_perms_init(1) }, }, {sect=>49, name=>"Public Directories - Make sure sticky bit is set on public directories", report=> sub{ check_perms( "/tmp 1777 root", "/var/tmp 1777 root", )}, fix=> sub{ fix_perms( "/tmp 1777 root", "/var/tmp 1777 root", )}, }, {sect=>59, name=>"Terminal Messaging - limit the ability to send messages to terminal sessions.", report=> \&check_profile_msg, fix=> \&fix_profile_msg, }, {sect=>61, name=>"Single-User Mode - Enable password prompt for root in single-user mode.", report=> \&check_sulogin, fix=> \&fix_sulogin, }, {sect=>93, name=>"System Logging - Make sure logs are retained based on company retention policies.", report=> \&check_logrotate, fix=> \&fix_logrotate, }, {sect=>95, name=>"Failed Logins - Enabled failed login monitoring.", report=> \&check_failedlogin, fix=> \&fix_failedlogin, }, # stuff from the "Unix Security Standard" Doc (no version listed) {sect=>"UID0", name=> "Multiple Root-Level Accounts - Ensure that only the root account has a ID (UID) of 0 (zero).", report=> \&check_uid_zero, fix=> sub { check_uid_zero("fix") }, }, ]; # filehandling junk to make life easier sub readfile{ my ($filename)=@_; local *F; print "* reading $filename ...\n" if $debug; open F,$filename or die "open: $filename: $!\n"; if (wantarray){ #read whole thing as array my @data=; close F; return @data; } else { #read whole thing as scalar local $/=undef; my $data=; close F; return $data; } } sub writefile{ my ($filename,@contents)=@_; print "* writing $filename ...\n" if $debug; backupfile($filename); # always make backups local *F; open F,">$filename" or die "write: $filename: $!\n"; print F @contents; close F; } # common way to backup config files sub backupfile{ use POSIX qw/strftime/; my ($filename)=@_; my $backup="$filename.".strftime("%y%m%d",localtime); return if -f $backup; # don't clobber today's backup print "* backing up $filename to $backup ...\n" if $debug; system qq'cp -p "$filename" "$backup"'; } my @badServices=qw/ ftp telnet rsh shell login exec talk finger netstat time echo discard daytime chargen rquotad sgi_fam xfs pcmcia vlan tftp rwalld/; # reports on the services that are currently enabled # based upon the /etc/inetd.conf sub checkInetSvcs{ my %conf; my $report=""; # now do the report foreach (@badServices){ my $val=`/sbin/chkconfig --list $_ 2>/dev/null`; my $status="not listed"; $status="disabled" if $val; $status="enabled" if $val=~/\bon\b/; print "1.$_ " if $terse and $status eq "enabled"; # hack next if !$verbose and $status ne "enabled"; $report.=sprintf "\t%-16s %s\n",$_,$status; } $report="" if $terse; # hack $report; } sub disableInetSvcs{ foreach my $svc (@badServices){ my $val=`/sbin/chkconfig --list $svc 2>/dev/null`; next unless $val=~/\bon\b/; if ($interactive){ system "/sbin/chkconfig $svc off" if askUser("\nDo you want disable: $svc ?\n"); } else { system "/sbin/chkconfig $svc off"; } } } # User and group are optional sub check_perms{ my $report; foreach (@_){ my ($file,$mod,$uid,$gid)=split; print "* check_perms: file=$file mod=$mod uid=$uid gid=$gid\n" if $debug; $uid=undef if $uid eq ""; $gid=undef if $gid eq ""; # convert names numbers $mod=oct($mod) if $mod; # ensure octal $uid=(getpwnam $uid)[2] if $uid && $uid !~/^\d+$/; $gid=(getpwnam $gid)[2] if $gid && $gid !~/^\d+$/; # stat the actual file my ($fmod,$fuid,$fgid)=(stat $file)[2,4,5]; $report.= "\t$file: Wrong Perms!\n" if defined $mod && $mod != ($fmod & 4095); $report.= "\t$file: Wrong Owner!\n" if defined $uid && $uid != $fuid; $report.= "\t$file: Wrong Group!\n" if defined $gid && $gid != $fgid; } $report= "\tCompliant.\n" if $verbose && !$report; return $report; } # User and group are optional sub fix_perms{ foreach (@_){ my ($file,$mod,$uid,$gid)=split; print "* fix_perms: file=$file mod=$mod uid=$uid gid=$gid\n" if $debug; $uid=undef if $uid eq ""; $gid=undef if $gid eq ""; # convert names numbers $mod=oct($mod) if $mod; # ensure octal $uid=(getpwnam $uid)[2] if $uid && $uid !~/^\d+$/; $gid=(getpwnam $gid)[2] if $gid && $gid !~/^\d+$/; # stat the actual file my ($fmod,$fuid,$fgid)=(stat $file)[2,4,5]; $uid=$fuid if !defined $uid; # defaults $gid=$fgid if !defined $gid; # defaults # make the changes as necessary chown $uid,$gid,$file if $uid != $fuid || $gid != $fgid; chmod $mod,$file if defined $mod && $mod != ($fmod & 4095); } } sub check_ndd{ my $report; my $data=readfile "/etc/sysctl.conf"; foreach (@_){ my ($var,$val)=split /[\t =]+/; print "* check_ndd: var=$var val=$val\n" if $debug; #my $realtimedata=`sysctl -n $var`; # enable for sanity check # follows what an auditor might do $report.="\t$var: Not Compliant!\n" if $data!~/\n$var\s*=\s*$val\s*\n/s; } $report="\tCompliant.\n" if !$report && $verbose; return $report; } sub fix_ndd{ my $data=readfile "/etc/sysctl.conf"; my $mod; foreach (@_){ my ($var,$val)=split /[\t =]+/; print "* fix_ndd: var=$var val=$val\n" if $debug; $mod.="$var=$val\n" if $data!~/\n$var\s*=\s*$val\s*\n/s; } writefile "/etc/sysctl.conf","$data\n#Added by sysaudit\n$mod" if $mod; } # routine to both check and fix sub check_uid_zero{ my ($fix)=@_; my @data=readfile "/etc/passwd"; # are we running in compatability that allows + in /etc/passwd ? # (Item 18) my $compat=grep /^passwd:\s+compat\b/,readfile "/etc/nsswitch.conf"; my $report; foreach (@data){ my ($user,$crypt,$uid,$gid,$gecos,$homedir,$shell)=split/:/; next if $user=~/^+/ && $compat; # ignore + if in compat mode if ($user ne "root" && $uid == 0){ $report.=sprintf "\tFound %s User\n",$user; if ($fix){ backupfile "/etc/passwd"; backupfile "/etc/shadow"; print "removing password entry for $user ($uid)\n" if $debug; system "/usr/sbin/userdel $user"; } } } $report="\tCompliant.\n" if $verbose && !$report; $report; } sub askUser{ my ($msg)=@_; $|++; print "$msg [y/N]: "; my $ans=; $ans=~/^y/i; } sub check_am{ if (grep /^\/net\s/,readfile "/etc/auto.master"){ return "\tFound /net in /etc/auto.master.\n"; } $verbose ? "\tCompliant.\n" : undef; } sub fix_am{ my @data=readfile "/etc/auto.master"; map { s+^(/net)+#$1+ } @data and writefile "/etc/auto.master",@data; } sub check_nfs{ my $nfs_runs=`/sbin/chkconfig --list nfs 2>/dev/null`; if ($nfs_runs =~/\bon\b/ && ! -s "/etc/exports"){ return "\tNon fileserver allowed to start nfs!\n"; } $verbose ? "\tCompliant.\n" : undef; } sub fix_nfs{ my $nfs_runs=`/sbin/chkconfig --list nfs 2>/dev/null`; if ($nfs_runs =~/\bon\b/ && ! -s "/etc/exports"){ system "/sbin/chkconfig nfs off"; } } sub check_limits{ local $_=readfile "/etc/security/limits.conf"; if (/\n\s*\*\s+soft\s+nproc\s+/ && /\n\s*\*\s+soft\s+nproc\s+/){ return $verbose ? "\tCompliant.\n" : undef; } return "\tMissing NPROC limits in /etc/security/limits.conf!\n"; } sub fix_limits{ local $_=readfile "/etc/security/limits.conf"; /\n\s*\*\s+soft\s+nproc\s+/ or $_.="*\t\tsoft\tnproc\t\t400\n"; /\n\s*\*\s+hard\s+nproc\s+/ or $_.="*\t\thard\tnproc\t\t1000\n"; writefile "/etc/security/limits.conf",$_; } sub checkfix_perms_init{ my ($fix)=@_; my $report; foreach ( , qw"/etc/rc.d/rc /etc/rc.d/rc.local /etc/rc.d/rc.sysinit" ){ my $mode=(stat $_)[2]; if ($mode & 022){ $report.="\tFile $_ is Writable!\n"; $mode&=~022; chmod $mode,$_ if $fix; } } $report="\tCompliant.\n" if $verbose && !$report; $report; } sub check_profile_msg{ my @data=readfile "/etc/profile"; if (grep /^\s*mesg n\s*$/,@data){ return $verbose ? "\tCompliant.\n" : undef; } else { return "\tMissing 'mesg n' in /etc/profile!\n"; } } sub fix_profile_msg{ my $data=readfile "/etc/profile"; writefile "/etc/profile",$data,"\n#Added by sysaudit\nmesg n\n"; } sub check_sulogin{ my @data=readfile "/etc/inittab"; if (grep m@:[Ss]:wait:/sbin/sulogin@,@data){ return $verbose ? "\tCompliant.\n" : undef; } else { return "\tPassword not required for single user mode!\n"; } } sub fix_sulogin{ my $data=readfile "/etc/inittab"; writefile "/etc/inittab",$data,"sum:S:wait:/sbin/sulogin\n"; } sub check_logrotate{ my $data=readfile "/etc/logrotate.conf"; my ($keep)=$data=~m/\nrotate (\d+)/; if ($keep >= 5){ return $verbose ? "\tCompliant.\n" : undef; } else { return "\tLogs must be kept for at least 30 days!\n"; } } sub fix_logrotate{ my $data=readfile "/etc/logrotate.conf"; $data=~s/\nrotate (\d+)/\nrotate 5/ or $data="#keep 30 days\nrotate 5\n$data"; writefile "/etc/logrotate.conf",$data; } sub check_failedlogin{ my $data=readfile "/etc/syslog.conf"; if ($data=~m/\nauth,authpriv\.\*/s){ return $verbose ? "\tCompliant.\n" : undef; } else { return "\tFailed logins are not properly logged!\n"; } } sub fix_failedlogin{ my $data=readfile "/etc/syslog.conf"; $data=~s/\nauthpriv\.\*/\nauth,authpriv.*/s; writefile "/etc/syslog.conf",$data; } # # Main program starts here # my $hostname=`uname -n`; chomp $hostname; my $date=`date`; chomp $date; if (!$silent && !$terse){ print "x" x 75,"\n"; print "Sysaudit (Linux) report [v$VERSION] for $hostname on $date\n"; print "x" x 75,"\n"; } foreach (@$standard){ my $report=$_->{report}->(); if ($report){ if (!$terse){ print "\n", $_->{sect} ? "Section: $_->{sect} . " : "", $_->{name} . "\n", "\n", $report if !$silent; } else { # terse print $_->{sect}," "; } if ($interactive){ askUser("Do you want the system to be changed?\n") or next; $debug++; printf("Performing changes to system\n"); $_->{fix}->(); $debug--; } elsif ($silent) { $_->{fix}->(); } } } print "\n" if $terse;