通过脚本检查root完整性

时间:2015-02-26 11:06:28

标签: linux bash scripting redhat

以下是检查根路径完整性的脚本,以确保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 [ ""$1"" != """" ]; do
    if [ ""$1"" = ""."" ]; then
        echo ""PATH contains ."" shift
        continue
    fi
    if [ -d $1 ]; then
        dirperm=`/bin/ls -ldH $1 | /bin/cut -f1 -d"" ""`
        if [ `echo $dirperm | /bin/cut -c6 ` != ""-"" ]; then
            echo ""Group Write permission set on directory $1""
        fi
        if [ `echo $dirperm | /bin/cut -c9 ` != ""-"" ]; then
            echo ""Other Write permission set on directory $1""
        fi
        dirown=`ls -ldH $1 | awk '{print $3}'`
        if [ ""$dirown"" != ""root"" ] ; then
            echo $1 is not owned by root
        fi
    else
        echo $1 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变量中删除它们。想到很多冗长的想法。但请寻找快速而“不那么复杂”的方法。

4 个答案:

答案 0 :(得分:2)

我建议你买一本关于Bash shell脚本的好书。看起来你通过查看30年历史的系统shell脚本并通过黑客攻击来学习Bash。这不是一件可怕的事情。事实上,它显示出主动性和伟大的逻辑技能。不幸的是,它会导致一些非常糟糕的代码。

如果声明

在原始的Bourne shell中,[是一个命令。事实上,/bin/[/bin/test的硬链接。 test命令是一种测试文件某些方面的方法。例如,test -e $file如果0是可执行的,则会返回$file,如果不可执行则返回1

if仅在它之后接受命令,如果该命令返回退出代码为零,则运行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

请注意,我只是在运行grep,如果grep成功,我就会回应我的陈述。不需要[ ... ]

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

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

与上述if语句相同。

从不解析ls

您永远不应解析ls命令。您应该使用stat代替。 stat以一种易于解析的形式获取命令中的所有信息。

[ ... ][[ ... ]]

正如我之前提到的,在最初的Bourne 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

壳牌数学

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

例如,如果我的目录权限是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

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

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

答案 1 :(得分:2)

没有冒犯,但你的代码完全坏了。您以创造性的方式使用引号,但却以完全错误的方式使用。不幸的是,您的代码受路径名扩展和单词拆分的影响。拥有一个不安全的代码来“保护”你的PATH真的很遗憾。

一种策略是(安全!)将PATH变量拆分为数组,并扫描每个条目。分裂是这样完成的:

IFS=: read -r -d '' -a path_ary < <(printf '%s:\0' "$PATH")

查看我的mock whichHow 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方式,也没有可移植的方法。但解析ls很可能不是一个好主意。 stat可以工作,但已知在不同平台上有不同的行为。因此,您必须尝试一下适合您的方法。这是一个在Linux上与GNU stat一起使用的示例:

read perms owner_id < <(/usr/bin/stat -Lc '%a %u' -- "${path_ary[i]}")

您要检查owner_id是否为0(请注意,拥有不属于root用户的目录路径是可以的;例如,我有{ {1}}那很好!)。 /home/gniourf/bin是八进制的,您可以使用位测试轻松检查permsg+w

o+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" 强制Bash将8#$perms理解为八进制数。

现在,要删除它们,您可以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[*]}"' 是邪恶的,但这是一种规范(和安全!)方式来加入Bash中的数组元素(观察单引号)。

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

eval

这个带有clean_path() { local path_ary perms owner_id unset_it IFS=: read -r -d '' -a path_ary < <(printf '%s:\0' "$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 。但是因为在下面我还要检查多个条目,我需要有一个查找表(关联数组),并且有一个使用关联的独立函数很奇怪在其他地方定义的数组...

return

对于clean_path() { local path_ary perms owner_id unset_it oneblock local -A lookup IFS=: read -r -d '' -a path_ary < <(printf '%s:\0' "$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命令。

答案 2 :(得分:1)

以下内容可以完成整个工作,也可以删除重复的条目

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

答案 3 :(得分:1)

我喜欢@ 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在所有OOTB(例如Solaris)中不附带stat

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

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)