高级 shell

Table of Contents

1 trap

假设有一个 shell 脚本,它在中途会产生一些临时文件,但在工作作期间突然被中断执行了,那这些临时文件是不能自动清理的。有了 trap,就可以捕获中断信号,然后做相应的文件清理。

trap 可以想象为 shell 的异常处理,trap 根据捕获进程信号来执行指定的命令。

命令格式:

trap command 信号

Linux 具体的信号可以通过 man 获得: man 7 signal。只是 trap 命令写信号时不需要“SIG”开头。

例:

trap 'echo bye' INT

while (true); do
    echo '1'
done

当 Ctrl+c 中断脚本时,会执行 echo bye。注意 trap 的位置,要在写在被“保护”的代码之前。

2 eval

将字符串交给 shell 再次扫描并执行,比如x文件的内容:

$ cat x
ls /etc

然后用 eval 可以让 x 里的内容再次执行:

eval $(cat x)

例,远程另外一台机器登录我的笔记本,无法 ssh-add:

$ ssh-add
Could not open a connection to your authentication agent.

这个时候需要执行 ssh-agent 的内容:

$ ssh-agent
SSH_AUTH_SOCK=/tmp/ssh-mVzghWGagC5W/agent.11859; export SSH_AUTH_SOCK;
SSH_AGENT_PID=11860; export SSH_AGENT_PID;
echo Agent pid 11860;

便可以用 eval:

$ eval $(ssh-agent)
Agent pid 11864

谨慎使用 eval,eval 会产生一定的安全隐患,如果输入源不可控就有可能产生任意命令执行漏洞。

3 set

-e:脚本执行时一出错就中断执行 -u:引用了不存在的变量时报错

set –:将参数分别设置到$1、$2等变量中

4 过程替换

语法:<(命令)

当调用“<(…)”时,会在产生一个文件,文件内容就是中间命令输出的内容。

场景1,给 spark-shell -i 传递参数

spark-shell -i可以指定Scala脚本文件名,加载并执行。但某些情况下需要传递参数,例如传递日期。

if [ $# -eq 1 ]
then
    year=$(expr substr $1 1 4)
    month=$(expr substr $1 5 2)
    day=$(expr substr $1 7 2)
else
    year=$(date +'%Y' -d '-1days')
    month=$(date +'%m' -d '-1days')
    day=$(date +'%d' -d '-1days')
fi

spark-shell -i <(
    echo "val yyyyMMdd = ${year}${month}${day}";
    echo "val yyyyMM = ${year}${month}";
    cat main.scala
)

场景2,省去中间结果文件

有一个文件 a,我需要对某条命令执行结果和文件 a 做 diff 对比,一般是将命令执行结果写到一个中间文件中再执行 diff,但可以用过程替换给省去中间结果文件:

diff -u a <(command)

5 script

script 命令可以将所有 shell 上的显示写入到文件中(默认是 typescript),比如我在 FreeBSD 中编译内核提示出错,就可用 script 命令将内核编译时打印输出内容全部写入到某个文件里(如果不指定文件,默认记录到 typescript 文件中),然后再在这个文件中找出错点即可。

例:

script /tmp/log
echo 'hello world'
exit
cat /tmp/log
hello world

exit 即可退出 script 环境,并且 script 会在记录文件中添加结束时间。

6 命令堆栈

用 pushd 把当前目录保存到命令堆栈中:

$ cd /etc/rc.d
$ pushd
~ /etc/rc.d

dirs 命令可以浏览命令堆栈中的内容:

$ dirs
~ /etc/rc.d

popd 可以弹出最近的堆栈目录,然后切换过去:

$ cd /tmp
$ popd
/etc/rc.d
$ pwd
/etc/rc.d

堆栈内容可以移动:

pushd +1
pushd -1

7 调试

bash -x

Bashdb(bashdb.sourceforge.net)+ RoalGUD(https://github.com/realgud/realgud

8 shell 安全

由于 shell 简单灵活,一不小心就掉进坑里,我们要时刻注意一些问题,这些问题有可能会造成安全隐患。

8.1 LD_PRELOAD 环境变量

在国外网上流传了这么一段脚本,号称运行后就可获得 root 权限:

#!/bin/sh
echo "1|nux r007 3xp10|7 by 1c4m7uf"
cd /tmp
cat >ex.c <<eof
int getuid() { return 0; }
int geteuid() { return 0; }
int getgid() { return 0; }
int getegid() { return 0; }
eof
gcc -shared ex.c -oex.so
LD_PRELOAD=/tmp/ex.so sh
rm /tmp/ex.so /tmp/ex.c

执行结果如下:

bash test.sh
1|nux r007 3xp10|7 by 1c4m7uf
sh-4.3# id
uid=0(root) gid=0(root) 组=0(root),1000(lu4nx) 环境=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
sh-4.3#

这里要注意脚本用到了 LD_PRELOAD 环境变量,它可以指定一个动态链接库在加载其他 .so (比如 glibc)之前优先加载。上面的代码实质上编译了一个动态链接库,那段 C 代码重新实现了 getuid、geteuid、getgid 和 getegid,并且全部返回 0(返回 0 表示 root),等同劫持了 getuid 等函数。shell 权限在判断是 root 时,提示符会变成“#”;而 id 命令也是调用了这几个函数。这里实际上只是个假象,并没有真正获得 root 权限,但要注意这个环境变量可能导致其他程序逻辑判断。

8.2 通配符问题

shell 在遇到字符“*”时,会把它转变成当前目录下全部文件,比如目录下有 a、b 和 c 三个文件,当执行“ls *”时,shell 会将最终执行的命令转变成“ls a b c”。

假如目录下有一个文件名叫“-l”:

$  ls
-l

当执行:ls *时,-l 会被展开成参数:

$  ls *
总用量 0
-rw-rw-r--. 1 lu4nx lu4nx 0 12月  8 10:37 -l

利用这个特性,我们可以把文件名构造得像参数一样,很容易引起安全隐患。

例1,当前目录下的两个文件:

$ ls
-c  '__import__('\''os'\'').system('\''id'\'')'

当执行 python * 时就会出问题:

$  python *
uid=1000(lu4nx) gid=1000(lu4nx) 组=1000(lu4nx) 环境=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023

例2,当目录下有这几个文件时:

$ ls
-exec  id

执行 find 命令:

$  find * \;
uid=1000(lu4nx) gid=1000(lu4nx) 组=1000(lu4nx) 环境=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
uid=1000(lu4nx) gid=1000(lu4nx) 组=1000(lu4nx) 环境=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
uid=1000(lu4nx) gid=1000(lu4nx) 组=1000(lu4nx) 环境=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023

唯一鸡肋的地方是命令必须加“;”,通过对 Bash 的源码调试,发现如果创建了一个叫“;”的文件,通配符展开时,“;”比较靠前,导致无法正确解析。展开后的命令如下:

find ; -exec id