摘自 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 权限,保持警觉仍旧是明智的做法。
脚本46 设置系统日期比起演示如何破坏系统,更有用的是告诉你如何识别系统中所有设置过 setuid 或 setgid 的 shell 脚本!代码清单 6-1 中给出了详细的做法。
简洁是 Linux 及其 Unix 先驱的核心,极大地影响了 Linux 的发展。但在某些场合,这种简洁会
让系统管理员抓狂。最常见的烦恼之一是重置系统日期所要求的格式,如下面所示的 date 命令:
usage: date [[[[[cc]yy]mm]dd]hh]mm[.ss]
代码 setdate找出所有的中括号就能把人搞晕,更别提哪些地方需要指定,哪些地方不用指定了。我们来
解释一下:你可以只输入分钟;或者分钟和秒;或者小时、分钟和秒;或者月份和其他所有部分;
还可以加上年份,甚至是世纪。这也太疯狂了!不用再猜了,代码清单 6-4 中的 shell 脚本可以提
示各个相关字段,然后构建出压缩形式的日期字符串。绝对能让你轻松不少。
#!/bin/bash # setdate -- 易用的date命令前端 # 日期格式:[[[[[cc]yy]mm]dd]hh]mm[.ss] # 为了便于用户使用,该函数提示特定的日期值 # 根据当前日期和时间在[]中显示默认值 . ../1/library.sh # 导入函数库,使用其中的函数echon() askvalue(){ # 1 # = 字段名, = 默认值, = 最大值, # = 所要求的字符/数字长度 echon " [] : " read answer if [ ${answe:=} -gt ];then echo "运行结果$ 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:: $answer is invalid" exit 0 elif [ "$(( $(echo $answer | wc -c) - 1))" -lt ];then echo "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: $answer is too short:please specify digits" exit 0 fi eval =$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
$ ./killall -n csmount kill -INT 1292 kill -INT 1296 kill -INT 1306 kill -INT 1310 kill -INT 1318脚本47 依据名字杀死进程
Linux 和一些 Unix 版本中有一个叫作 killall 的实用命令,你可以使用该命令杀死匹配特定 模式的所有进程。当你想杀死 9 个 mingetty 守护进程,或是向 xinetd 发送 SIGHUP 信号,提醒它 重新读取自己的配置文件的时候,这个命令就派上用场了。没有 killall 命令的系统可以利用 shell 脚本模拟该命令,这需要在其中用到 ps 和 kill,前者用于识别匹配的进程,后者负责发送 特定的信号。
脚本中最麻烦的地方在于 ps 的输出格式在不同的 *** 作系统之间差异很大。举例来说,考虑一下默认的 ps 输出在 FreeBSD、Red Hat Linux 以及 OS X 上的差别。先来看看 FreeBSD 上的输出:
"/tmp/crontab.Dj7Tr4vw6R":9: bad day-of-week crontab: errors in crontab file, can't install
先不说去比照这些 ps 命令,更糟的地方在于 GNU 的 ps 命令还能接受 BSD/SYSV/GNU 风格 的选项。彻底乱套了!
幸运的是,其中一些不一致性可以利用 cu 选项在该特定脚本中避开,这两个选项生成的结 果要一致得多,其中包括进程属主、完整的命令名以及我们真正感兴趣的进程 ID。
代码 killall这也是第一个真正发挥了 getopts 命令威力的脚本,它使得我们能够处理大量不同的命令行 选项,甚至是可选值。代码清单 6-6 中的脚本可以接受 4 个选项,即-s SIGNAL、-u USER、-t TTY 和-n,其中 3 个选项需要参数。在第一个代码块中你就可以看到这些选项的处理。
#!/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运行结果 精益求精
脚本运行的时候会出现一个不大可能但并非不存在的 bug。为了只匹配指定的模式,awk 会 输出匹配该模式以及出现在输入行末尾的前导空格的进程 ID。但是在理论上,可能会有两个进 程,比如说,一个叫作 bash,另一个叫作 emulate bash。如果调用 killall 的时候以 bash 作为 模式,那么这两个进程都能够匹配,但只有前者才是我们真正想要的。要想解决这个问题,以期 在不同的平台上获得一致的结果,可能是件相当棘手的事情。
脚本48 验证用户crontab条目如果你有这方面的考虑,也可以编写一个着重依赖于 killall 的脚本,除了进程 ID,还能够 依据作业名称对其执行 renice *** 作。唯一要做出的改动就是把 kill 换成 renice。调用 renice 可以修改进程的相对优先级,例如,允许你降低大文件传输的优先级,同时提高老板正在使用的 视频编辑器的优先级。
Linux 宇宙① 中最有用的工具之一就是 cron 了,它可以将作业安排在未来的任意时刻,或是 每分钟、每几个小时、每个月、甚至是每年自动执行作业。每位优秀的系统管理员都免不了有一 件脚本利器是通过 crontab 文件运行的。
但是,cron 规范的格式有点棘手,其字段内容可以是数字值、区间、集合,甚至是月份或者 一周中某天的名称。更糟糕的是 crontab 程序在碰到用户错误或系统 cron 文件错误的时候,产生 的相关信息晦涩难懂。
例如,如果在指定周几的时候出现了输入错误,那么 crontab 会报告类似于下面的信息:
实际上,在样例输入文件中的第 12 行还存在另一个错误,但是 crontab 非得让我们绕一圈 才找到问题所在,这都是拜其糟糕的错误检查功能所赐。
代码 verifycron现在可以放弃 crontab 的错误检查方式了,下面这个比较长的 shell 脚本(代码清单 6-8)能 够扫描 crontab 文件,检查语法,确保取值均在合理范围内。shell 脚本中实现这种验证是可行的, 原因之一在于可以将集合和区间视为多个单独的值。所以,要想确定 3-11,或者 4、6、9 是否为 可取的字段值,对于前者,只需要测试 3 和 11;对于后者,只需要测试 4、6、9。
#!/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 < # 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条)