shell 脚本实战 八

摘自 shell脚本实战 第二版 第六章 系统管理:系统维护

脚本45 跟踪设置过setuid的程序

无论流氓和数字犯罪分子有没有账户,他们都有很多方法可以闯入 Linux 系统,最简单的一 种方法是寻找错误设置 setuid 或 setgid 的命令。前面章节讲过,这种命令会根据配置修改其所 调用的子命令的有效用户 ID,因此一个普通用户所运行的脚本,其中的命令能够以 root 或超级 用户的身份执行。这就太糟糕,太危险了!

举例来说,在一个设置了 setuid 的 shell 脚本中,加入下面的代码之后,如果系统管理员以 root 身份登录,毫无戒心地执行了这段代码,就会为坏人创建一个 setuid root shell。

if [ "${USER:-$LOGNAME}" = "root" ] ; then  		# REMOVEME
	cp /bin/sh /tmp/.rootshell 						# REMOVEME
	chown root /tmp/.rootshell 						# REMOVEME
	chmod -f 4777 /tmp/.rootshell 					# REMOVEME
	grep -v "# REMOVEME" $0 > /tmp/junk 			# REMOVEME
	mv /tmp/junk $0 								# REMOVEME
fi													# REMOVEME

如果脚本不小心被 root 运行,那么/bin/sh 的副本会被偷偷地复制到/tmp,更名为.rootshell, 然后设置 setuid 为 root,以供破坏者随意利用。最后,脚本重写自身,删掉条件语句(也就是末尾带有# REMOVEME 的那些行),抹去证据。

上面这段代码也可以用于其他任何脚本或命令,只要它们是以 root 作为有效用户 ID 运行的 就行。因此,关键在于确保要知晓并逐一许可系统中所有的 setuid root 命令。有鉴于此,你绝不 应该让脚本有任何形式的 setuid 或 setgid 权限,保持警觉仍旧是明智的做法。

比起演示如何破坏系统,更有用的是告诉你如何识别系统中所有设置过 setuid 或 setgid 的 shell 脚本!代码清单 6-1 中给出了详细的做法。

脚本46 设置系统日期

简洁是 Linux 及其 Unix 先驱的核心,极大地影响了 Linux 的发展。但在某些场合,这种简洁会
让系统管理员抓狂。最常见的烦恼之一是重置系统日期所要求的格式,如下面所示的 date 命令:

usage: date [[[[[cc]yy]mm]dd]hh]mm[.ss]

找出所有的中括号就能把人搞晕,更别提哪些地方需要指定,哪些地方不用指定了。我们来
解释一下:你可以只输入分钟;或者分钟和秒;或者小时、分钟和秒;或者月份和其他所有部分;
还可以加上年份,甚至是世纪。这也太疯狂了!不用再猜了,代码清单 6-4 中的 shell 脚本可以提
示各个相关字段,然后构建出压缩形式的日期字符串。绝对能让你轻松不少。

代码 setdate

#!/bin/bash

# setdate -- 易用的date命令前端
# 日期格式:[[[[[cc]yy]mm]dd]hh]mm[.ss]

# 为了便于用户使用,该函数提示特定的日期值
# 根据当前日期和时间在[]中显示默认值

. ../1/library.sh               # 导入函数库,使用其中的函数echon()

askvalue(){                     # 1

        # $1 = 字段名,$2 = 默认值, $3 = 最大值,
        # $4 = 所要求的字符/数字长度

        echon "$1 [$2] : "
        read answer
        if [ ${answe:=$2} -gt $3 ];then
                echo "$0: $1 $answer is invalid"
                exit 0
        elif [ "$(( $(echo $answer | wc -c) - 1))" -lt $4 ];then
                echo "$0: $1 $answer is too short:please specify $4 digits"
                exit 0
        fi
        eval $1=$answer         # 使用指定的值重新载入变量
}

eval $(date "+nyear=%Y nmon=%m nday=%d nhr+%H nmin=%M") # 2 使用eval给 nyear nmon nday nhr nmin 这些变量赋值

askvalue year $nyear 3000 4
askvalue month $nmon 12 2
askvalue day $nday 31 2
askvalue hour $nhr 24 2
askvalue minute $nmin 59 2


squished="$year$month$day$hour$minute"

# 如果你使用的是linux系统
# squished="$month$day$hour$minute$year  3 Linux的日期格式
# 没错,Linux和OS X/BSD系统采用的是不同的格式。有用吧


echo "Setting date to $squished.You might need to enter your sudo password:"
sudo date $squished

exit 0

运行结果

$ setdate 
year [2017] :
month [05] :
day [07] :
hour [16] : 14 
minute [53] : 50 
Setting date to 201705071450. You might need to enter your sudo password: 
passwd:

脚本47 依据名字杀死进程

Linux 和一些 Unix 版本中有一个叫作 killall 的实用命令,你可以使用该命令杀死匹配特定 模式的所有进程。当你想杀死 9 个 mingetty 守护进程,或是向 xinetd 发送 SIGHUP 信号,提醒它 重新读取自己的配置文件的时候,这个命令就派上用场了。没有 killall 命令的系统可以利用 shell 脚本模拟该命令,这需要在其中用到 ps 和 kill,前者用于识别匹配的进程,后者负责发送 特定的信号。

脚本中最麻烦的地方在于 ps 的输出格式在不同的操作系统之间差异很大。举例来说,考虑一下默认的 ps 输出在 FreeBSD、Red Hat Linux 以及 OS X 上的差别。先来看看 FreeBSD 上的输出:

BSD $ ps 
PID  TT STAT TIME    COMMAND
792  0  Ss   0:00.02 -sh (sh) 
4468 0  R+   0:00.01 ps

# 然后是Red Hat Linux上的输出
RHL $ ps
 PID  TTY     TIME     CMD
 8065 pts/4   00:00:00 bash
12619 pts/4   00:00:00 ps

# 最后是OS X上的输出
 PID  TTY      TIME    CMD
37055 ttys000  0:00.01 -bash
26881 ttys001  0:00.08 -bash

先不说去比照这些 ps 命令,更糟的地方在于 GNU 的 ps 命令还能接受 BSD/SYSV/GNU 风格 的选项。彻底乱套了!

幸运的是,其中一些不一致性可以利用 cu 选项在该特定脚本中避开,这两个选项生成的结 果要一致得多,其中包括进程属主、完整的命令名以及我们真正感兴趣的进程 ID。

这也是第一个真正发挥了 getopts 命令威力的脚本,它使得我们能够处理大量不同的命令行 选项,甚至是可选值。代码清单 6-6 中的脚本可以接受 4 个选项,即-s SIGNAL、-u USER、-t TTY 和-n,其中 3 个选项需要参数。在第一个代码块中你就可以看到这些选项的处理。

代码 killall

#!/bin/bash

# killall -- 向匹配指定进程名的所有进程发送特定信号

# 默认情况下,脚本只杀死属于同一用户的进程,除非你是root
# -s SIGNAL 可指定发送给进程的信号, -u USER可以指定用户
# -t TTY可以指定tty, -n只报告操作结果,不报告具体过程


signal="-INT"                                   # 默认发送中断信号
user=""   tty=""   donothing=0

while getopts "s:u:t:n" opt;do
        case "$opt" in
                # 注意下面的技巧:实际的kill命令需要的是-SIGNAL,
                # 但我们想要用户指定的是SIGNAL,所以要在前面加上"-".

                s ) signal="-$OPTARG";          ;;
                u ) if [ ! -z "$tty" ];then
                        # 逻辑错误:你不能同时指定用户和TTY设备
                        echo "$0: Error : -u and -t are mutually exclusive." > &2
                        exit 1
                    fi

                    user=$OPTARG;               ;;
                t ) if [ ! -z "$user" ];then
                        echo "$0: Error : -u and -t are mutually exclusive." > &2
                        exit 1
                    fi
                    
                    tty=$2;                     ;;
                n ) donothing=1;                ;;
                ? ) echo "Usage: $0 [ -s signal ] [ -u user | -t tty ] [ -n ] pattern" > &2
                    exit 1
        esac
