点击上方蓝字“Ots安全”一起玩耍
目录遍历漏洞:(也称为路径遍历漏洞)允许不良行为者访问他们不应访问的文件夹。在这篇文章中,我们将看看目录遍历漏洞如何在用 C/C++ 编写的 Web 服务器上工作,以及如何防止它们。
对于我们安全研究团队来说,C 和 C++ 生态系统已经超出了我们的范围很长时间了,但是当FossID 加入 Snyk时,情况发生了变化。FossID 的专长是 C/C++,这意味着我们能够启动几个项目来了解这些语言的整体信息安全状况。因此,我们能够发现许多高严重性漏洞,并很高兴开始分享我们的发现。
在这篇文章中,我们将重点关注对受限目录 ( CWE-22 ) 的路径名的不当限制。它有许多变体,根据CWE 排名前 25 位的最危险软件弱点,到 2022 年仍将非常普遍和危险。我们将探索三种不同类型的目录遍历,并将漏洞与易受攻击的代码示例、修复和可能的影响相对应。
任意文件读取:Web 服务器经常实现提供静态资源(如 HTML、CSS 和 JS 文件)的功能。通常资产存储在文件系统中的专用文件夹(例如/static、/www 或/assets)中。当 Web 应用程序没有正确清理静态文件的路径时,就会出现任意文件读取漏洞,从而允许用户使用“../”路径段超出预期的文件夹并最终读取磁盘上的任意文件。
我们将直接进入我们最近在 Crow Web 框架中发现的这个漏洞的示例。Crow是一个用于运行 Web 服务的 C++ 微框架。
对于这个例子,我们将创建一个简单的“hello world”应用程序,它由一个main.cpp文件组成:
int main()
{
crow::SimpleApp app;
CROW_ROUTE(app, "/")
([]() {
return "Hello world!";
});
app.port(8000).run();
return 0;
}
为了编译这个例子,我们使用 Ubuntu: g++ -pthread -o server main.cpp. 从这一点开始,可以./server在终端中运行服务器调用。
根据文档,默认情况下,Crow 会在与可执行文件/static相同的位置提供文件夹中的静态文件。server我们可以通过在其中创建/static文件夹和文本文件来尝试:mkdir static && echo “test” > static/test.txt
现在从另一个终端窗口,我们可以执行curl以检查它是否有效:
curl https://localhost:8000/给我们Hello world!作为回应。
curl https://localhost:8000/static/test.txt显示test- 这确实是static/test.txt文件的内容。
要利用该漏洞,我们可以执行curl --path-as-is "https://localhost:8000/static/../../etc/passwd". 在响应中,我们将看到/etc/passwd文件的内容。这里的诀窍是--path-as-is flag。它禁用 URL 中的路径规范化。对我们来说,这意味着服务器将准确接收我们提供的 URL——包括“../”段。
安全隐患
从生产服务器泄露任意文件通常是一个关键问题:SSH 密钥、各种凭据和服务器源代码可以为恶意行为者提供升级攻击并接管服务器或机密用户数据的方法。
对于最常使用 C++ 服务器的嵌入式设备也是如此。该目录遍历漏洞是 Wi-Fi 路由器中的常见访客:NETGEAR、Belkin、TP-Link等。在这种情况下,可能的含义可能是窃取管理面板凭据并获得对本地网络的完全控制。
在某些情况下,即使易受攻击的服务器在您的计算机上本地运行,目录遍历问题也可能很危险。本地 Web 服务器通常用于混合 Web 桌面应用程序,如 Electron。要了解有关此类攻击媒介的更多信息,您可以阅读我们关于易受攻击的 VS Code 插件的研究。
减轻
Crow v0.3+4修复了这个目录遍历漏洞。问题是通过app.h 文件引入的,其中代码只是将 URL 的一部分与静态文件夹路径连接起来,并调用set_static_file_info方法来提供文件:
res.set_static_file_info(CROW_STATIC_DIRECTORY + file_path_partial);
在这种情况下,解决方法是在方法的开头utility::sanitize_filename(path)添加一个调用。将所有出现的“..”替换为修复漏洞的“_”。该修复并不完美,因为服务器将无法提供文件名中包含“..”的文件,但这可能很少见,并且可能不会导致任何重大问题。set_static_file_infosanitize_filename
任意文件写入
任意文件写入漏洞是应用程序实现文件上传功能的代码库中非常常见的问题。该漏洞的根本原因与前一个案例非常相似,但更加晦涩难懂。想象一个简单的 HTML 表单来上传文件:
<form method=”post”>
<input type="file" name="avatar">
<input type="submit" value="submit">
</form>
让我们尝试选择一个名称为“test.txt”且内容为“test”的文本文件。浏览器将产生类似于以下内容的 HTTP 请求:
POST /upload HTTP/1.1
Host: localhost:8000
Content-Length: 150
Content-Type: multipart/form-data; boundary=----xxx
Connection: close
------xxx
Content-Disposition: form-data; name="file"; filename="test.txt"
Content-Type: text/plain
test
------xxx--
如果您更深入地查看请求正文,您会发现它包含的信息不仅仅是文件内容。也就是说,它有一个文件名字段,其中可以包含“../”段,并且必须由服务器正确处理。
为了展示这个问题,我们将使用最近在非常流行的嵌入式 Web 服务器框架Mongoose中发现的任意写入漏洞。
Mongoose 存储库中的文件上传示例允许用户以块的形式上传文件,如果您在运行服务器的设备上有少量 RAM,这将非常方便。make我们可以通过简单地克隆存储库并在示例文件夹中运行命令来构建和运行 示例。服务器立即开始监听 8000 端口,并将上传文件的目标文件夹设置为 /tmp(在文件第 13 行中指定main.c。)
此时,我们就可以使用curl上传文件了。该命令将在服务器上curl -X POST --data-binary "test" https://localhost:8000/upload?offset=0&name=test.txt创建一个带有“测试”文本的文件。/tmp/test.txt
为了利用该漏洞,我们可以在名称查询参数之前添加“../”路径段:curl -X POST --data-binary "pwned" https://localhost:8000/upload?offset=0&name=../malicious-file. 然后,该漏洞利用将malicious-file在文件系统的根目录创建一个。
安全隐患
利用任意文件写入漏洞并不像任意文件读取那样简单,但在许多情况下,它仍然可能导致远程代码执行 (RCE)。RCE 通常是可能的,因为恶意行为者能够覆盖重要的系统文件/etc/init.d/,甚至服务器可执行文件本身。
减轻
与前面的示例类似,Mongoose通过从用户提供的文件名中删除“..”来修复漏洞。该修复程序作为 7.6 版的一部分发布。
作为一般建议,我们建议您避免使用用户提供的文件名,并使用随机字符串、时间戳或 MD5 哈希来代替原始文件名。它通常适用于具有文件上传功能的应用程序,并有助于避免许多其他漏洞,如 XSS 等。
拉链
我们今天要介绍的第三种目录遍历问题是 zip slip。从名称可以看出,它代表与存档提取逻辑相关的漏洞。这种类型与任意文件写入非常相似,但具有更窄的表面,因为存档提取逻辑并不常见。
为了演示 zip slip 漏洞,我们将使用易受攻击的JUCE 6.1.4 版本和开源跨平台 C++ 应用程序框架。
int main()
{
juce::File file("archive.zip");
juce::ZipFile zipFile(file);
zipFile.uncompressTo(juce::File("data"));
return 0;
}
这是一个非常简单的应用程序,它使用 JUCE API 将 archive.zip 解压缩到数据文件夹。
要查看它是如何工作的,我们可以使用 test.txt 文件创建一个示例存档:
echo test > test.txt && zip archive.zip test.zip
adding: test.zip (stored 0%)
如果我们编译并运行我们的示例应用程序,data将创建文件夹并且test.txt文件将在其中。但是,如果我们在文件层次结构中创建一个包含上一级文件的档案怎么办?
echo bad > ../bad.txt && zip archive.zip ../bad.txt
在这种情况下,如果我们执行我们的程序,我们将拥有bad.txt目标文件夹之外的data文件。如您所见,zp slip 的安全问题与任意文件写入情况完全相同。
zip slip 还存在其他变体(如符号链接提取),我们建议阅读我们的研究论文以更深入地了解该主题。
减轻
我们展示的漏洞在 6.1.5 版本的 JUCE 框架中与另一个变体 - zip slip via symlink 一起修复。JUCE通过验证完整解析的文件路径是否包含在目标目录中来修复漏洞。
一般补救建议
在研究此类漏洞时,Snyk 安全研究团队查看了大量实现各种文件系统路径相关逻辑的代码示例,我们相信我们找到了一个通用的经验法则,您可以使用它来保护自己免受目录遍历漏洞的侵害。
如果可以不使用用户提供的文件路径,请不要!
这条规则对于上传逻辑和存档提取逻辑的许多情况是公平的。
但是如果您必须使用用户提供的文件路径,我们可以建议您遵循下一个逻辑(使用 std 实现):
// #include <iostream>
// #include <string>
// #include <filesystem>
// #include <algorithm>
// This is the path to your static files folder.
std::string base_path("/tmp/path_to_the_content_root_directory/");
// User input – possibly contains "../" path segments.
std::string user_input("user_provided_path");
// The "canonical" method resolves ".", ".." and symlinks.
std::filesystem::path base_resolved_path(
std::filesystem::canonical(base_path));
// The "weakly_canonical" method does the same as "canonical" but not requires
// the file to exist.
std::filesystem::path requested_file_path(
std::filesystem::weakly_canonical(base_resolved_path / user_input));
// Using "equal" we can check if "requested_file_path" starts
// with base_resolved_path. Because we previously canonicalized both paths
// they can't contain any ".." segments, so this check is sufficient.
if (std::equal(
base_resolved_path.begin(),
base_resolved_path.end(),
requested_file_path.begin())) {
std::cout << "It is safe to work with the file: " << requested_file_path << "n";
} else {
std::cout << "Not safe! We have to throw an exception here.n";
}
上面的代码至少需要 C++17,但同样可以在 boost.js 中实现。如果您的设置既没有 std 也没有 boost 可用,将“..”替换为空字符串,然后将多个结果“/”字符替换为一个也足够了。
此外,如果您的应用程序旨在跨平台并且还将在 Windows 上运行,请考虑规范化目录分隔符。在大多数情况下,Windows 接受斜杠(“/”)和反斜杠(“”)作为目录分隔符。这意味着“..”payload相当于“../”,也可以用来实现目录遍历攻击。确保代码在 Windows 上安全的最简单方法是在处理路径之前将“/”替换为“”。
原文始发于微信公众号(Ots安全):探索C/C++中的3种目录遍历漏洞
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论