【Portswigger 学院】文件上传

教程和靶场来源于 Burpsuite 的官网 Portswigger:File upload vulnerabilities - PortSwigger

原理与危害

很多网站都有文件上传的功能,比如在个人信息页面允许用户上传图片作为头像。如果网站应用程序对用户上传的文件没有针对文件名、文件类型、文件内容或文件大小做充分的验证,就可能导致允许攻击者上传有潜在危险的文件,比如服务端的脚本文件,然后访问该文件以触发代码执行。

文件上传漏洞的危害取决于两个因素:

  • 网站应用程序对文件名、文件类型、文件内容或文件大小等参数中的哪一个没有做充分的验证。
  • 上传文件成功后受到哪些限制。

最坏的情况是文件类型没有受到服务器的任何限制,允许 .php.jsp 作为脚本文件执行,那么攻击者可以上传 .php.jsp 文件作为 webshell,接管服务器。

其他的一些危害:

  • 对文件名没有做验证,攻击者上传一个同名的文件,覆盖服务器上已存在的文件,如果还存在路径遍历漏洞,那么危害更大,可以覆盖服务器的系统文件。
  • 对文件大小没有做验证,攻击者上传一个非常大的文件,占用服务器的磁盘空间,也就是 DoS 攻击。

由于文件上传漏洞的危害很大,所以现在大部分网站应用程序都做了防御,能直接上传 .php.jsp 文件的网站很少看到了。然而,漏洞仍然会产生,这是因为开发人员坚信他们的防御足够有效。

静态文件处理

在刚学习 Web 安全时,看了很多课程和书籍,我们潜意识认为一个 URL 对应网站服务器上的一个文件,对应关系是 1:1,比如 http://example.com/includes/func.php 这个 URL 在网站服务器上对应的文件位于网站根目录下的 includes 目录下的 func.php 文件。这种理解在以前静态网站甚至 PHP 网站和 ASP 网站流行的时候确实是这样,这只不过是 Apache 或 IIS 等中间件正好这样管理站点的文件,但是现代 Web 应用程序已经不是这样 URL 与文件呈现一对一的映射关系,我们现在看到一个 URL 比如  https://****.net/mp_blog/creation/editor,在网站服务器并不存在一个路径为 <网站根目录>/mp_blog/creation/editor 的文件

当然,像图片、CSS 和 JS 等静态资源,Web 服务器仍然用一对一的映射关系处理它们,处理步骤是:解析 URL 中的 path 部分,识别出请求的文件的扩展名,然后根据扩展名匹配对应的 MIME,最后根据服务器预先的配置执行下一步:

  • 如果文件类型是不可执行的,就作为静态文件把文件内容响应给客户端。
  • 如果文件类型是可执行的,例如 PHP 文件,就创建运行环境,根据请求头和请求参数分配变量,然后执行脚本。
  • 如果文件类型是可执行的,但服务器被配置成不执行该类型的文件,就给客户端响应一个错误。不过,大多数情况下,服务器仍然把这些当成普通文本,将它们的内容返回给客户端,这一点特性可能被利用实现泄露代码或其他敏感信息的目的。

实验

实验说明:

服务器没有对上传的文件没做任何验证,上传一个 PHP 脚本并读取 /home/carlos/secret 文件的内容就能完成实验。

进入实验场景:

用账号 wiener:peter 登录,进入个人信息页面:

点击最下面的“浏览”按钮,选择一个 PHP 脚本上传,PHP 脚本的内容:

<?php echo file_get_contents("/home/carlos/secret"); ?>

读取并返回 /home/carlos/secret 文件的内容。

点击“Upload”按钮上传文件,响应结果:

结果表示文件上传成功并给出保存的文件路径。除了这里可以看到保存的文件路径,还可以返回个人信息页面打开 F12 查看路径:

访问 /files/avatars/info.php 就能读取到 /home/carlos/secret 文件的内容:

复制这段数据,返回到首页点击 “Submit solution” 按钮提交即可。

绕过验证机制

Content-Type 伪造

提交表单后客户端发送一个 POST HTTP 请求,Content-Type 是请求包中的一个请求头,表示请求体的内容类型,如果是发送一段简短的文本,Content-Type 的值一般是 application/x-www-form-url-encoded,但如果发送的是一个大文件,比如 PDF,那么就得用文件上传的方式发送,此时的 Content-Type 是 multipart/form-data,表示表单数据多个部分,而 HTTP 请求包内容类似于这样:

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--

表单中每个输入之间用一段 ---------------------------012345678901234567890123456 字符串分割,第一个输入是图片,它也有自己的 Content-Type,值是 image/jpeg,表示这部分属于图片类型。第二个输入是没有 Content-Type,其实它相当于一个请求参数,参数名是 decsription,参数值是 

This is an interesting description of my image.

第三个输入同上。

如果服务器在接受上传的文件时,根据 Content-Type 决定文件的类型,那么就能让攻击者利用伪造绕过。如图:

