MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Bash中的脚本安全加固

2022-11-232.6k 阅读

一、Bash脚本安全概述

Bash(Bourne - Again SHell)是Linux和Unix系统中最常用的命令行解释器,广泛用于编写自动化脚本。然而,Bash脚本若编写不当,可能会带来严重的安全风险,如命令注入、权限泄露等。对Bash脚本进行安全加固,是保障系统安全的重要环节。

1.1 常见安全风险剖析

  1. 命令注入风险:当Bash脚本接受外部输入并直接嵌入到命令中执行时,如果输入未经过严格校验,恶意用户可能会注入额外的命令。例如,假设有一个简单的脚本接收一个文件名作为参数并删除该文件:
#!/bin/bash
rm $1

若恶意用户输入file; rm -rf /,脚本会执行删除整个根目录的操作,造成系统崩溃。 2. 变量注入风险:类似于命令注入,当脚本使用未经过滤的外部输入来设置变量,可能导致变量被恶意篡改。例如:

#!/bin/bash
input=$1
echo "Your input is: $input"
eval "echo The result is: \$$input"

如果恶意用户输入FOO='; echo Hacked',脚本可能会执行恶意的echo Hacked命令。 3. 权限相关风险:脚本在执行过程中若使用不当的权限,如以root权限运行不必要的操作,或者在脚本执行过程中权限泄露,都可能导致安全问题。比如,一个脚本原本以普通用户运行,但在执行过程中调用了一个以root权限运行的命令,且该命令存在可被利用的漏洞,就可能导致攻击者获取root权限。

二、输入校验与过滤

2.1 基本输入验证

  1. 检查参数个数:在脚本开头,先检查传递给脚本的参数个数是否符合预期。例如,一个脚本需要两个参数:
#!/bin/bash
if [ $# -ne 2 ]; then
    echo "Usage: $0 arg1 arg2"
    exit 1
fi

这里使用$#获取参数个数,若参数个数不为2,则输出使用说明并退出脚本。 2. 验证输入类型:对于数值类型的输入,可以使用正则表达式来验证。假设脚本接受一个数字作为参数,并进行相关计算:

#!/bin/bash
if [ $# -ne 1 ]; then
    echo "Usage: $0 number"
    exit 1
fi
if! [[ $1 =~ ^[0 - 9]+$ ]]; then
    echo "Error: Input must be a number"
    exit 1
fi
result=$(( $1 + 10 ))
echo "The result is: $result"

上述脚本使用[[ $1 =~ ^[0 - 9]+$ ]]来判断输入是否为纯数字。如果不是,则输出错误信息并退出。

2.2 过滤特殊字符

  1. 防止命令注入的字符过滤:为了防止命令注入,需要过滤掉可能用于构造恶意命令的特殊字符,如;&|$()等。可以使用tr命令来删除这些字符。例如:
#!/bin/bash
input=$1
filtered_input=$(echo $input | tr -d ';|&$()')
echo "Filtered input: $filtered_input"

此脚本将输入中的危险字符删除,降低命令注入风险。 2. 过滤正则表达式特殊字符:当输入可能用于正则表达式匹配时,需要过滤掉正则表达式的特殊字符,如*?+.^$等。同样可以使用tr命令:

#!/bin/bash
input=$1
regex_safe_input=$(echo $input | tr -d '*?+.$^')
echo "Regex - safe input: $regex_safe_input"

这样可以防止恶意用户通过输入恶意正则表达式来破坏脚本逻辑。

三、变量使用规范

3.1 变量初始化

  1. 避免未初始化变量:在使用变量之前,始终进行初始化。未初始化的变量可能会导致意外的行为或安全漏洞。例如:
#!/bin/bash
# 错误示例,未初始化变量
echo $uninitialized_variable
# 正确示例,初始化变量
initialized_variable="default value"
echo $initialized_variable
  1. 使用本地变量:在函数内部,尽量使用本地变量,避免与全局变量冲突。例如:
#!/bin/bash
global_variable="global"
function my_function {
    local local_variable="local"
    echo "Local variable: $local_variable"
    echo "Global variable: $global_variable"
}
my_function

这里在函数my_function中使用local关键字定义了一个本地变量,防止对全局变量的意外修改。

3.2 变量引用安全

  1. 双引号引用变量:在引用变量时,尽量使用双引号。如果不使用双引号,变量中的空格可能会导致命令参数解析错误,甚至可能引发安全问题。例如:
#!/bin/bash
file_name="multi word file.txt"
# 错误示例,未使用双引号
rm $file_name
# 正确示例,使用双引号
rm "$file_name"

若不使用双引号,rm命令可能会将multiwordfile.txt当作三个不同的参数,导致找不到文件或误删除其他文件。 2. 避免使用eval解析变量eval命令可以动态执行字符串作为命令,但它非常危险,因为如果字符串来自不可信的输入,可能导致命令注入。例如:

#!/bin/bash
# 危险示例
input=$1
eval "echo $input"
# 安全替代方案
echo "$input"

在安全替代方案中,直接输出输入内容,避免了eval带来的风险。

四、脚本执行权限管理

4.1 最小权限原则

  1. 以普通用户运行脚本:除非绝对必要,脚本应尽量以普通用户权限运行。例如,一个简单的文件备份脚本,不需要root权限:
#!/bin/bash
source_dir="/home/user/source"
dest_dir="/home/user/backup"
cp -r $source_dir $dest_dir

此脚本在普通用户的家目录下进行文件复制操作,以普通用户权限运行即可。 2. 限制脚本对敏感资源的访问:脚本应仅访问其正常运行所需的资源,避免访问不必要的敏感文件或目录。例如,一个日志分析脚本,只需要访问日志文件目录,而不需要访问系统配置文件目录:

#!/bin/bash
log_dir="/var/log"
for log_file in $log_dir/*.log; do
    grep "error" $log_file
done

该脚本仅在/var/log目录下查找日志文件进行分析,不涉及其他敏感目录。

4.2 权限提升与降低

  1. 合理提升权限:如果脚本确实需要临时提升权限来执行某些操作,应使用sudo并进行严格的权限配置。例如,一个更新系统软件包的脚本:
#!/bin/bash
sudo apt update
sudo apt upgrade -y

在使用sudo之前,确保在sudoers文件中对脚本的执行进行了安全配置,只允许特定用户以特定权限执行该脚本。 2. 及时降低权限:在完成需要高权限的操作后,应及时降低权限。例如,在使用sudo提升权限安装软件包后,切换回普通用户权限继续执行其他操作:

#!/bin/bash
sudo apt install some_package -y
# 切换回普通用户权限继续操作
current_user=$(whoami)
if [ "$current_user" == "root" ]; then
    su - some_normal_user -c "echo 'Continuing as normal user'"
fi

这样可以减少高权限运行的时间,降低风险。

五、安全编码实践

5.1 错误处理

  1. 检查命令执行结果:在脚本中执行每个命令后,检查其执行结果。如果命令执行失败,应采取适当的措施,如输出错误信息并停止脚本执行。例如:
#!/bin/bash
rm some_file.txt
if [ $? -ne 0 ]; then
    echo "Failed to remove file"
    exit 1
fi

这里使用$?获取上一个命令的退出状态码,若不为0,则表示命令执行失败。 2. 设置严格的错误处理模式:可以在脚本开头设置set -e,使脚本在遇到任何命令执行失败时立即停止。例如:

#!/bin/bash
set -e
rm some_file.txt
echo "File removed successfully"

这样,如果rm命令失败,脚本将立即停止,避免后续可能因错误状态导致的安全问题。

5.2 避免硬编码敏感信息

  1. 密码和密钥管理:不要在脚本中硬编码密码、密钥等敏感信息。可以通过环境变量或配置文件来存储这些信息,并在脚本中安全地读取。例如,使用环境变量存储数据库密码:
#!/bin/bash
db_password=$DB_PASSWORD
echo "Connecting to database with password: $db_password"

在运行脚本前,通过export DB_PASSWORD='your_password'设置环境变量。 2. 敏感路径和配置:同样,对于敏感的文件路径或配置参数,也应避免硬编码。可以将这些配置放在一个配置文件中,脚本读取配置文件来获取相关信息。例如,一个备份脚本的配置文件backup.conf

source_dir=/home/user/source
dest_dir=/home/user/backup

脚本读取配置文件的内容:

#!/bin/bash
while IFS='=' read -r key value; do
    case "$key" in
        source_dir) source_dir="$value" ;;
        dest_dir) dest_dir="$value" ;;
    esac
done < backup.conf
cp -r $source_dir $dest_dir

这样可以在不修改脚本代码的情况下,灵活调整敏感配置信息,并且降低了敏感信息泄露的风险。

六、脚本加密与保护

6.1 脚本加密

  1. 使用工具加密脚本:有一些工具可以对Bash脚本进行加密,使其在运行时解密并执行。例如,shc工具可以将Bash脚本编译成可执行文件,并对脚本内容进行加密。首先安装shc
sudo apt install shc

然后对脚本进行加密:

shc -f my_script.sh

这将生成my_script.sh.x可执行文件和my_script.sh.sig签名文件。运行my_script.sh.x时,它会在后台解密并执行原脚本内容,而他人无法直接查看脚本的原始代码。 2. 自定义加密方案:可以使用加密算法(如AES)对脚本进行加密,并在脚本运行时解密。例如,使用openssl进行加密和解密:

# 加密脚本
openssl enc -aes -256 -cbc -salt -in my_script.sh -out my_script.sh.enc -k my_secret_key
# 解密并运行脚本
openssl enc -d -aes -256 -cbc -salt -in my_script.sh.enc -out my_script.sh.tmp -k my_secret_key
bash my_script.sh.tmp
rm my_script.sh.tmp

这种方法需要妥善保管加密密钥my_secret_key,以确保脚本的安全性。

6.2 防止脚本篡改

  1. 文件权限设置:通过设置脚本文件的权限,防止非授权用户修改脚本。例如,将脚本文件的权限设置为仅所有者可读写执行:
chmod 700 my_script.sh

这样只有脚本所有者可以对脚本进行修改,降低了脚本被篡改的风险。 2. 使用数字签名:类似于shc生成的签名文件,可以使用GnuPG对脚本进行数字签名。首先生成密钥对:

gpg --gen - key

然后对脚本进行签名:

gpg --sign -a my_script.sh

这将生成my_script.sh.asc签名文件。在运行脚本前,可以验证签名:

gpg --verify my_script.sh.asc my_script.sh

如果签名验证通过,则说明脚本未被篡改;否则,脚本可能已被恶意修改。

七、运行环境安全配置

7.1 系统环境变量配置

  1. PATH变量安全:确保PATH环境变量中只包含可信任的目录。恶意用户可能会在PATH中添加恶意目录,当脚本执行命令时,可能会执行恶意命令。例如,检查PATH变量:
echo $PATH

若发现有可疑目录,可以从PATH中移除。例如,移除/tmp/malicious_dir

export PATH=$(echo $PATH | sed 's|/tmp/malicious_dir:||g')
  1. 其他环境变量安全:对于其他可能影响脚本运行的环境变量,如LD_LIBRARY_PATH(用于动态链接库查找),也要进行严格的安全配置。避免在不可信的目录中查找动态链接库,防止恶意库的加载。例如,设置LD_LIBRARY_PATH只包含系统默认的库目录:
export LD_LIBRARY_PATH=/lib:/usr/lib

7.2 运行时安全限制

  1. 资源限制:可以使用ulimit命令对脚本运行时的资源进行限制,如文件打开数、内存使用等。例如,限制脚本最多只能打开100个文件:
ulimit -n 100

这可以防止脚本因打开过多文件而耗尽系统资源,同时也能防止一些恶意脚本通过大量打开文件进行攻击。 2. 沙盒环境:对于一些安全性要求较高的脚本,可以在沙盒环境中运行。例如,使用chroot创建一个隔离的文件系统环境,脚本在该环境中运行,无法访问外部系统的敏感资源。首先创建一个沙盒目录结构:

mkdir -p /sandbox/bin /sandbox/lib /sandbox/usr/bin /sandbox/usr/lib
cp /bin/bash /sandbox/bin
cp -r /lib/x86_64 - linux - gnu /sandbox/lib
cp -r /usr/bin /sandbox/usr
cp -r /usr/lib/x86_64 - linux - gnu /sandbox/usr/lib

然后使用chroot进入沙盒环境并运行脚本:

chroot /sandbox bash my_script.sh

这样脚本在沙盒环境中运行,只能访问沙盒内的资源,提高了安全性。

八、监控与审计

8.1 日志记录

  1. 脚本自身日志:在脚本中添加详细的日志记录,记录脚本的执行过程、关键操作和错误信息。可以使用echo输出日志到文件,也可以使用更专业的日志记录工具,如logger。例如,使用echo记录日志:
#!/bin/bash
log_file="script.log"
echo "$(date): Starting script" >> $log_file
# 执行命令
rm some_file.txt
if [ $? -ne 0 ]; then
    echo "$(date): Failed to remove file" >> $log_file
else
    echo "$(date): File removed successfully" >> $log_file
fi
  1. 系统日志关联:将脚本的日志与系统日志进行关联,便于整体的安全审计。例如,在脚本中使用logger命令将日志记录到系统日志中:
#!/bin/bash
logger -t my_script "Starting script"
rm some_file.txt
if [ $? -ne 0 ]; then
    logger -t my_script "Failed to remove file"
else
    logger -t my_script "File removed successfully"
fi

这样可以在系统日志文件(如/var/log/syslog)中查找与脚本相关的日志信息。

8.2 审计跟踪

  1. 记录脚本执行历史:可以通过记录脚本的执行历史,包括执行时间、执行用户、传入参数等信息,进行审计跟踪。例如,创建一个脚本执行日志文件script_execution.log
#!/bin/bash
execution_log="script_execution.log"
echo "$(date): $(whoami) executed $0 with arguments $*" >> $execution_log
# 脚本主体内容
  1. 监控脚本活动:使用系统监控工具(如auditd)对脚本的活动进行监控。例如,配置auditd监控特定脚本的执行:
# 在/etc/audit/audit.rules中添加规则
-a exit,always -F path=/path/to/my_script.sh -F perm=x -k my_script_execution

然后重启auditd服务:

sudo systemctl restart auditd

这样auditd会记录关于该脚本执行的详细信息,包括成功和失败的执行尝试,便于安全审计。