tl;博士:这就是你的代码所做的,作为一个 shell 脚本:
#!/bin/bash
echo -en "Content-type:text/html\r\n\r\n"
ps -eo lstart,cmd | grep init | grep -v $QUERY_STRING | \
head -n 1 | awk '{ print $1" "$3" "$2" "$5" "$4}'
现在是更长的答案。
重写代码
首先,让我们通过一些错误处理将它变成 C++ 而不是 C(就像您的标签建议您询问的那样),然后谈谈发生了什么:
#include <iostream>
#include <string>
#include <string_view>
int main () {
auto query_string = getenv("QUERY_STRING");
if (query_string == nullptr) {
std::cerr << "Couldn't obtain QUERY_STRING environment variable\n";
return EXIT_FAILURE;
}
if (std::string_view{query_string}.empty()) {
std::cerr << "Empty query string (QUERY_STRING environment variable)\n";
return EXIT_FAILURE;
}
std::stringstream command_line;
command_line
<< "ps -eo lstart,cmd | grep "
<< query_string
<< " | grep -v grep | head -n 1 | awk '{ print $1\" \"$3\" \"$2\" \"$5\" \"$4}'";
std::cout << "Content-type:text/html\r\n\r\n";
return system(command_line.str()); // security vulnerability, see below
}
我们在这里做什么?
因此,我们在这里创建一个命令行,然后使用该system()
函数执行该命令行。它是使用一些开关调用ps
命令,然后使用 , 进行一些文本处理grep
,head
以及awk
- 使用管道机制将每个命令的输出移动到下一个命令。它们的关键部分是我们使用环境变量QUERY_STRING
来过滤ps
结果,即我们列出匹配某个短语的进程。如果我们编译这个程序,设置环境变量并运行,它是这样的:
$ export QUERY_STRING=init
$ ./the_program
Content-type:text/html
Sun 3 Jun 2018 21:48:56
这给我们的是命令行不包含短语“init”的第一个进程的开始时间。所以现在你可以猜到我的系统从昨天开始就已经启动了......
最后,作为一个网络人,您可能意识到“内容类型”mumbo-jumbo 并且双换行符是一个 MIME 标头,因此此输出可能旨在用作 HTTP 响应。这可能是为了作为某种 CGI 脚本。
安全漏洞
- 在原始代码中,缓冲区大小被任意限制为 1024 - 而没有什么限制 QUERY_SIZE 不超过此值。如果它更长,您将有内存损坏,这可能会产生安全隐患;并且攻击者可能能够弄清楚您的内存布局,因此更加危险。这在 C++ 版本中消失了。
第二个漏洞与system
命令有关。我们将任意字符串注入到我们正在创建的字符串中;并且没有什么可以阻止某人设置
$export QUERY_STRING="dummy; rm -rf $HOME ; echo"
在这种情况下,您将运行:
ps -eo lstart,cmd | grep dummy; rm -rf $HOME ; echo | grep -v init | head -n 1 | awk '{ print $1" "$3" "$2" "$5" "$4}'
这将删除有效用户主目录下的所有内容。或者它可以是任何命令,包括编译自定义 C/C++ 程序以在您的系统上运行一些任意代码。很坏。
- 即使您将 QUERY_STRING 清理为仅是一个有效的 grep 模式,如果有人以某种方式提供复杂的超长 grep 模式,仍然可能存在拒绝服务攻击。所以限制长度也是一个好主意。