结合路径遍历漏洞

上传文件的存储目录可能被限制为不允许执行脚本,这的确是可以实现的配置。例如,Apache 服务器可以针对某个特定目录配置特性,只要在目录下放置一个 .htaccess 文件,里面配置如下的指令:

php_flag engine offphp_flag engine off

那么当前目录下的所有脚本被访问时都不会运行,而是当作普通文本返回。

尝试利用路径遍历漏洞绕过这个限制,把文件上传到存储目录的上一级目录:

访问该文件时注意它的路径,avatars/../info.php 其实就是 info.php。

这里用 URL 编码处理了斜杠,因为应用程序在保存上传之前对文件名做了一次 URL 解码处理:

<?php
$target_dir = "avatars/";
$target_file = urldecode($target_dir.$_FILES["avatar"]["name"]);

if (strpos($target_file, ".htaccess")) {
  echo "The upload of .htaccess files is prohibited.";
  http_response_code(403);
} else if (move_uploaded_file($_FILES["avatar"]["tmp_name"], $target_file)) {
  echo "The file ". htmlspecialchars( $target_file). " has been uploaded.";
} else {
  echo "Sorry, there was an error uploading your file.";
  http_response_code(403);
}
?>

如果没有做 URL 解码处理,文件名中的 ../..\ 会被去掉。我在本地做了一个测试,如图:

也就是说文件上传漏洞结合路径遍历漏洞需要满足一定的条件。

重写服务器配置

有些应用程序对文件上传功能的安全防护是设置一个后缀名黑名单,里面包含不允许上传的文件后缀名,但这仍然存在风险,因为无法囊括所有不合法的后缀名,比如 .php5.shtml 等等,这些可能会被忽略。另外,相关配置文件的文件名或后缀名也应该被列为黑名单。

大多数 Web 服务器并不是在一开始安装成功后就能执行文件的,像 Apache,要执行 PHP 脚本,需要在 /etc/apache2/apache2.conf 配置文件中添加如下指令:

LoadModule php_module /usr/lib/apache2/modules/libphp.so
    AddType application/x-httpd-php .php

意思是加载 php 模块,以及当访问后缀名为 .php 的文件时,作为 php 脚本执行。

不止 Apache,Nginx 和 IIS 也都是要做一些配置才能执行脚本。

除了 apache2.conf 这个系统级的配置文件,Apache 服务器允许在每个目录下放置一个 .htaccess 文件,在该文件中添加的指令可以覆盖和补充 apache2.conf 的配置。如果应用程序允许上传 .htaccess,那么就能利用它绕过安全防护,执行代码。

首先,第一步是先上传 .htaccess

文件内容:

AddType application/x-httpd-php .png

这表示 .png 后缀的文件也被当成 PHP 脚本执行。

第二步是上传包含 PHP 代码的 .png 文件:

最后一步是访问 phpinfo.png 文件,触发代码执行。

IIS 服务器也有一个类似的配置文件,名为 web.config,它的指令示例:

<staticContent>
    <mimeMap fileExtension=".json" mimeType="application/json" />
    </staticContent>

表示 .json 文件被当成 json 发送给客户端。

PS:通常,.htaccess 和 web.config 被从客户端禁止访问。

混淆文件后缀

下面列举一些混淆技巧:

  • 如果在验证文件后缀名是否合法时区分大小写,但是在后缀名映射 MIME 时不区分大小写,那么可尝试大写后缀,比如 exploit.pHpexploit.PHP 等。
  • 多后缀名,比如 exploit.php.jpg。(Apache 多后缀解析漏洞)
  • 末尾加点,比如 exploit.php.  。(Windows不允许文件名末尾有点,如果有,会自动去掉)
  • 在验证文件后缀名之前没有做 URL 解码处理,但是在之后做了,那么可用 URL 编码绕过安全验证,比如 exploit%2Ephp
  • 如果服务器用低级语言如 C/C++ 写的,那么可尝试空字节绕过,比如 exploit.asp%00.jpg
  • 结合 IIS 6.0 解析漏洞,如:exploit.asp;.jpg
  • 如果应用程序匹配到不合法的后缀名后只做一次删除处理,而不是递归删除,那么可双写后缀绕过,比如 exploit.p.phphp

下图是利用空字节绕过的一个例子:

下面是应用程序处理这个文件名的代码,我们看看如何处理这个 %00 的:

<?php
$target_dir = "avatars/";
$target_file = urldecode($target_dir . $_FILES["avatar"]["name"]);
$uploadOk = true;

$imageFileType = strtolower(pathinfo($target_file,PATHINFO_EXTENSION));

if($imageFileType != "jpg" && $imageFileType != "png") {
  echo "Sorry, only JPG & PNG files are allowed\n";
  $uploadOk = false;
}

//去掉空字节及其后面的字符
$target_file = strtok($target_file, chr(0));

