Bash Cookbook 学习笔记 【高级】
Read Me
约定格式# 注释:前导的$表示命令提示符 # 注释:无前导的第二+行表示输出 # 例如: $ 命令 参数1 参数2 参数3 # 行内注释 输出_行一 输出_行二 $ cmd par1 par1 par2 # in-line comments output_line1 output_line2 七、编写安全的脚本安全是一个过程,而不是某种成品、对象、或技术,且没有终点。 -- Bruce Schneier 比如:
当然,以上也适用于所有的软件。 展开细说,先从脚本头 shebang !shebang #!/bin/bash 同时,内核也会接收解释器(比如 所以,最好在该位置用减号 #!/bin/bash - 但这样的路径 解决方法:通过原生的 $ env ... SHELL=/bin/bash ... 所以,这样写行了吗? #!/usr/bin/env bash - 还是不行。文件找不到了。 /usr/bin/env: ‘bash -’: No such file or directory linux和很多其他unix的 所以,可移植和安全性有点鱼和熊掌的意思,需要自己权衡轻重 # 轻安全,重移植 #!/usr/bin/env bash # 重安全,轻移植 #!/bin/bash - 最后,再提一个细节。有时候,你会看到有些脚本的 #! /bin/bash 安全路径 $PATH getconfshebang之后,写所有其他代码之前,请先设置安全路径
显式声明一遍PATH变量,并再次注册到运行环境。反斜杠用于禁用别名扩展功能。 PATH='/usr/local/bin:/bin:/usr/bin' export PATH
export PATH=$(getconf PATH)
$ getconf -a | grep PATH PATH_MAX 4096 _POSIX_PATH_MAX 4096 PATH /bin:/usr/bin CS_PATH /bin:/usr/bin 但第二种写法还存在个问题: 而且,变量声明和 # 移植性不好 export var='foo' # 最好拆开来写 var='foo'; export var
既然注册 这样写脚本会很长。所以最好打包进一个函数内,供各脚本调用。 #!/usr/bin/env bash # 工具查找 # 复制、移动、删除,每个系统都一样 _cp='/bin/cp' _mv='/bin/mv' _rm='/bin/rm' # 分支判断 case $(/bin/uname) in 'Linux') _cut='/bin/cut' _nice='/bin/nice' # [其他工具] ;; 'SunOS') _cut='/usr/bin/cut' _nice='/usr/bin/nice' # [其他工具] ;; # [其他系统环境] esac 当前路径 ./为了减少输入量,有些用户习惯把当前路径 # 当前路径 PATH=.:$PATH PATH=$PATH:. # 空路径 PATH='/bin:/usr/bin:' PATH='/bin::/usr/bin' 从安全的角度,这是很不好的习惯,尤其是对 因为对命令进行路径搜索是按 当前路径在搜索链的存在会造成一些不可控的结果。
如果当前路径放在最前 $ PATH='.:/bin:/usr/bin'; export PATH 此时在 $ cd /tmp; pwd /tmp $ ls # 此处中招了
点号 $ PATH='/bin:/usr/bin:.'; export PATH 你机子上恰好装有一款叫midnight commander程序,它的命令恰好是 $ PATH='/bin:/usr/bin:.'; export PATH $ mc file1 file2 # 此处再次中招 以上两个例子有点极端。但所谓安全,不正是预防此类小概率事件吗? 禁用别名恶意的别名类似木马(trojan),可以诱导用户执行不安全的命令。 看个简单的例子。 $ alias unalias=echo $ alias builtin=ls $ builtin unalias vi ls: unalias: No such file or directory ls: vi: No such file or directory $ unalias -a -a 通过使用别名,原生的 删除所有的别名,可以消除隐患。 unalias -a 敏感信息哈希表 hash当前运行环境下,执行过的命令会被添加到哈希表(hash),用于提高再次调用时的访问速度。 污染(poison)哈希表 # dog指向cat $ hash -p /bin/cat dog $ hash -l builtin hash -p /bin/cat cat builtin hash -p /bin/cat dog builtin hash -p /bin/stty stty builtin hash -p /usr/bin/clear clear
# 清理命令路径下的所有哈希值 hash -r 核转储 core dump
被转储的内存页面可能含有密码等信息,最好禁用该功能。 且最好是写入系统级的配置文件中,如 # 禁用脚本和相关进程的内核转储功能 可参考`man 1 bash`的相关章节 ulimit -H -c 0 -- # -H 硬上限 # -c 0 核转储大小限制为0,即禁用 明文密码首先一点,千万千万不要像这样写 $ ./某脚本 -u 用户 -p 密码 & [1] 13301 就算输入密码时,不回显到屏幕,也不行 read -s -p "password: " PASSWD; 因为,以参数形式传递给脚本的密码,始终是以明文的形式存在,通过 $ ps PID TTY TIME CMD 2348 pts/1 00:00:00 bash 9661 pts/1 00:00:00 ps 13301 pts/1 00:00:00 ./某脚本 -u 用户 -p 密码 & 如果避免不了要使用明文密码,可以单独放进其他用户没有查看权限的文件中 $ ./某问题脚本 ~.隐藏目录/密码文件 像这样间接引用,至少避免明文暴露的问题。
首先,哈希是不可逆的,你无法还原回原来的明文。也就是无法访问那些需要该明文密码的数据库。如此,你只能取消数据库的密码保护,有点得不偿失。 哈希给你的,只是一种"安全"的假象。还不如用明文。 对于明文,一种简单的防护措施,可以是ROT-13的形式,这个在前边介绍过。或用47个字符的扩展版本,除了大小写26个字母外,还支持标点。 $ ROT13=$(echo password | tr 'A-Za-z' 'N-ZA-Mn-za-m') $ ROT47=$(echo password | tr '!-~' 'P-~!-O') 这种打乱字母顺序的方式,有总比没有好点,至少不会让你产生"安全"的假象。 比以上更好的,是 文件权限 rwxrwxrwx默认掩码 umask
# 注意:该设置对命令行已被重定向的文件不会产生影响 # 设置成变量形式,便于根据需要修改 UMASK=002 umask $UMASK 侦测外部可写目录 【脚本】外部可写(world writable)目录,是任何其他用户都有可写权限的目录。当然,你肯定不希望此类权限出现在根用户的$PATH中。 最好能有个脚本,能检查指定路径下,此类不安全的目录是否存在。运行效果类似这样: $ ./chkpath.sh; echo $? ok drwxrwsr-x root staff /usr/local/bin ok drwxr-xr-x root root /usr/bin ok drwxr-xr-x root root /bin ok drwxrwsr-x root staff /usr/local/games ok drwxr-xr-x root root /usr/games 外部可写 drwxrwxrwt root root /tmp 符号链接,ok drwxr-xr-x root root /var/run 缺失 /不存在的目录 2 $ #!/usr/bin/env bash # 统计异常目录个数 exit_code=0 # 列举所有需要检查的目录; for dir in ${PATH//:/ } /tmp /var/run /不存在的目录 ; do # 如果是符号链接 [ -L "$dir" ] && printf "%b" "符号链接," # 如果不是目录 if [ ! -d "$dir" ]; then printf "%b" "缺失tttt" (( exit_code++ )) else # 显示目录自身 | 取 [权限,用户,组]三列 stat=$(ls -lHd $dir | awk '{print $1,$3,$4}') # 其他用户可写 if [ "$(echo $stat | grep '^d.......w. ')" ]; then printf "%b" "外部可写t$stat " (( exit_code++ )) else printf "%b" "oktt$stat " fi fi printf "%b" "$dirn" done exit $exit_code 该脚本的几个要点简单说明一下:
用$IFS=':'的形式也能切割变量,但灵活性不如符号替换。
你可以添加任意目录进来 for dir in 目录1 目录2 ...; do ... done 也可以在循环体内进行任意的条件测试 for dir in ...; do [ -L "$dir" ] && ... if [ ! -d "$dir" ]; then ... else ... if [ ... ]; then ... ... done
$ echo ${PATH//:/ } | xargs ls -ldH drwxr-xr-x 2 root root 4096 Jan 21 08:22 /bin drwxr-xr-x 2 root root 36864 Jan 21 08:23 /usr/bin drwxr-xr-x 2 root root 4096 Jul 13 2017 /usr/games drwxrwsr-x 2 root staff 4096 Jul 24 2017 /usr/local/bin drwxrwsr-x 2 root staff 4096 Jul 24 2017 /usr/local/games 更改权限 chmod
首先,权限可以有两种表现形式:
$ chmod 0755 some_script 很多人的习惯,是只使用后三位数。第一位是个特殊位,很少用到。但显式的写全四位能避免歧义。
$ chmod -x some_script $ chmod ugo+rx some_script 相对值假设你知道原来的权限,带有主观性。绝对值不会造成误判,更保险一些。 修改完之后最好用 关于批量修改:
$ chmod -R 0644 some_directory 正确的写法,是对文件和目录区别对待,以 $ find some_directory -type f | xargs chmod 0644 # 文件 $ find some_directory -type d | xargs chmod 0755 # 目录 创建新目录并设置权限,两个动作可以用一条命令完成,避免分开执行两条命令时,产生竞态(race condition)的隐患。 $ mkdir -m mode new_directory 批量修改权限前,你可能需要对整个系统或特定目录的权限设置先做备份。 备份文件系统的元数据 【脚本】#!/usr/bin/env bash # 文件名 archive_meta.sh printf "%b" "权限t用户t组t大小t修改时间t文件描述n" > archive_file find / ( -path /proc -o -path /mnt -o -path /tmp -o -path /var/tmp -o -path /var/cache -o -path /var/spool ) -prune -o -type d -printf 'd%mt%ut%gt%st%tt%p/n' -o -type l -printf 'l%mt%ut%gt%st%tt%p -> %ln' -o -printf '%mt%ut%gt%st%tt%pn' >> archive_file 其中的 $ sudo ./archive_meta.sh $ head archive_file 权限 用户 组 大小 修改时间 文件描述 d755 root root 4096 Tue Oct 31 04:45:47.2825806270 2017 // d555 root root 0 Fri Jan 26 06:35:45.5240001190 2018 /sys/ d755 root root 0 Fri Jan 26 06:35:45.5360001780 2018 /sys/kernel/ ... 这个脚本功能比较简单,只作为说明用。更专业的文件备份和完整性检查,可参考Tripwire等工具。 特殊权限 setuid setgid在脚本中设置特殊位setuid (用户 user)和setgid (组 group),造成的混乱比解决的问题,要多得多。强烈不建议使用。 简单介绍一下。
先分别创建两个普通的目录和文件 $ mkdir suid_dir sgid_dir; touch suid_file sgid_file; ls -l total 8 drwxr-xr-x 2 jimhs jimhs 4096 Jan 26 11:58 sgid_dir/ -rw-r--r-- 1 jimhs jimhs 0 Jan 26 11:58 sgid_file drwxr-xr-x 2 jimhs jimhs 4096 Jan 26 11:58 suid_dir/ -rw-r--r-- 1 jimhs jimhs 0 Jan 26 11:58 suid_file 四位权限绝对值的第一位数,4和2,就是setuid位和setgid位 $ chmod 4755 suid_dir suid_file $ chmod 2755 sgid_dir sgid_file 再次查看,已经设置好了。用户和组的 $ ls -l total 8 drwxr-sr-x 2 jimhs jimhs 4096 Jan 26 11:58 sgid_dir/ -rwxr-sr-x 1 jimhs jimhs 0 Jan 26 11:58 sgid_file* drwsr-xr-x 2 jimhs jimhs 4096 Jan 26 11:58 suid_dir/ -rwsr-xr-x 1 jimhs jimhs 0 Jan 26 11:58 suid_file*
这两个值会改变创建和从属关系,导致不可控的权限泄漏。这也是造成混乱的源头。所以,没有关注就没有伤害~ 隔离的环境随机数 $RANDOM在脚本运行环境,使用随机数命名的临时目录及文件,可以增加非法访问的难度。 最简单的随机数生成方式,是使用bash的内置变量 $ echo ${RANDOM}${RANDOM}${RANDOM} 68981103829905
# 随机临时目录 until [ -n "$temp_dir" -a ! -d "$temp_dir" ]; do temp_dir="/tmp/自定义前缀.${RANDOM}${RANDOM}${RANDOM}" done mkdir -p -m 0700 $temp_dir || { echo "FATAL: 无法创建临时目录'$temp_dir': $?"; exit 100 } # 随机临时文件 temp_file="$temp_dir/自定义前缀.${RANDOM}${RANDOM}${RANDOM}" touch $temp_file && chmod 0600 $temp_file || { echo "FATAL: 无法创建临时文件'$temp_file': $?"; exit 101 } # 退出前记得删除临时目录 cleanup="rm -rf $temp_dir" trap "$cleanup" ABRT EXIT HUP INT QUIT 相比起马上要介绍的其他方法, 也可以这样生成随机数: $ echo $( (last; who; free; date; echo $RANDOM) | md5sum | cut -d' ' -f1 ) c0b5676e55987de62432117842247286 即,将一组无规律的命令打包,然后将结果进行哈希,再从中取出特定字段来作为随机数。这样做有点取巧,只是提供一种思路。 更专业的实现方式,当然是使用 创建安全的临时目录或文件 【脚本】# 调用方法: # $temp_file=$(MakeTemp <file|dir> [path/to/name-prefix]) # 示例: # $temp_dir=$(MakeTemp dir /tmp/$PROGRAM.foo) # $temp_file=$(MakeTemp file /tmp/$PROGRAM.foo) function MakeTemp { # 首先,确保$TMP变量已设置 [ -n "$TMP" ] || TMP='/tmp' local temp_type='' local sanity_check='' # 类型 file或dir local type_name=$1 # 如果未指定前缀,则使用$TMP + temp local prefix=${2:-$TMP/temp} case $type_name in file ) temp_type='' ur_cmd='touch' # 条件测试: 是常规文件、可读、可写、只有我有访问权限 sanity_check='test -f $TEMP_NAME -a -r $TEMP_NAME -a -w $TEMP_NAME -a -O $TEMP_NAME' ;; dir|directory ) temp_type='-d' ur_cmd='mkdir -p -m0700' # 条件测试: 是目录、可读、可写、可执行、只有我有访问权限 sanity_check='test -d $TEMP_NAME -a -r $TEMP_NAME -a -w $TEMP_NAME -a -x $TEMP_NAME -a -O $TEMP_NAME' ;; * ) Error "n$PROGRAM:MakeTemp 参数错误! file或dir." 1 ;; esac # 先试下mktemp TEMP_NAME=$(mktemp $temp_type ${prefix}.XXXXXXXXX) # 失败的话,则用urandom if [ -z "$TEMP_NAME" ]; then TEMP_NAME="${prefix}.$(cat /dev/urandom | od -x | tr -d ' ' | head -1)" $ur_cmd $TEMP_NAME fi # 看下创建好没有,没有的话只能退出了 if ! eval $sanity_check; then Error "a致命错误: 无法创建$type_name with '$0:MakeTemp $*'!n" 2 else echo "$TEMP_NAME" fi } # MakeTemp函数结束 受限控制台 rbash
使用前,需要做些必要配置:
硬币的另一面:一些实用的程序被禁用后,肯定也影响到使用体验。而且,总会有漏网之鱼。所以, 监狱 chroot没错,这个是叫监狱(jail)。 很好理解,就是把那些可疑的坏脚本或程序,用 类似于构建了一道隐形的围墙, 但有些程序,天生需要被暴露给外边的网络,比如各种DNS、HTTP或邮件服务器等。功能越复杂,管理成本也越高。 扩展阅读,可参考wiki上关于强制访问控制MAC的介绍。 权限提升 sudo
使用前请先花点时间学习该命令、授权配置工具 类似 查看用户授权 $ sudo -l 查看 $ sudo sudo -V | less
sudo 命令1 && 命令2 || 命令3 正确的写法 $ sudo bash -c '命令1 && 命令2 || 命令3' 能用 输入验证所谓验证,就是定义一种模式,然后将用户输入与之比较,结果无外乎两种,要么匹配,要么不匹配。 常用的句法结构,可以是简单的一条语句 [模式] && 执行 复杂点的,可以是庞大的分支结构 case 模式1) 执行1 ;; 模式2) 执行2 ;; ... esac 这些在前边基础部分的测试/流程控制都已经都介绍过了。 本节着重讲如何定义验证模式,及如何拆解用户提供的选项和参数。并结合一些实例,来强化学习。 最简单的匹配语法,是像这样: [ 文件名 == *.jpg ] && echo "是jpg文件" # 模式不要用括号包裹。否则会被理解为字符本身 [ 文件名 == "*.jpg" ] && echo "是jpg文件" 在这里,星号 简单匹配 【简表】
简单匹配 【脚本】bash安装包的examples路径下,给出了一些输入验证的示范代码。
#examples/functions/isnum2 # 整数 isnum2() { case "$1" in '[-+]' | '') return 1;; # 为空,或只有正负号 [-+]*[!0-9]*) return 1;; # 有正负号,但不是数字 [-+]*) return 0;; # OK *[!0-9]*) return 1;; # 不是数字 *) return 0;; # OK esac } # 浮点数 isnum3() { case "$1" in '') return 1;; # 为空 *[!0-9.+-]*) return 1;; # 非数字、正负号或小数点 *?[-+]*) return 1;; # 符号不是首位 *.*.*) return 1;; # 小数点超过一个 *) return 0;; # OK esac }
# examples/functions/isvalidip is_validip() { case "$*" in ""|*[!0-9.]*|*[!0-9]) return 1 ;; esac local IFS=. # 以.作为分隔符 set -- $* # 将参数分隔后映射到位置变量 [ $# -eq 4 ] && [ ${1:-666} -le 255 ] && [ ${2:-666} -le 255 ] && [ ${3:-666} -le 255 ] && [ ${4:-666} -le 254 ] } bash 2.0之后,引入了双括号 # 启用**扩展匹配**(extended globbing) shopt -s extglob # 对大小写不敏感 shopt -s nocasematch # 匹配次数(关键字1|关键字2) if [[ 文件名 == *.@(jpg|jpeg) ]] then # ... 扩展匹配 【简表】
如果扩展匹配还是不能满足要求,就该正则表达式(以下简称regex)出场了。 在中级部分讲 正则表达式 【简表】其他工具,比如 [[ 文件名 =~ [[:alpha:]]{3,6}.jpg ]] && echo "是jpg文件" 方括号内的方括号,是POSIX字符集合,常用的包括: 复杂一点的例子。比如想用数字编号重命名CD曲目 $ ls Ludwig Van Beethoven - 01 - Allegro.ogg Ludwig Van Beethoven - 02 - Adagio un poco mosso.ogg Ludwig Van Beethoven - 03 - Rondo - Allegro.ogg Ludwig Van Beethoven - 04 - "Coriolan" Overture,Op. 62.ogg Ludwig Van Beethoven - 05 - "Leonore" Overture,No. 2 Op. 72.ogg $ 文件名的结构:
进一步抽象:
所以,最终的regex表达式:
三个圆括号包裹的子表达式,被映射到内置变量BASH_REMATCH数组中,数组第0项表示整条regex语句,其他分别按1、2、3等一一对应。它也是一个内置变量。 for CDTRACK in * do if [[ "$CDTRACK" =~ "([[:alpha:][:blank:]]*)- ([[:digit:]]*) - (.*)$" ]] then echo Track ${BASH_REMATCH[2]} is ${BASH_REMATCH[3]} mv "$CDTRACK" "Track${BASH_REMATCH[2]}" fi done 选项与参数 getops $OPTIND $OPTARG选项(option)有两种。 一种不带参数(argument),类似于一个开关,通过打开或关闭,来改变脚本的行为 # 分开 $ ls -a -l -h ... # 合并 $ ls -alh ... 另一种要带参数 $ mysql -u 用户名 除此之外的,都被视为非选项参数。 以上介绍了四个概念,用个完整的例子来演示: myscript -a -b alt plow harvest reap 其中:
在脚本中,如何接收和验证这些选项和参数? 先贴答案: #!/usr/bin/env bash #getopts.sh aflag= bflag= while getopts 'ab:' OPTION do case $OPTION in a) aflag=1 ;; b) bflag=1 bval="$OPTARG" ;; ?) printf "用法: %s: [-a] [-b value] argsn" $(basename $0) >&2 exit 2 ;; esac done shift $(($OPTIND – 1)) if [ "$aflag" ] then printf "选项 -a 已提供n" fi if [ "$bflag" ] then printf '选项 -b "%s" 已提供n' "$bval" fi printf "剩下的参数是: %sn" "$*" 脚本的核心部分是: getopts 'ab:' OPTION 内置命令
shift $(($OPTIND – 1))
所以,以下命令在 myscript -a -b alt plow harvest reap 位置参数 1 2 3 4 脚本中, printf "剩下的参数是: %sn" "$*" 运行效果: ./getopts.sh -ab alt plow harvest reap 选项 -a 已提供 选项 -b "alt" 已提供 剩下的参数是: plow harvest reap 自定义错误 【脚本】对于非法选项, 如需使用自定义的错误警告,则在 getopts ':ab:' OPTION 增加了自定义错误警告的脚本: #!/usr/bin/env bash #getopts.sh aflag= bflag= # printf "OPTIND: %dn" $OPTIND #OPTERR=0 while getopts :ab: FOUND do # printf "OPTIND: %dn" $OPTIND case $FOUND in a) aflag=1 ;; b) bflag=1 bval="$OPTARG" ;; :) # 反斜杠表示取消对冒号转义,下同 printf "%s 选项缺少参数n" $OPTARG printf "用法: %s: [-a] [-b value] argsn" $(basename $0) exit 2 ;; ?) printf "未知选项: -%sn" $OPTARG printf "用法: %s: [-a] [-b value] argsn" $(basename $0) exit 2 ;; esac >&2 done shift $(($OPTIND - 1)) if [ "$aflag" ] then printf "选项 -a 已提供n" fi if [ "$bflag" ] then printf '选项 -b "%s" 已提供n' "$bval" fi printf "剩下的参数是: %sn" "$*" 与前一个例子不同的几个地方:
运行效果: ./getopts.sh -a -b b 选项缺少参数 用法: getopts.sh: [-a] [-b value] args bash现在的主要维护者 Chet Ramey,在bash源代码目录下(examples/scripts/shprompt),给出了一个输入验证的完整模板。内容太长,这里不贴了。有兴趣的读者可以参考。 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |