第15章 安全的Shell脚本:起点

背景 Shell脚本很容易引入安全漏洞;本章讲解写安全脚本的原则、限制性shell、特洛伊木马、setuid脚本风险。 注意:本章仅shell层面安全,不是完整系统安全教程。

15.1 安全Shell脚本编写提示

目标:抵御输入篡改、环境篡改、木马、竞争条件等攻击。下面是实操要点+示例。

1. PATH环境变量安全

  1. 不要把当前目录.放入PATH,会触发特洛伊木马风险。
  2. PATH内的目录,权限必须严格:目录仅属主可写,其他用户不能写。
  3. 脚本开头强制重置PATH为可信系统路径,不要继承外部传入PATH。

安全初始化示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 重置IFS为标准:空格、制表符、换行
IFS=$' \t\n'

# 清除别名,防止别名劫持内置命令
unset -f unalias
\unalias -a
unset -f command

# 获取系统可信PATH
SYSPATH="$(command -p getconf PATH 2>/dev/null)"
if [[ -z "$SYSPATH" ]]; then
    SYSPATH="/usr/bin:/bin"
fi
PATH="$SYSPATH"   # 只使用可信系统路径
command 和 getconf 详解

command -p 与 getconf 拆解

1
2
3
4
5
SYSPATH="$(command -p getconf PATH 2>/dev/null)"
if [[ -z "$SYSPATH" ]]; then
    SYSPATH="/usr/bin:/bin"
fi
PATH="$SYSPATH"

目的:拿到系统出厂默认、干净可信的 PATH,完全不继承当前环境被篡改过的 PATH,安全脚本专用。

  1. command 内置命令

command 是 shell 内置命令。 语法:

command [-p] 命令 [参数]

两个核心作用:

  1. 绕开函数:即使你写了同名 shell 函数,command xxx 强制执行外部真实命令,不去调用函数,防止函数劫持。
  2. ‑p 选项(重点):使用系统默认的安全 PATH去查找程序,忽略你当前脚本环境里被修改、被污染的 PATH 变量。

command -p:不管现在$PATH被攻击者改成什么,使用系统内置原始搜索路径来找可执行程序。

示例区分:

1
2
3
4
5
6
7
8
# 普通ls,如果有ls()函数,会执行函数
ls

# 绕开函数,执行真实外部ls程序
command ls

# -p:用系统原始PATH去找ls,不受当前PATH环境影响
command -p ls
  1. getconf 命令(外部工具,POSIX 标准)

getconf:查询系统配置参数,查询操作系统编译、环境的标准设置。

语法:

1
getconf 配置项

这里:

1
getconf PATH

输出 POSIX 标准规定的系统默认 PATH 字符串,这是操作系统内置的干净 PATH,不受用户、脚本当前环境$PATH变量的影响。

举个输出例子:

1
2
$ getconf PATH
/usr/bin:/bin

为什么不用直接写 echo $PATH? $PATH是当前环境变量,攻击者可以恶意篡改;getconf PATH读取系统本身的标准配置,不是读环境变量。

但是!

有些老旧 UNIX 系统没有 getconf 这个工具,命令执行会报错,输出为空。

1
2
command -p getconf PATH 2>/dev/null
# 2>/dev/null:把stderr错误丢弃,找不到getconf不输出报错

[[ -z "$SYSPATH" ]] 判断,如果拿不到结果(getconf 不存在),手动兜底写死默认值 /usr/bin:/bin。

整行完整拆解,一步一步执行

1
SYSPATH="$(command -p getconf PATH 2>/dev/null)"
  1. command -p:使用系统原始搜索路径,无视当前被污染的 PATH,去寻找getconf程序,防止被伪造的 getconf 木马劫持。
  2. 执行 getconf PATH,读取操作系统标准默认 PATH。
  3. 2>/dev/null:屏蔽错误;没有 getconf 工具时,不会打印错误。
  4. 命令替换$(...),把输出赋值给变量SYSPATH。
1
2
3
if [[ -z "$SYSPATH" ]]; then
    SYSPATH="/usr/bin:/bin"
fi
  • 如果执行结果为空(系统没有 getconf),手动设置兜底的安全 PATH。
1
PATH="$SYSPATH"

脚本把自己的 PATH 替换为这套干净、可信的系统路径。

安全脚本的关键点:不再继承外部传入的 PATH 环境变量,杜绝木马攻击。

组合记忆表格

片段 作用
command 绕开同名 shell 函数,执行真正外部命令
command -p 使用系统原始 PATH 查找命令,忽略当前环境 PATH
getconf PATH 查询操作系统内置标准 PATH 配置,不是读取环境变量 $PATH
2>/dev/null 丢弃错误输出,兼容没有 getconf 的老旧系统

2. 校验所有输入

  • 期待数字:必须校验输入是数字,并且在合法范围。
  • 用户输入参数,全部务必加双引号包裹 "$1" "$@",防止元字符注入。
1
2
3
4
5
# 错误,不引用,存在注入风险
rm $filename

# 正确,加双引号
rm "$filename"

3. 不信任外部传入环境变量

攻击者可以篡改环境变量:PATH、IFS、TZ。

脚本启动主动重置 PATH、IFS,不要继承外部环境。

4. 切换到已知工作目录

使用相对路径前,先cd到确定目录,并且判断cd是否执行成功。

1
cd /opt/app-dir || exit 1

5. 尽量使用命令完整绝对路径

不依赖PATH查找,避免被劫持。

1
/bin/rm  /tmp/test.txt

6. 谨慎使用eval

尽量不要直接把用户输入交给eval,极易被注入攻击。

7. 命令出错要检查返回状态

外部命令有可能被恶意环境强迫失败,不要假设命令一定执行成功。

8. 日志审计,使用logger记录行为

记录执行时间、操作用户,方便事后排查。

$*:把全部位置参数,用 IFS 第一个字符(默认空格)拼接成单个字符串。这里只有 1 个参数,$*就等于这个完整字符串。

1
2
3
4
logger(){
    printf "%s\n" "$*" >> /var/adm/logsysfile
}
logger "脚本执行,用户:$(id -un),时间:$(/bin/date)"

9. 警惕符号链接、竞争条件

  • 处理文件前,判断是否为符号链接,[ -L file ],防止指向系统关键文件。
  • 两个命令之间存在时间窗口,攻击者可以修改文件,尽量避免这种窗口。

10. setuid/setgid 建议

  1. 优先setgid,危害范围限制在组,而不是setuid。
  2. 尽量不要直接setuid到root;新建普通专用账号来完成权限操作。
  3. setuid代码尽量精简,独立拆分。

15.2 限制性Shell restricted shell

作用:限制普通用户操作,多用于guest访客账号,防止用户做危险操作。 支持:bash(rbash)、ksh93(rksh);POSIX没有标准化,设置环境非常麻烦,不推荐生产使用。

如何启用

  1. ksh93:程序链接为rksh,或者使用ksh -r;passwd登录shell写rksh完整路径。
  2. bash:程序链接为rbash,或者bash -r。

⚠限制生效时机:用户的.profile环境脚本执行完毕之后才开启限制。管理员在profile里面预置安全环境。

ksh93 rksh 禁止行为

  1. 不能cd切换目录
  2. 禁止输出重定向:> >> >|
  3. 不能修改环境变量:PATH ENV FPATH SHELL
  4. 只能执行PATH内命令;不能写带/的绝对路径命令。