if ($uploadOk && move_uploaded_file($_FILES["avatar"]["tmp_name"], $target_file)) {
  echo "The file ". htmlspecialchars( $target_file). " has been uploaded.";
} else {
  echo "Sorry, there was an error uploading your file.";
  http_response_code(403);
}

这很容易看懂。然而,在实际的场景中,并不会有开发人员会这样处理文件名,让攻击者有利用空字节绕过的可能,更多是服务器由 C/C++ 开发时才有可能出现。

文件内容验证绕过

当应用程序不信任 Content-Type 所指示的那样判断一个文件的类型,可能会直接通过验证文件的内容来判断。对于这种情况,最简单的验证方式就是查看文件开头几个字节,这几个字节又称魔术数字,大多数类型的文件都会在开头设置独一无二的字节序列,比如 JPEG 文件的就是 FF D8 FF,这种验证方式很容易绕过,因为攻击者也能在脚本的开头放置这几个字节并且不会影响代码执行。

比较复杂的情况是,应用程序用一些图片处理函数来判断上传的文件的数据是否真的是合法的图片数据,比如获取图片的尺寸,脚本文件就不会有这种数据。但是,攻击者可以将代码嵌入到图片的一些数据区,这些数据区不会破坏图片本身的数据和图片的渲染,比如 JPEG 图片的注释部分,用 Exiftool 工具可以做到这些事情。(Exiftool 工具在 kali 系统中已默认安装)

Exiftool 将 PHP 代码嵌入到 JPG 图片的注释部分:

exiftool -Comment="<?php echo 'START ' . file_get_contents('/home/carlos/secret') . ' END'; ?>" test.jpg -o polyglot.php

意思是将 -Comment 参数指定的内容嵌入到 test.jpg 的注释部分,最后生成 polyglot.php 文件。

条件竞争上传文件

有些应用程序接收到上传的文件后先保存到网站的一个目录下,接着对文件的安全性做验证,如果文件被判断为不安全的就会删除该文件,这样的处理是有问题。在保存到目录后和被判断为不安全而删除之前存在一个时间空隙,攻击者在这段空隙访问所上传的文件就能执行该文件,这是能够做到,利用预先写好的程序快速不停地访问即可。

现代开发框架都内置了处理文件上传的组件,它们通常都能够阻止这类漏洞,其处理步骤是

  1. 把上传的文件保存到一个沙盒化的临时目录(这里的文件被隔离且无法被访问到并执行);
  2. 重命名为一个随机字符串以避免文件覆盖;
  3. 做安全性验证,如果文件被判断为安全的,就移动文件到最终保存的目录,否则就删除。

如果开发人员自己编写文件上传处理程序,那么就有可能存在这类漏洞。

攻击者很难在黑盒测试中找到这种漏洞,除非他能找到应用程序的源代码,通过审计发现这种漏洞。

另外,还有一种情况可能发生条件竞争文件上传漏洞,就是让用户提供一个 URL 来保存文件,这一般只能由开发人员自己编写处理程序,也必须先下载一份文件副本到本地,然后再做安全性验证,所以这种情况很容易出现漏洞。

如果上传的文件或通过 URL 下载的文件保存到一个具有随机名称的临时目录,这一般不太可能被攻击者知道,但如果临时目录是伪随机数,那么仍然有可能被攻击者用暴力破解的方法知道。

为了让条件竞争上传文件的攻击更容易,攻击者会上传一个恶意的大文件,里面填充大量无用字符,但不会影响到里面的代码被执行,应用程序处理这类大文件会处理得更久,那么文件在文件系统上停留的时间更久,也就更容易被攻击者趁机访问。

客户端攻击

能上传带有恶意代码的文件是最严重的漏洞,这是针对服务器的攻击,但文件上传也能用于针对客户端攻击。上传一个 HTML 文件或 SVG 图像,里面包含 XSS 代码,那么就相当于是一个存储型 XSS 漏洞了,然后把文件的 URL 发给受害者,因为域名是合法的,所以更容易被信任。(注意,上传的文件如果被保存到另一个网站,那么要考虑同源策略的影响)

PUT 文件上传

某些服务器被配置为允许 HTTP PUT 请求方法,这种请求方法是用于上传文件,攻击者可先用 OPTIONS 请求方法查看网站是否支持 HTTP PUT 请求方法,然后再尝试上传文件。

PUT 上传请求示例:

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'); ?>

防护

下面是一些防护方法:

  • 使用后缀名白名单而不是黑名单,因为列举出所有允许的后缀名更容易。
  • 文件名不能包含 ../ 序列。
  • 重命名文件避免文件覆盖。
  • 对文件做完安全性验证再移动它到永久保存的目录,在此之前保存到临时目录。
  • 尽量使用框架内置的文件上传处理组件,而不是自己编写。
上一篇:鸿蒙语言基础类库:【@ohos.url (URL字符串解析)】


下一篇:定个小目标之刷LeetCode热题(42)