以下是检查根路径完整性的脚本,以确保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变量中删除它们。想到很多冗长的想法。但请寻找快速而“不那么复杂”的方法。
答案 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
的值。这很有效。
$IFS
有一个名为$IFS
的shell变量,用于设置read
和for
循环解析其参数的方式。通常,它设置为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 which
和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方式,也没有可移植的方法。但解析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
是八进制的,您可以使用位测试轻松检查perms
或g+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)