done

# getopts 的选项处理完成
if [ $# -eq 0 ];then
        pids=$(ps cu -t $tty |awk "/ $1$/ {print \$2 }")                 # 1 匹配$1 所在的行 $2 输出pid 信息
elif [ ! -z "$user" ];then
        pids=$(ps cu -U $user |awk "/ $1$ {print \$2 }")                 # 2 匹配$1 所在的行 $2 输出pid 信息
else
        pids=$(ps cu -U ${USER:-LOGNAME} | awk "/ $1$/ {print \$2 }")    # 3 匹配$1 所在的行 $2 输出pid 信息
fi

# 没找到匹配的进程?这简单!
if [ -z "$pids" ];then
        echo "$0: no processes match pattern $1" > &2
        exit 1
fi

for pid in $pids;do
        # 向id为$pid的进程发送信号$signal:如果进程已经结束,或是用户没有
        # 杀死特定进程的权限,等等,那么kill命令可能会有所抱怨、不过没关系
        # 反正任务至少是完成了
        if [ $donothing -eq 1 ];then
                echo "kill $signal $pid"        # -n 选项: “显示,但不执行”
        else
                kill $signal $pid
        fi
done

exit 0

运行结果

$ ./killall -n csmount 
kill -INT 1292 
kill -INT 1296 
kill -INT 1306 
kill -INT 1310 
kill -INT 1318

精益求精

脚本运行的时候会出现一个不大可能但并非不存在的 bug。为了只匹配指定的模式,awk 会 输出匹配该模式以及出现在输入行末尾的前导空格的进程 ID。但是在理论上,可能会有两个进 程,比如说,一个叫作 bash,另一个叫作 emulate bash。如果调用 killall 的时候以 bash 作为 模式,那么这两个进程都能够匹配,但只有前者才是我们真正想要的。要想解决这个问题,以期 在不同的平台上获得一致的结果,可能是件相当棘手的事情。

如果你有这方面的考虑,也可以编写一个着重依赖于 killall 的脚本,除了进程 ID,还能够 依据作业名称对其执行 renice 操作。唯一要做出的改动就是把 kill 换成 renice。调用 renice 可以修改进程的相对优先级,例如,允许你降低大文件传输的优先级,同时提高老板正在使用的 视频编辑器的优先级。

脚本48 验证用户crontab条目

Linux 宇宙① 中最有用的工具之一就是 cron 了,它可以将作业安排在未来的任意时刻,或是 每分钟、每几个小时、每个月、甚至是每年自动执行作业。每位优秀的系统管理员都免不了有一 件脚本利器是通过 crontab 文件运行的。

但是,cron 规范的格式有点棘手,其字段内容可以是数字值、区间、集合,甚至是月份或者 一周中某天的名称。更糟糕的是 crontab 程序在碰到用户错误或系统 cron 文件错误的时候,产生 的相关信息晦涩难懂。

例如,如果在指定周几的时候出现了输入错误,那么 crontab 会报告类似于下面的信息:

"/tmp/crontab.Dj7Tr4vw6R":9: bad day-of-week 
crontab: errors in crontab file, can't install

实际上,在样例输入文件中的第 12 行还存在另一个错误,但是 crontab 非得让我们绕一圈 才找到问题所在,这都是拜其糟糕的错误检查功能所赐。

现在可以放弃 crontab 的错误检查方式了,下面这个比较长的 shell 脚本(代码清单 6-8)能 够扫描 crontab 文件,检查语法,确保取值均在合理范围内。shell 脚本中实现这种验证是可行的, 原因之一在于可以将集合和区间视为多个单独的值。所以,要想确定 3-11,或者 4、6、9 是否为 可取的字段值,对于前者,只需要测试 3 和 11;对于后者,只需要测试 4、6、9。

代码 verifycron

#!/bin/bash
# verifycron--Checks a crontab file to ensure that it's
#    formatted properly. Expects standard cron notation of
#       min hr dom mon dow CMD,    
#    where min is 0-59, hr is 0-23, dom is 1-31, mon is 1-12 (or names)
#    and dow is 0-7 (or names). Fields can be ranges (a-e) or lists
#    separated by commas (a,c,z) or an asterisk. Note that the step 
#    value notation of Vixie cron (e.g., 2-6/2) is not supported by 
#    this script in its current version.


validNum()
{
  # Return 0 if the number given is a valid integer and 1 if not. 
  # Specify both number and maxvalue as args to the function.
  num=$1   max=$2

  # Asterisk values in fields are rewritten as "X" for simplicity,
  #   so any number in the form "X" is de facto valid.

  if [ "$num" = "X" ] ; then
    return 0
  elif [ ! -z $(echo $num | sed 's/[[:digit:]]//g') ] ; then
    # Stripped out all the digits, and the remainder isn't empty? No good.
    return 1
  elif [ $num -gt $max ] ; then
    # Number is bigger than the maximum value allowed.
    return 1
  else
    return 0
  fi
}

validDay()
{
  # Return 0 if the value passed to this function is a valid day name, 
  #   1 otherwise.

  case $(echo $1 | tr '[:upper:]' '[:lower:]') in
    sun*|mon*|tue*|wed*|thu*|fri*|sat*) return 0 ;;
    X) return 0 ;;         # Special case--it's a rewritten "*"
    *) return 1
  esac
}

