pikachu之 unsafe upfileupload (不安全的文件上传漏洞)
不安全的文件上传漏洞概述
文件上传功能在web应用系统很常见,比如很多网站注册的时候需要上传头像、上传附件等等。当用户点击上传按钮后,后台会对上传的文件进行判断 比如是否是指定的类型、后缀名、大小等等,然后将其按照设计的格式进行重命名后存储在指定的目录。 如果说后台对上传的文件没有进行任何的安全判断或者判断条件不够严谨,则攻击着可能会上传一些恶意的文件,比如一句话木马,从而导致后台服务器被webshell。
所以,在设计文件上传功能时,一定要对传进来的文件进行严格的安全考虑。比如:
--验证文件类型、后缀名、大小;
--验证文件的上传方式;
--对文件进行一定复杂的重命名;
--不要暴露文件上传后的路径;
--等等...
client check
首先进入页面,题目要求只能上传图片,大概格式是jpg,png,gif等格式。
创建一个phpinfo.txt,其内容为
<? echo phpinfo(); ?>
接着将其后缀改为.png,随后点击浏览选中该文件。
然后再开始上传的时候抓包,然后将filename的phpinfo.png改为phpinfo.php,随后放包。
ok上传成功,浏览器返回了文件保存的路径。
尝试访问,在当前目录下面拼接uploads/phpinfo.php,ok访问成功,为什么不在http://192.168.112.1/vul/unsafeupload/clientcheck.php#后面直接拼接呢?因为clientcheck.php是当前unsafeupload目录下面的一个php文件。
MIME type
创建文件phpinfo.php,内容为
<? echo phpinfo(); ?>
随后点击选择文件,选中我们刚刚的phpinfo.php文件。
在点击开始上传的时候抓包。
随后将Content-Type: application/octet-stream改为Content-Type: inage/png,即告诉服务器我上传是一个png文件,随后放包,发现上传成功。
访问一把,输出成功。
getimagesize()
在过这一关时看到了getimagesize()这个函数,它是php的一个内置函数,它的主要作用就是获取一个图片文件的基本信息(如尺寸和类型),它还能准确判断文件的真实类型,这使它在文件上传安全校验中扮演着至关重要的角色。它的工作原理是读取图片文件的文件头 来判断其真实的图像类型和尺寸。
使用 getimagesize()
函数进行校验,可以直接有效地防御以下两种低级攻击方式:
-
只修改文件后缀名(例如:将
shell.php
重命名为shell.jpg
)。 -
只修改 HTTP 请求中的 MIME Type 头(例如:在上传表单中将
Content-Type
从text/plain
改为image/jpeg
)。
那么这个函数,首先它就是通过读取文件内容的文件头来确定这个文件是否是图片格式,如果是的话会通过数组的方式返回哪种图片类型,如果文件不是比如jpg,jpeg,png,gif等图片文件,就会产生报错,从而进行简单防御。
这个函数一般可以使用图片马进行攻击!因为该函数仅仅检查文件头,没有检查整个图片文件导致可以上传包含有php恶意代码的文件!
思路一:
首先网上随便下载一个图片,比如我下载的是C.jpg的一张图片,然后编辑一个php.php文件,内容为:
<?php
phpinfo();
?>
随后进入windows终端,输入(注意你的图片文件位置!)
copy C.jpg/b+php.php/a 2.jpg
其实就是将php代码内容追加到图片文件最后随后形成新的文件2.jpg
或
copy /b C.jpg+php.php 10.jpg
随后上传10.jpg文件
然后进入pikachu靶场的文件包含漏洞靶场,上传文件10.jpg,随后发现解析成功。
思路二(GIF89a):
本道题缺少MIME类型一致性校验,就是虽然本关要求必须是jpg,jpeg,png后缀,但是没有要求getimagesize()函数返回的文件类型必须满足image/jpeg或image/jpg或image/png。从而导致这个函数只要不报错,然后文件后缀满足jpg,png和jpeg的形式,那么就会成功上传。
既然如此那么我们可以伪造一个具有恶意php代码的gif文件,随后更改格式为png格式。
GIF89a和GIF89他们都代表是gif文件,位于gif文件的开头。
首先我们创建一个文本文档,输入以下格式:
GIF89a
<?php phpinfo();
?>
随后将文本文档名字改为4.png,随后上传。
上面这两种方法其实是图片马。
当我们上传了一个图片马,那么怎么触发它呢?
首先直接用浏览器url搜索图片马地址是错误的行为,因为当服务器收到一个对 .jpg
文件的请求时,它会根据预先配置的 MIME类型 将其识别为 image/jpeg
。
因此,浏览器只会收到图片的二进制数据并显示它,附加的PHP代码只是被当作图片的无意义二进制数据的一部分传送,永远不会被PHP引擎解析。
那么如何才能解析其中的php文件呢?
配合文件包含漏洞
什么是文件包含漏洞?
文件包含漏洞通常发生在一个PHP脚本中,用 include
, require
, include_once
, require_once
等函数动态地包含外部文件,但却没有对用户传入的参数进行严格的过滤。从而读取指定文件的内容,并将其中的代码当作PHP代码来执行。
这里我们直接使用pikachu靶场中的File Inclusion测试栏目配合演示,打开文件包含漏洞靶场。
将filename的值改为图片马的地址,发现执行成功!
其他服务端检测(重点)
文件扩展名绕过
修改文件后缀即可
ashx上传绕过
特殊文件名绕过
00截断绕过上传
就这样,一个恶意的PHP文件被成功上传,并且扩展名是 .php
。
-
攻击者构造文件名:在上传时,将文件名改为
evil.php%00.jpg
(注意:这里的%00
是URL编码,实际发送的HTTP请求中,它会被解码成空字节0x00
)。 -
务器的检查逻辑(第1, 2步):
-
$file_name = "evil.php%00.jpg";
-> 但PHP在接收到请求参数时,可能会自动对URL编码进行解码,所以变量实际变成了"evil.php\0.jpg"
。 -
$file_ext = substr("evil.php\0.jpg", ...);
-> 这个函数在读取到空字节\0
时,认为字符串已经结束,所以它提取出的扩展名是jpg
! -
白名单检查:
'jpg'
在['jpg','png','gif']
里吗?在!检查通过!
-
-
服务器的保存逻辑(第3步):
-
move_uploaded_file($tmp_name, '/uploads/evil.php\0.jpg');
-
这个文件系统函数(底层是C语言函数)在处理保存路径
/uploads/evil.php\0.jpg
时,同样遇到了空字节\0
。 -
它认为路径名到此为止,所以最终保存的文件名是
/uploads/evil.php
。
-
htaccess解析漏洞(服务器使用Apache)
什么是 .htaccess?
是Apache Web服务器的一个配置文件。它允许用户针对特定目录及其子目录修改服务器的配置,而无需拥有主服务器配置文件的权限(httpd.conf
)。
它的主要就是强制指定特定文件的解析方式,这一点,正是我们绕过上传限制的关键。
攻击步骤
第1步:准备一个“图片马”
创建一个内容为一句话木马的文本文件,将其命名为 shell.jpg
。
第2步:准备一个恶意的 .htaccess
文件
创建一个名为 .htaccess
的文本文件(注意前面有个点),内容如下:
apache
# 最常用、最有效的方法:使用AddType指令 AddType application/x-httpd-php .jpg# 其他可用的方法: # SetHandler application/x-httpd-php
-
AddType application/x-httpd-php .jpg
: 这行命令告诉Apache服务器:“在这个目录及其子目录下,所有扩展名为.jpg
的文件,都不要当成图片处理,而应该当成PHP脚本来解析和执行。” -
SetHandler application/x-httpd-php
: 这行命令更狠,它表示:“这个目录下的所有文件,无论扩展名是什么,都当成PHP脚本来执行。”
第3步:上传文件
-
先将恶意的
.htaccess
文件上传到目标服务器的上传目录(例如/uploads/
)。 -
再将“图片马”
shell.jpg
上传到同一个目录。
第4步:访问执行
现在,你不再需要寻找一个 .php
文件了。你直接访问你上传的“图片”即可:
http://target.com/uploads/shell.jpg
Apache服务器在收到这个请求时,会先去检查 /uploads/
目录下有没有 .htaccess
文件。它找到了你上传的那个,并且读到了规则:“哦,.jpg
文件要当成PHP执行。” 于是,它就会调用PHP解析器来解析 shell.jpg
文件中的代码。
你的PHP代码 <?php @eval($_POST['cmd']);?>
就被成功执行了!你可以用中国蚁剑、中国菜刀等工具直接连接这个“图片”地址,就像连接一个PHP木马一样。
.user.ini绕过(服务器使用PHP)
什么是.user.ini?
是PHP特有的一个配置文件。它类似于Apache的 .htaccess
,允许用户在每个目录下覆盖部分的PHP配置指令
攻击步骤
攻击者成功上传两个文件到同一个目录(例如 /uploads/
):
-
一个恶意的
.user.ini
文件,内容为auto_prepend_file=mac.png
。 -
一个图片马
mac.png
,内容包含PHP木马。
之后,当任何用户(或攻击者自己)访问该目录下的任何一个合法的PHP文件时(例如 /uploads/index.php
或 /uploads/somefile.php
,甚至网站其他功能可能会调用上传目录里的PHP文件),会发生以下事情:
-
PHP准备执行
index.php
。 -
它先检查
index.php
所在目录是否有.user.ini
,发现了攻击者上传的文件。 -
它读取配置,知道需要先包含
mac.png
。 -
于是,PHP开始解析
mac.png
。由于服务器配置(通常默认)会将图片文件当作HTML/text处理,不会执行其中的PHP代码。 -
但是!
auto_prepend_file
指令的工作机制是include
,而include
在包含一个文件时,会无视文件的扩展名,直接尝试解析文件内容中的PHP代码。 -
因此,
mac.png
文件中的<?php ... ?>
代码被成功执行。 -
执行完图片马中的木马后,再继续执行原本的
index.php
文件。
这样一来,攻击者就通过“劫持”正常PHP文件执行流程的方式,间接地执行了图片马中的恶意代码。
突破MIME限制上传
什么是MIME类型?
MIME类型(Multipurpose Internet Mail Extensions)是一种标准,用来表示文档、文件或字节流的性质和格式。它在互联网上被广泛使用,尤其是在HTTP协议中,浏览器通过响应头中的 Content-Type
来知道如何处理服务器返回的内容。
常见例子:
-
text/html
-> HTML文档 -
image/jpeg
-> JPEG图片 -
image/png
-> PNG图片 -
application/pdf
-> PDF文档 -
application/x-php
-> PHP脚本 (非官方,但常用)
突破MIME限制就是修改客户端发送的MIME类型,如:
修改前:
http请求:Content-Disposition: form-data; name="file"; filename="shell.php"
Content-Type: application/octet-stream <-- 原来的类型,会被服务器拒绝
修改后:
http请求:Content-Disposition: form-data; name="file"; filename="shell.php"
Content-Type: image/jpeg <-- 修改为允许的图片类型
随后放包即可!
解析漏洞绕过
条件竞争绕过
其实就是利用burp的并发插件,并且使用一个脚本文件。
很多文件上传功能的安全设计存在一个“时间窗”(Time Window)漏洞。其典型流程如下:
-
上传文件:将文件临时保存在服务器某个目录。
-
安全检查:对临时文件进行病毒扫描、内容检测、扩展名检查等。
-
决定命运:
-
如果检查通过:将临时文件移动到最终的可访问目录(如
/uploads/
),并赋予其最终文件名。 -
如果检查不通过:删除临时文件。
-
这个设计的漏洞在于:在第1步完成之后,到第3步执行之前,存在一个极短的时间窗口。攻击者的目标就是在这个短暂的时间窗口内,抢在服务器删除文件之前,去访问并执行这个临时文件。
CONTENT-LENGTH绕过
有时会要求文件大小限制,可以通过抓包篡改content-length内容来实现绕过。
以下是两种类型:
1:仅校验 CONTENT-LENGTH,不校验实际请求体大小
部分服务器的防御逻辑仅 “被动信任”CONTENT-LENGTH
字段的值,未对实际接收的请求体数据大小进行二次校验。
例如:服务器配置 “允许上传最大 100KB(102400 字节)的文件”,防御逻辑仅判断CONTENT-LENGTH: 102399
(小于 102400)即允许通过,但未检查实际请求体中文件数据的真实大小。
攻击者利用方式:
- 构造
CONTENT-LENGTH
为小于限制值的虚假数值(如实际文件 1MB,却将CONTENT-LENGTH
设为 100000); - 服务器通过
CONTENT-LENGTH
校验后,开始接收请求体数据; - 由于服务器未校验实际接收的字节数,攻击者可继续发送完整的 1MB 恶意文件数据,服务器会 “超额接收” 并保存该文件,从而绕过大小限制。
2:对 CONTENT-LENGTH 的异常值处理不当
HTTP 协议对CONTENT-LENGTH
的格式、取值有明确规范(需为非负整数),但部分服务器未处理 “异常值”,导致校验逻辑失效。常见异常值场景包括:
- 负数:如
CONTENT-LENGTH: -1024
,若服务器未过滤负数,可能因数值比较逻辑错误(如 “if (contentLength> maxSize)”,负数永远满足 “小于 maxSize”)而绕过; - 特殊字符:如
CONTENT-LENGTH: 1024a
(字母)、CONTENT-LENGTH: 1,024
(逗号),若服务器未严格校验字段格式,可能将异常字符截断(如取 “1024”)或解析为 0,进而绕过限制; - 多字段重复:HTTP 协议未禁止
CONTENT-LENGTH
重复出现(如CONTENT-LENGTH: 1000
与CONTENT-LENGTH: 200000
),若服务器仅读取第一个字段值,攻击者可将第一个设为合法值、第二个设为真实值,实现绕过。