通过脚本检查根完整性

Checking root integrity via a script

下面是我检查根路径完整性的脚本,以确保 PATH 变量中没有漏洞。

#! /bin/bash

if [ ""`echo $PATH | /bin/grep :: `"" != """" ]; then
    echo "Empty Directory in PATH (::)" 
fi

if [ ""`echo $PATH | /bin/grep :$`""    != """" ]; then echo ""Trailing : in  PATH"" 
fi

p=`echo $PATH | /bin/sed -e 's/::/:/' -e 's/:$//' -e 's/:/ /g'`
set -- $p
while [ """" != """" ]; do
    if [ """" = ""."" ]; then
        echo ""PATH contains ."" shift
        continue
    fi
    if [ -d  ]; then
        dirperm=`/bin/ls -ldH  | /bin/cut -f1 -d"" ""`
        if [ `echo $dirperm | /bin/cut -c6 ` != ""-"" ]; then
            echo ""Group Write permission set on directory ""
        fi
        if [ `echo $dirperm | /bin/cut -c9 ` != ""-"" ]; then
            echo ""Other Write permission set on directory ""
        fi
        dirown=`ls -ldH  | awk '{print }'`
        if [ ""$dirown"" != ""root"" ] ; then
            echo  is not owned by root
        fi
    else
        echo  is not a directory
    fi
    shift
done

该脚本对我来说工作正常,并显示了 PATH 变量中定义的所有易受攻击的路径。我还想根据上述结果自动执行正确设置 PATH 变量的过程。有什么快速方法可以做到这一点。

例如,在我的 Linux 框中,脚本给出的输出为:

/usr/bin/X11 is not a directory
/root/bin is not a directory

而我的 PATH 变量定义了这些,所以我想有一个删除机制,将它们从 root 的 PATH 变量中删除。许多冗长的想法浮现在脑海中。但请搜索一种快速且 "not so complex" 的方法。

以下可以完成全部工作,还可以删除重复的条目

export PATH="$(perl -e 'print join(q{:}, grep{ -d && !((stat(_))[2]&022) && !$seen{$_}++ } split/:/, $ENV{PATH})')"

我喜欢@kobame 的回答,但如果您不喜欢 perl-依赖关系,您可以做类似的事情:

$ cat path.sh
#!/bin/bash

PATH="/root/bin:/tmp/groupwrite:/tmp/otherwrite:/usr/bin:/usr/sbin"
echo "${PATH}"

OIFS=$IFS
IFS=:
for path in ${PATH}; do
    [ -d "${path}" ] || continue
    paths=( "${paths[@]}" "${path}" )
done

while read -r stat path; do
    [ "${stat:5:1}${stat:8:1}" = '--' ] || continue
    newpath="${newpath}:${path}"
done < <(stat -c "%A:%n" "${paths[@]}" 2>/dev/null)
IFS=${OIFS}

PATH=${newpath#:}
echo "${PATH}"

$ ./path.sh
/root/bin:/tmp/groupwrite:/tmp/otherwrite:/usr/bin:/usr/sbin
/usr/bin:/usr/sbin

请注意,由于 stat 不可移植,因此不可移植,但它可以在 Linux(和 Cygwin)上运行。为了在 BSD 系统上工作,您必须调整格式字符串,其他 Unices 根本不附带 stat OOTB(例如,Solaris)。

它不会删除不属于 root 但可以轻松添加的重复项或目录。后者只需要稍微调整循环,以便 stat 也 returns 所有者的用户名:

while read -r stat owner path; do
    [ "${owner}${stat:5:1}${stat:8:1}" = 'root--' ] || continue
    newpath="${newpath}:${path}"
done < <(stat -c "%A:%U:%n" "${paths[@]}" 2>/dev/null)

我建议您获得一本关于 Bash shell 脚本编写的好书。看起来您是通过查看 30 年前的系统 shell 脚本和黑客攻击来学习 Bash 的。这不是什么可怕的事情。事实上,它显示了主动性和出色的逻辑能力。不幸的是,它会将您引向一些非常糟糕的代码。

If 语句

在原版 Bourne shell 中,[ 是一个命令。事实上,/bin/[ 很难 link 到 /bin/testtest 命令是一种测试文件某些方面的方法。例如,如果 $file 是可执行的,test -e $file 会 return 一个 0,如果不是,则 1

if 仅在其后执行命令,如果该命令 return 的退出代码为零,则 运行 then 子句,或者 else 子句(如果存在)如果退出代码不为零。

这两个是一样的:

if test -e $file
then
    echo "$file is executable"
fi

if [ -e $file ]
then
    echo "$file is executable"
fi

重要的是[只是一个系统命令。 if:

不需要这些
if grep -q "foo" $file
then
    echo "Found 'foo' in $file"
fi

请注意,我只是 运行ning grep,如果 grep 成功,我将重复我的声明。不需要 [ ... ]

if快捷方式是使用列表运算符&&||.例如:

grep -q "foo" $file && echo "我在 $file 中找到 'foo'"

与上面的if语句相同。

从不解析 ls

你不应该解析 ls 命令。您应该改用 statstat 以易于解析的形式获取命令中的所有信息。

[ ... ] 对比 [[ ... ]]

前面提到过,在原版的谍影重重中shell,[是一个系统命令。在 Kornshell 中,它是一个内部命令,Bash 也继承了它。

[ ... ] 的问题在于 shell 会在执行测试之前首先插入命令。因此,它容易受到各种 shell 问题的影响。 Kornshell 引入了 [[ ... ]] 作为 [ ... ] 的替代品,Bash 也使用它。

[[ ... ]] 允许 Kornshell 和 Bash 在 shell 插入命令之前评估参数 。例如:

foo="this is a test"
bar="test this is"
[ $foo = $bar ] && echo "'$foo' and '$bar' are equal."
[[ $foo = $bar ]] && echo "'$foo' and '$bar' are equal."

[ ... ] 测试中,shell 首先进行插值,这意味着它变为 [ this is a test = test this is ],这是无效的。在 [[ ... ]] 中,首先评估参数,因此 shell 理解它是 $foo$bar 之间的测试。然后,对 $foo$bar 的值进行插值。行得通。

For 循环和 $IFS

有一个名为 $IFS 的 shell 变量,它设置 readfor 循环如何解析它们的参数。通常,它设置为 space/tab/NL,但您可以修改它。由于每个 PATH 参数由 : 分隔,您可以设置 IFS=:",并使用 for 循环来解析您的 $PATH.

<<<重定向

<<< 允许您使用 shell 变量并将其作为 STDIN 传递给命令。这些都或多或少地做同样的事情:

statement="This contains the word 'foo'"
echo "$statement" | sed 's/foo/bar/'

statement="This contains the word 'foo'"
sed 's/foo/bar/'<<<$statement

中的数学Shell

使用 ((...)) 允许您使用数学,其中一项数学函数是屏蔽。我使用掩码来确定是否在权限中设置了某些位。

比如我的目录权限是0755,我反对0022,我可以看看是否设置了用户读写权限.注意前导零。这很重要,因此这些被解释为八进制值。

这是使用上面的代码重写的程序:

#! /bin/bash

grep -q "::" <<<"$PATH" && echo "Empty directory in PATH ('::')."
grep -q ":$" <<<$PATH && "PATH has trailing ':'"

#
# Fix Path Issues
#
path=$(sed -e 's/::/:/g' -e 's/:$//'<<<$PATH);

OLDIFS="$IFS"
IFS=":"
for directory in $PATH
do
    [[ $directory == "." ]] && echo "Path contains '.'."
    [[ ! -d "$directory" ]] && echo  "'$directory' isn't a directory in path."
    mode=$(stat -L -f %04Lp "$directory")       # Differs from system to system
    [[ $(stat -L -f %u "$directory") -eq 0 ]] &&  echo "Directory '$directory' owned by root"
    ((mode & 0022)) && echo "Group or Other write permission is set on '$directory'."
done

关于 PATH 漏洞,我不是 100% 确定你想要做什么或意味着什么。我不知道你为什么关心一个目录是否属于 root,如果 $PATH 中的条目不是目录,它不会影响 $PATH。但是,我要测试的一件事是确保 $PATH 中的所有目录都是绝对路径。

[[ $directory != /* ]] && echo "Directory '$directory' is a relative path"

没有冒犯,但您的代码已完全损坏。您以一种……创造性的方式使用引号,但却是一种完全错误的方式。不幸的是,您的代码受到路径名扩展和分词的影响。使用不安全的代码来“保护”您的 PATH.

真的很可惜

一个策略是(安全地!)将您的 PATH 变量拆分成一个数组,然后扫描每个条目。拆分是这样完成的:

IFS=: read -r -d '' -a path_ary < <(printf '%s:[=10=]' "$PATH")

查看我的 and How to split a string on a delimiter 答案。

使用此命令,您将拥有一个漂亮的数组 path_ary,其中包含 PATH.

的每个字段

然后您可以检查那里是否有空字段,或者 . 字段或相对路径:

for ((i=0;i<${#path_ary[@]};++i)); do
    if [[ ${path_ary[i]} = ?(.) ]]; then
        printf 'Warning: the entry %d contains the current dir\n' "$i"
    elif [[ ${path_ary[i]} != /* ]]; then
        printf 'Warning: the entry %s is not an absolute path\n' "$i"
    fi
done

您可以添加更多 elif,例如,检查条目是否为无效目录:

elif [[ ! -d ${path_ary[i]} ]]; then
    printf 'Warning: the entry %s is not a directory\n' "$i"

现在,要检查权限和所有权,不幸的是,没有纯粹的 Bash 方式或 portable 方式进行。但是解析 ls 很可能不是一个好主意。 stat 可以工作,但已知在不同平台上有不同的行为。因此,您必须尝试适合您的方法。这是一个在 Linux:

上使用 GNU stat 的示例
read perms owner_id < <(/usr/bin/stat -Lc '%a %u' -- "${path_ary[i]}")

您需要检查 owner_id 是否为 0(请注意,目录路径不属于 root 是可以的;例如,我有 /home/gniourf/bin这很好!)。 perms 是八进制的,您可以通过位测试轻松检查 g+wo+w

elif [[ $owner_id != 0 ]]; then
    printf 'Warning: the entry %s is not owned by root\n' "$i"
elif ((0022&8#$perms)); then
    printf 'Warning: the entry %s has group or other write permission\n' "$i"

注意使用8#$perms强制Bash理解perms为八进制数。

现在,要删除它们,您可以 unset path_ary[i] 当其中一个测试被触发时,然后将所有剩余的放回 PATH:

else
    # In the else statement, the corresponding entry is good
    unset_it=false
fi
if $unset_it; then
    printf 'Unsetting entry %s: %s\n' "$i" "${path_ary[i]}"
    unset path_ary[i]
fi

当然,您将 unset_it=true 作为循环的第一条指令。

然后将所有内容放回 PATH:

IFS=: eval 'PATH="${path_ary[*]}"'

我知道有些人会大声说 eval 是邪恶的,但这是在 Bash 中加入数组元素的规范(而且安全!)方式(注意单引号)。

最后,相应的函数可能如下所示:

clean_path() {
    local path_ary perms owner_id unset_it
    IFS=: read -r -d '' -a path_ary < <(printf '%s:[=17=]' "$PATH")
    for ((i=0;i<${#path_ary[@]};++i)); do
        unset_it=true
        read perms owner_id < <(/usr/bin/stat -Lc '%a %u' -- "${path_ary[i]}" 2>/dev/null)
        if [[ ${path_ary[i]} = ?(.) ]]; then
            printf 'Warning: the entry %d contains the current dir\n' "$i"
        elif [[ ${path_ary[i]} != /* ]]; then
            printf 'Warning: the entry %s is not an absolute path\n' "$i"
        elif [[ ! -d ${path_ary[i]} ]]; then
            printf 'Warning: the entry %s is not a directory\n' "$i"
        elif [[ $owner_id != 0 ]]; then
            printf 'Warning: the entry %s is not owned by root\n' "$i"
        elif ((0022 & 8#$perms)); then
            printf 'Warning: the entry %s has group or other write permission\n' "$i"
        else
            # In the else statement, the corresponding entry is good
            unset_it=false
        fi

        if $unset_it; then
            printf 'Unsetting entry %s: %s\n' "$i" "${path_ary[i]}"
            unset path_ary[i]
        fi
    done

    IFS=: eval 'PATH="${path_ary[*]}"'
}

这种设计,加上 if/elif/.../else/fi 非常适合这个简单的任务,但用于更复杂的测试可能会很尴尬。例如,请注意我们必须在测试之前尽早调用 stat,以便在测试后期可以使用信息,甚至在我们检查我们正在处理目录之前。

设计可能会通过使用一种意大利面条的可怕性来改变,如下所示:

for ((oneblock=1;oneblock--;)); do
    # This block is only executed once
    # You can exit this block with break at any moment
done

通常使用函数而不是 this 和函数中的 return 会好得多。但是因为在下面我还要检查多个条目,所以我需要查找 table(关联数组),并且有一个独立函数使用在某处定义的关联数组是很奇怪的否则……

clean_path() {
    local path_ary perms owner_id unset_it oneblock
    local -A lookup
    IFS=: read -r -d '' -a path_ary < <(printf '%s:[=19=]' "$PATH")
    for ((i=0;i<${#path_ary[@]};++i)); do
        unset_it=true
        for ((oneblock=1;oneblock--;)); do
            if [[ ${path_ary[i]} = ?(.) ]]; then
                printf 'Warning: the entry %d contains the current dir\n' "$i"
                break
            elif [[ ${path_ary[i]} != /* ]]; then
                printf 'Warning: the entry %s is not an absolute path\n' "$i"
                break
            elif [[ ! -d ${path_ary[i]} ]]; then
                printf 'Warning: the entry %s is not a directory\n' "$i"
                break
            elif [[ ${lookup[${path_ary[i]}]} ]]; then
                printf 'Warning: the entry %s appears multiple times\n' "$i"
                break
            fi
            # Here I'm sure I'm dealing with a directory
            read perms owner_id < <(/usr/bin/stat -Lc '%a %u' -- "${path_ary[i]}")
            if [[ $owner_id != 0 ]]; then
                printf 'Warning: the entry %s is not owned by root\n' "$i"
                break
            elif ((0022 & 8#$perms)); then
                printf 'Warning: the entry %s has group or other write permission\n' "$i"
                break
            fi
            # All tests passed, will keep it
            lookup[${path_ary[i]}]=1
            unset_it=false
        done
        if $unset_it; then
            printf 'Unsetting entry %s: %s\n' "$i" "${path_ary[i]}"
            unset path_ary[i]
        fi
    done

    IFS=: eval 'PATH="${path_ary[*]}"'
}

关于空格和全局字符以及内部换行符,所有这些都是非常安全的 PATH;我唯一不喜欢的是使用外部(和非 portable)stat 命令。