bash rbash 额外禁止行为

  • 不能执行set +r关闭限制模式
  • `. source 命令不能带带/路径的文件
  • exec替换shell被禁止

重要坑

  1. 配置.profile权限必须保护,用户不能修改profile文件,否则绕过限制。
  2. PATH里面不能放其他shell程序、不能放vi/ed/ex这类带shell逃逸的编辑器;用户调用编辑器内部shell即可突破限制。

现实:限制性shell很难配置完全安全,实际工作尽量寻找替代方案。

15.3 特洛伊木马 Trojan horse

背景:伪装成正常程序,偷偷执行恶意代码。最经典攻击手段:PATH顺序 + 可写目录。

攻击示例

用户jprog,个人bin目录放在PATH最前面,且这个bin目录其他用户可以写入。 攻击者放一个伪造grep脚本到该目录:

1
2
3
4
5
6
7
8
# ~jprog/bin/grep 木马脚本
/bin/grep "$@"         # 先执行真正grep,伪装正常
case $(whoami) in
root)
    # 这里放恶意操作
    rm ~jprog/bin/grep  # 作案完成,删除木马,销毁证据
    ;;
esac
PATH 继承原理

继承 PATH 的原理

Linux/Unix:子进程默认继承父进程全部环境变量(PATH、HOME、TZ、IFS 等等)。
PATH 本质就是一个环境变量 (environment variable)。
父进程的环境变量,会复制一份,传给它创建出来的子进程。

关键点区分两个概念:

  1. Shell 内部变量(普通变量):仅当前 shell 内部,不会传给子进程,需要export导出,才会进入环境变量。
  2. 环境变量:存放在进程的环境块,fork()创建子进程时,操作系统自动完整复制一份给子进程 → 子进程继承。
1
2
3
4
# a只是shell内部变量,不导出,子进程拿不到
a=100
# export之后 a变成环境变量,fork出来的子进程可以继承
export a

PATH 几乎永远都是export导出的环境变量,所以子进程默认拿到父的 PATH。

什么时候会继承 PATH?(父进程创建子进程就继承)

只要是父进程 fork+exec 启动新程序,子进程默认继承父全部环境(包含 PATH):

  1. 在 shell 里面执行外部命令(ls、grep、cat、su、sudo 等)
  2. 执行脚本 ./test.sh
  3. su root(不带- / -l登录参数)
  4. 管道、$(...)命令替换、()子 shell
  5. ssh user@host command 非登录执行一条远程命令

以上全部:子进程复制父进程的环境变量,PATH 原样继承。

什么时候不会继承旧 PATH?(会重置 PATH)

触发「登录 shell」的时候,系统会重新初始化环境,读取目标用户的/etc/profile、~/.profile,丢弃继承过来的 PATH,重新设置成该用户的标准 PATH。

su 的两种模式(重点,对应书上木马案例)

① su root (没有横杠 -)

  • 只是切换用户 ID,不启动登录 shell
  • 继承原来用户全部环境变量(PATH 保留原来的值!!)

jprog 执行su root,父 shell 的 PATH=~jprog/bin:/usr/bin:/bin,root 的 su 子进程直接继承这份 PATH,这就是木马攻击能成功的根源。

② su - root / su -l root(带横杠,登录模式)

  • 启动登录 shell,模拟用户真正登录系统
  • 丢弃继承来的旧环境;执行 root 的 profile,生成 root 自己干净的 PATH。

此时 PATH 不再包含~jprog/bin,木马就不会触发。

其他会重置 PATH 的场景

  1. ssh user@host 不带命令,交互式登录:登录 shell,加载 profile,重置 PATH。
  2. 直接本机控制台输入账号密码登录:登录 shell,重置 PATH。
  3. sudo:默认行为要看 sudo 配置 /etc/sudoers;
    • 默认很多系统sudo会重置 PATH 为安全值,不会继承用户 PATH;
    • 如果 sudoers 设置Defaults env_keep += "PATH",就会继承用户 PATH(危险配置)。

脚本自身

脚本执行的时候,作为子 shell,继承调用者的 PATH 环境变量。

⚠这就是为什么安全脚本开头必须手动重置 PATH!因为调用脚本的外部环境 PATH 可能被攻击者篡改。

通俗流程演示木马案例

  1. jprog 登录,登录 shell:PATH=~jprog/bin:/usr/bin:/bin(环境变量,已 export)
  2. jprog 执行 su root
    • su 是子进程,继承 jprog 的 PATH:~jprog/bin:/usr/bin:/bin
    • su 把 UID 切换成 root,但环境变量没有重置
  3. 在这个 su 出来的 root shell 里面执行 grep
  4. shell 根据继承过来的 PATH 查找命令,优先找到木马脚本 ~jprog/bin/grep
  5. 木马以 root 权限运行。

如果是 su - root: su 启动 root 的登录 shell,加载 root 的 profile,抛弃继承过来的 PATH,重新设置 PATH=/usr/sbin:/usr/bin:/sbin:/bin,不再包含 jprog 的 bin 目录,攻击失效。

一张表总结

表格

操作 是否继承父进程 PATH 原因
su root(无-) 继承 非登录 shell,仅改 UID,保留全部环境变量
su - root / su -l root 重置 PATH 登录 shell,读取目标用户 profile
./myscript.sh 继承 子 shell,继承调用方环境变量
ssh root@ip 'grep xxx'(远程执行单条命令,非登录) 继承 ssh 客户端环境(远程服务器这边一般不会继承客户端本地 PATH,远程是 sshd 设置环境) 非登录 shell
ssh root@ip(交互式登录) 重置 PATH 登录 shell,加载 profile

攻击流程:

  1. jprog执行su切换root,继承原来的PATH。
  2. PATH优先查找~jprog/bin/grep,执行木马脚本。
  3. 脚本以root权限执行恶意代码,自我删除不留痕迹。

防御要点

  1. PATH不要把.放在前面。
  2. PATH里面所有目录权限严格:只有属主可写,其他人禁止写。
  3. 脚本开头重置PATH为可信系统路径。

15.4 Shell脚本使用setuid:一个坏主意

概念回顾

  • setuid(u+s):执行文件时,进程有效用户ID变成文件属主,而不是运行者的id。
  • setgid(g+s):执行文件时,进程有效组ID变成文件属组。
  • ls -l出现s代替x代表开启setuid/setgid。

⚠现代Linux、BSD系统已经直接忽略shell脚本上的setuid位,就算chmod u+s,也不会生效。

漏洞示例

脚本/etc/setuid_script,属主root,设置setuid,开头:

1
#!/bin/sh

攻击者操作:

1
2
3
4
cd /tmp
ln /etc/setuid_script -i
PATH=.
-i

会被解析为执行 /bin/sh -i,直接拿到setuid权限的交互式shell。

修复写法:脚本解释器参数末尾加横杠-

1
#!/bin/sh -

含义:后面全部参数当做文件名,不识别shell选项,规避该攻击。

重要区分:

  • setuid shell脚本:大部分系统直接失效,漏洞巨大。
  • setuid 二进制可执行程序:依旧生效,风险同样很高。

攻击者可以通过木马生成setuid的sh二进制文件,永久获取root权限。

15.5 ksh93 特权模式 privileged mode

作用:ksh93专门为setuid脚本设计的安全模式;当有效UID≠真实UID(开启setuid),自动开启set -o privileged / set -p。

行为

  1. 自动读取/etc/suid_profile,管理员在这里设置安全环境,把PATH设置只读。
1
2
typeset -r PATH
PATH="/bin:/usr/bin"
  1. 如果手动关闭特权模式 set +p,有效UID会回退到真实UID,setuid权限丢失。

/etc/suid_exec 代理程序

ksh93配套工具:用来安全执行setuid脚本,校验脚本安全性。

现实工作:尽量不要写setuid脚本;优先使用sudo完成权限提升。 示例:sudo /bin/ksh -p

15.6 本章小结

  1. 安全脚本基础原则:重置PATH/IFS;校验所有输入;双引号引用变量;不要信任外部环境;检查命令返回值;警惕符号链接与竞争条件。
  2. 限制性shell(rbash/rksh):限制用户行为,但配置极其复杂,容易被绕过,尽量寻找替代方案。
  3. 特洛伊木马:利用PATH顺序、目录权限漏洞伪装正常程序;防御:严格管控PATH和目录写权限。
  4. setuid shell脚本:强烈不推荐,现代UNIX直接忽略shell脚本setuid位;存在大量攻击面。
  5. ksh93特权模式set -p是专门针对setuid脚本的保护机制,但依然需要专业知识;日常优先使用sudo代替setuid脚本。

高频速记卡片

要点 正确示例 错误示例
脚本初始化PATH PATH="/bin:/usr/bin" 继承外部PATH,PATH里面带.
用户输入引用 rm "$filename" rm $filename
cd跳转 cd app || exit 1 cd app
setuid脚本开头 #!/bin/sh - #!/bin/sh