validMon()
{
  # This function returns 0 if given a valid month name, 1 otherwise.

   case $(echo $1 | tr '[:upper:]' '[:lower:]') in 
     jan*|feb*|mar*|apr*|may|jun*|jul*|aug*) return 0           ;;
     sep*|oct*|nov*|dec*)                    return 0           ;;
     X) return 0 ;; # special case, it's an "*"
     *) return 1        ;;
   esac
}

fixvars()
{
  # Translate all '*' into 'X' to bypass shell expansion hassles.
  # Save original input as "sourceline" for error messages.

  sourceline="$min $hour $dom $mon $dow $command"
   min=$(echo "$min" | tr '*' 'X')      # minute
  hour=$(echo "$hour" | tr '*' 'X')     # hour
   dom=$(echo "$dom" | tr '*' 'X')      # day of month
   mon=$(echo "$mon" | tr '*' 'X')      # month
   dow=$(echo "$dow" | tr '*' 'X')      # day of week
}

if [ $# -ne 1 ] || [ ! -r $1 ] ; then
  # If no crontab filename is given or it's not readable by the script, fail.
  echo "Usage: $0 usercrontabfile" >&2; exit 1
fi

lines=0  entries=0  totalerrors=0

# Go through the crontab file line by line, checking each one.

while read min hour dom mon dow command
do
  lines="$(( $lines + 1 ))" 
  errors=0
  
  if [ -z "$min" -o "${min%${min#?}}" = "#" ] ; then
    # If it's a blank line or the first character of the line is "#", skip it.
    continue    # Nothing to check
  fi


  ((entries++))

  fixvars

  # At this point, all the fields in the current line are split out into
  # separate variables, with all asterisks replaced by "X" for convenience,
  # so let's check the validity of input fields...

  # Minute check

  for minslice in $(echo "$min" | sed 's/[,-]/ /g') ; do
    if ! validNum $minslice 60 ; then
      echo "Line ${lines}: Invalid minute value \"$minslice\""
      errors=1
    fi
  done

  # Hour check
  
  for hrslice in $(echo "$hour" | sed 's/[,-]/ /g') ; do
    if ! validNum $hrslice 24 ; then
      echo "Line ${lines}: Invalid hour value \"$hrslice\"" 
      errors=1
    fi
  done

  # Day of month check

  for domslice in $(echo $dom | sed 's/[,-]/ /g') ; do
    if ! validNum $domslice 31 ; then
      echo "Line ${lines}: Invalid day of month value \"$domslice\""
      errors=1
    fi
  done

  # Month check: Has to check for numeric values and names both.
  #   Remember that a conditional like "if ! cond" means that it's
  #   testing whether the specified condition is FALSE, not true.

  for monslice in $(echo "$mon" | sed 's/[,-]/ /g') ; do
    if ! validNum $monslice 12 ; then
      if ! validMon "$monslice" ; then
        echo "Line ${lines}: Invalid month value \"$monslice\""
        errors=1
      fi
    fi
  done

  # Day of week check: Again, name or number is possible.

  for dowslice in $(echo "$dow" | sed 's/[,-]/ /g') ; do
    if ! validNum $dowslice 7 ; then
      if ! validDay $dowslice ; then
        echo "Line ${lines}: Invalid day of week value \"$dowslice\""
        errors=1
      fi
    fi
  done

  if [ $errors -gt 0 ] ; then
    echo ">>>> ${lines}: $sourceline"
    echo ""
    totalerrors="$(( $totalerrors + 1 ))"
  fi
done < $1 # read the crontab passed as an argument to the script

# Notice that it's here, at the very end of the "while" loop, that we
#   redirect the input so that the user-specified filename can be 
#   examined by the script!

echo "Done. Found $totalerrors errors in $entries crontab entries."

exit 0

运行结果

$ crontab -l > my.crontab 
$ verifycron my.crontab 
$ rm my.crontab

# 在包含无效条目的crontab文件上运行脚本verifycron
$ verifycron sample.crontab 
Line 10: Invalid day of week value "Mou" 
>>>> 10: 06 22 * * Mou /home/ACeSystem/bin/del_old_ACinventories.pl

Line 12: Invalid minute value "99" 
>>>> 12: 99 22 * * 1-3,6 /home/ACeSystem/bin/dump_cust_part_no.pl

Done. Found 2 errors in 13 crontab entries.

精益求精

脚本中有几处地方有必要做出改进。验证月份和日期组合的兼容性可以确保用户不会把 cron 作业安排在 2 月 31 日运行。检查是否能成功调用指定的命令也很有用,不过这涉及解析并处理 PATH 变量(一系列目录,要在其中查找脚本中指定的命令),该变量可以在 crontab 文件中明确设 置。最后,还可以加入对@hourly 或@reboot 的支持,cron 使用这些特殊值表示常用的脚本运行 时间。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值