文件上传漏洞
文件上传漏洞是指当 Web 服务器允许用户将其文件上传到文件系统时,没有充分验证文件名、类型、内容或大小等情况。如果未能正确执行这些限制,即使是基本的图像上传功能也可能被用来上传任意和潜在危险的文件。这可能包括允许远程代码执行的服务器端脚本文件。
文件上传漏洞的影响通常取决于两个关键因素:
网站未能正确验证文件哪个方面,无论是其大小、类型、内容等。
文件成功上传后对其施加的限制。
在最坏的情况下,文件类型未被正确验证,服务器配置允许某些类型(如 .php
和 .jsp
)被执行为代码。在这种情况下,攻击者可能上传一个服务器端代码文件,该文件充当 web shell,从而有效地获得对服务器的完全控制
如果文件名没有被正确验证,这可能会允许攻击者仅通过上传一个同名文件来覆盖关键文件。如果服务器也容易受到目录遍历攻击,这意味着攻击者甚至能够将文件上传到意想不到的位置。
未能确保文件大小在预期范围内,也可能导致一种拒绝服务(DoS)攻击,攻击者会填满可用磁盘空间
例如,他们可能会尝试将危险的文件类型加入黑名单,但在检查文件扩展名时却未能考虑到解析差异。和任何黑名单一样,也很容易不小心遗漏一些可能仍然危险的较为罕见的文件类型。
Web 服务器如何处理静态文件的请求
如今,网站越来越动态,请求的路径通常与文件系统没有任何直接关系。尽管如此,Web 服务器仍然处理一些静态文件请求,包括样式表、图像等。
如果文件类型是可执行的,比如 PHP 文件,并且服务器配置为执行此类文件,那么它会在运行脚本之前根据 HTTP 请求中的头部和参数分配变量。最终生成的输出可能会通过 HTTP 响应发送给客户端。
The Content-Type
响应头可能提供关于服务器认为它所提供文件类型的线索。如果这个头没有被应用程序代码显式设置,它通常包含文件扩展名/MIME 类型映射的结果。
上传一个 web shell,你就实际上获得了对服务器的完全控制权。这意味着你可以读取和写入任意文件,窃取敏感数据,甚至可以利用服务器作为跳板,对内部基础设施和其他网络外的服务器发起攻击。例如,以下 PHP 单行代码可以用来从服务器的文件系统中读取任意文件:
<?php echo file_get_contents('/path/to/target/file'); ?>
一个更通用的 web shell 可能看起来像这样:
<?php echo system($_GET['command']); ?>
此脚本允许您通过查询参数传递任意系统命令,如下所示:
GET /example/exploit.php?command=id HTTP/1.1
利用文件上传的缺陷验证
当提交 HTML 表单时,浏览器通常会以 POST
请求的方式发送提供的数据,内容类型为 application/x-www-form-url-encoded
。这对于发送简单的文本信息,如姓名或地址是可行的。然而,它不适合发送大量的二进制数据,例如整个图像文件或 PDF 文档。在这种情况下,推荐使用内容类型 multipart/form-data
POST /images HTTP/1.1
Host: normal-website.com
Content-Length: 12345
Content-Type: multipart/form-data; boundary=---------------------------012345678901234567890123456
---------------------------012345678901234567890123456
Content-Disposition: form-data; name="image"; filename="example.jpg"
Content-Type: image/jpeg
[...binary content of example.jpg...]
---------------------------012345678901234567890123456
Content-Disposition: form-data; name="description"
This is an interesting description of my image.
---------------------------012345678901234567890123456
Content-Disposition: form-data; name="username"
wiener
---------------------------012345678901234567890123456--
消息正文被拆分为与表单的每个输入相对应的独立部分。每个部分都包含一个 Content-Disposition
标题,它提供了一些关于它所关联的输入字段的基本信息。这些独立的部分还可能包含自己的 Content-Type
标题,它告诉服务器使用此输入提交的数据的 MIME 类型
网站验证文件上传的一种方式是检查这个特定输入的 Content-Type
头部是否与预期的 MIME 类型匹配。例如,如果服务器只期望图像文件,它可能只允许 image/jpeg
和 image/png
这样的类型。当服务器隐式信任这个头部的值时,可能会出现问题。如果服务器没有进一步验证文件内容是否确实与假设的 MIME 类型匹配,这种防御措施就可以使用 Burp Repeater 等工具轻易绕过。
防止在用户可访问目录中执行文件
作为预防措施,服务器通常只运行那些它们被明确配置为可执行的 MIME 类型的脚本。否则,它们可能会返回某种错误消息,或者在某些情况下,将文件内容作为纯文本提供:
GET /static/exploit.php?command=id HTTP/1.1
Host: normal-website.com HTTP/1.1 200 OK
Content-Type: text/plain Content-Length: 39
Web 服务器通常使用 filename
字段在 multipart/form-data
请求中确定文件应保存的名称和位置。
对危险文件类型黑名单不足
防止用户上传恶意脚本的一个更明显的方法是黑名单潜在的危险文件扩展名,如 .php
。黑名单的做法本身存在缺陷,因为它很难明确阻止所有可能用于执行代码的文件扩展名。这种黑名单有时可以通过使用不太知名、替代的文件扩展名来绕过,这些扩展名可能仍然可执行,例如 .php5
、 .shtml
等等。
覆盖服务器配置
服务器通常不会执行文件,除非它们被配置为执行。例如,在 Apache 服务器执行客户端请求的 PHP 文件之前,开发者可能需要在他们的 /etc/apache2/apache2.conf
文件中添加以下指令:
LoadModule php_module /usr/lib/apache2/modules/libphp.so
AddType application/x-httpd-php .php
服务器通常不会执行文件,除非它们被配置为执行。例如,在 Apache 服务器执行客户端请求的 PHP 文件之前,开发者可能需要在他们的 /etc/apache2/apache2.conf 文件中添加以下指令:
同样地,开发者可以使用 web.config
文件在 IIS 服务器上进行目录特定的配置。这可能包括如下指令,在这个例子中,它允许向用户提供 JSON 文件:
<staticContent>
<mimeMap fileExtension=".json" mimeType="application/json" />
</staticContent>
Web 服务器在存在这些配置文件时会使用它们,但你通常不被允许通过 HTTP 请求访问它们。然而,你可能会偶尔遇到服务器未能阻止你上传自己的恶意配置文件的情况。在这种情况下,即使你需要文件扩展名被列入黑名单,你也可能能够欺骗服务器将任意自定义文件扩展名映射到可执行 MIME 类型。
混淆文件扩展名
假设验证代码区分大小写,并且无法识别 exploit.pHp
实际上是 .php
文件。如果随后将文件扩展名映射到 MIME 类型的代码不区分大小写,这种差异允许你悄悄地将恶意 PHP 文件绕过验证,这些文件最终可能会被服务器执行。
提供多个扩展名。根据解析文件名的算法,以下文件可能被解释为 PHP 文件或 JPG 图像: exploit.php.jpg
添加尾随字符。某些组件会删除或忽略尾随空格、点号等: exploit.php.
尝试使用 URL 编码(或双重 URL 编码)来处理点号、正斜杠和反斜杠。如果在验证文件扩展名时未对值进行解码,但在服务器端后续解码,这也可能允许你上传恶意文件,而这些文件原本会被阻止: exploit%2Ephp
在文件扩展名之前添加分号或 URL 编码的空字节字符。如果验证代码是用 PHP 或 Java 等高级语言编写的,但服务器使用 C/C++等低级函数处理文件,例如,这会导致对文件名结尾的处理出现不一致: exploit.asp;.jpg
或 exploit.asp%00.jpg
尝试使用多字节 Unicode 字符,这些字符在 Unicode 转换或规范化后可能会被转换为空字节和点。像 xC0 x2E
、 xC4 xAE
或 xC0 xAE
这样的序列,如果文件名被解析为 UTF-8 字符串,可能会被转换为 x2E
,但在用于路径之前被转换为 ASCII 字符。
其他防御措施包括移除或替换危险的文件扩展名,以防止文件被执行。如果这种转换不是递归执行的,你可以将禁止的字符串放置在某种位置,使得移除它后仍然留下有效的文件扩展名。例如,考虑一下从以下文件名中移除 .php
会发生什么:exploit.p.phphp
文件内容验证存在缺陷
与其隐式信任请求中指定的 Content-Type
,更安全的服务器会尝试验证文件内容是否确实符合预期
在图像上传功能的情况下,服务器可能会尝试验证图像的某些内在属性,例如其尺寸。如果你尝试上传一个 PHP 脚本,例如,它将没有任何尺寸。因此,服务器可以推断出它不可能是图像,并相应地拒绝上传
同样地,某些文件类型在其头部或尾部可能始终包含特定的字节序列。这些序列可以像指纹或签名一样使用,以确定内容是否与预期类型匹配。例如,JPEG 文件始终以字节 FF D8 FF
开头。
这是一种更强大的文件类型验证方式,但即便如此也不是万无一失的。使用特殊工具,如 ExifTool,可以轻易创建包含恶意代码的多语言 JPEG 文件。
利用文件上传竞态条件
现代框架对这些攻击更具抵抗力。它们通常不会直接将文件上传到文件系统的目标位置。相反,它们会先采取预防措施,如先上传到临时、沙盒目录,并随机化文件名以避免覆盖现有文件。然后,它们会对这个临时文件进行验证,只有当它被认为是安全时,才会将其转移到目标位置。
例如,一些网站会将文件直接上传到主文件系统,如果它未通过验证,则再次将其删除。这种行为在依赖杀毒软件等工具检查恶意软件的网站上很常见。这可能只花费几毫秒,但对于文件存在于服务器上的短暂时间,攻击者仍然可能执行它
基于 URL 的文件上传中的竞态条件
在允许通过 URL 上传文件的功能中也可能发生类似的竞态条件。在这种情况下,服务器必须先从互联网获取文件并创建本地副本,然后才能执行任何验证
例如,如果文件被加载到一个具有随机名称的临时目录中,理论上攻击者应该不可能利用任何竞态条件。如果他们不知道目录的名称,他们将无法请求该文件以触发其执行。另一方面,如果随机生成的目录名称使用伪随机函数(如 PHP 的 uniqid()
)生成,则可能被暴力破解。
可以尝试延长处理文件所需的时间,从而延长暴力破解目录名称的时间窗口。一种方法是通过上传一个较大的文件来实现。如果它是以块的方式处理的,您可以通过创建一个带有有效负载的恶意文件,然后跟随着大量的任意填充字节来利用这一点。
利用文件上传漏洞而不执行远程代码
如果你可以上传 HTML 文件或 SVG 图像,你可能会使用 <script>
标签来创建存储型 XSS 有效载荷。
如果上传的文件出现在其他用户访问的页面上,当他们的浏览器尝试渲染该页面时,将会执行该脚本。请注意,由于同源策略的限制,这类攻击只有在上传的文件从与上传相同的源提供时才会起作用。
利用上传文件解析中的漏洞
如果上传的文件似乎既被安全存储又被安全提供,最后的办法是尝试利用针对不同文件格式解析或处理的特定漏洞。例如,你知道服务器解析基于 XML 的文件,如 Microsoft Office .doc
或 .xls
文件,这可能是一个潜在的 XXE 注入攻击向量
使用 PUT 上传文件
值得注意的是,某些 Web 服务器可能被配置为支持 PUT
请求。如果适当的防御措施没有到位,这可能会提供一种替代的上传恶意文件的方式,即使通过 Web 界面没有提供上传功能。
PUT /images/exploit.php HTTP/1.1
Host: vulnerable-website.com
Content-Type: application/x-httpd-php
Content-Length: 49
<?php echo file_get_contents('/path/to/file'); ?>
您可以尝试向不同的端点发送 OPTIONS
请求,以测试是否存在任何宣传支持 PUT
方法的端点。
如何防止文件上传漏洞
- 检查文件扩展名是否在允许的扩展名白名单中,而不是在禁止的扩展名黑名单中。猜测你可能想要允许的扩展名要容易得多,而猜测攻击者可能会尝试上传的扩展名则要困难得多。
- 确保文件名不包含任何可能被解释为目录或遍历序列(
../
)的子字符串。 - 重命名上传的文件,以避免可能覆盖现有文件的事故。
- 在文件经过完全验证之前,不要将文件上传到服务器的永久文件系统。
- 尽可能使用成熟的框架来预处理文件上传,而不是尝试自己编写验证机制。