| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 | 31 |
- 알고리즘
- Practical Malware Analysis Labs
- 프리코스
- sk 쉴더스 루키즈
- sk 쉴더스 루키즈 31기
- 깃
- 악성코드 분석
- 레나 튜토리얼
- sk 쉴더스
- 우테코
- React
- webhacking
- 위상 정렬
- SK쉴더스루키즈
- 루키즈31기
- linux
- 리버싱
- sk쉴더스 루키즈
- 루키즈 31기
- c
- 자바
- 웹개발
- 동적분석
- 우아한테크코스
- 루키즈
- top-cert
- 백엔드
- 레나튜토리얼
- Dreamhack
- 예술의 전당
- Today
- Total
yon11b
[SK 쉴더스 루키즈] 파일 다운로드 취약점 실습 문제(1~4번)와 대응방안 본문
1번: 단순 path traversal
/answer1/down.txt 다운 받아라.
아무 파일이나 업로드해서 게시글을 작성해준다.
그리고 그 게시글을 조회했을 때 해당 파일을 어느 경로에서 불러오는지 확인해보자.
서버가 올라간 파일을 어디서 불러오는지 그 경로를 보면 answer1/down.txt도 불러올 수 있을 것 같다.
나는 down.txt 이름으로 된 파일을 올렸다.

https://lab.eqst.co.kr:8086/exam26/down/download2.php?file_path=/lib/file/faq/down.txt 여기서 불러온다.
여기서 file_path를 조작해보자.
첫번째 시도
내가 원하는 파일 경로는 /answer1/down.txt이므로 그냥 무지성으로 file_path 자리에 넣어본다.


다운을 받아봤더니 0KB로 그냥 빈 파일만 준다… 경로가 잘못되었나보다.
어떻게 공격할지 생각하기
나는 file_path 다음의 경로가 실제 서버내부에서 어떻게 붙는지 모른다.
/var/path/answer1/down.txt 이렇게 붙을 수도 있고,
/var/a/b/c/answer1/down.txt 이렇게 붙을 수도 있다.
클라이언트 입장에서는 이걸 알 수가 없다.
그런데 우리 문제에서는 파일이 루트 경로에 존재한다.(/answer~로 시작하니까)
그럼 우리는 루트까지 일단 올라가야 한다. => ../로 계속 올라가야함.
실행하기
file_path=../../answer1/down.txt
file_path=../../../answer1/down.txt
이런식으로 하나씩 ../을 추가해서 요청을 보내고 응답이 오는지 확인해보면 된다.
정답: ../../../../../../../answer1/down.txt

실제로 다운을 받아 확인해봐도 보인다.

2번: 블랙리스트 필터링
1번과 똑같이 answer2/down.txt를 구하는 문제이다.
그런데 이번에는 아무리 ../로 올라가도 파일이 안 보인다.

응답을 보면 다 content-length가 0이다.
이런 경우, 이유를 다음과 같이 추측해볼 수 있다.
1. 해당 경로에 파일이 없다. -> ../을 계속 해봤는데도 안 나왔음. 이건 아닐듯
2. ../이 필터링 되어 상위경로로 이동되지 않았다. -> 일리있음
3. 파일은 있는데 0바이트이다. -> 이럴리가 없음. 문제인데.
필터링 처리하는 방법에는 여러가지가 있겠지만 여기서는 ../이 보이면 제거를 한다.
$file_path = str_replace("../", "", $_GET['file_path']);
코드로 나타내면 이런 느낌
필터링 우회 방법
-> ....//으로 필터링 걸리는 ../을 가운데에 두고 또다른 ../로 크게 감싸준다.
script 문자열이 필터링 걸렸을 때 scriscriptpt 써주는 것과 같은 원리!
GET /exam27/down/download2.php?file_path=....//....//....//....//....//....//....//....//answer2/down.txt HTTP/1.1

3번: 더블인코딩으로 해결
/answer3/down.txt를 다운 받는 문제.
이번에는 ....// 을 해줘도 안 된다. 이런 경우 더블인코딩을 생각해볼 수 있다.
서버 코드가 다음과 같다고 추측해볼 수 있다.
$file_path = $_GET['file_path']; // 이미 1차 디코딩된 값: %2e%2e%2f...
if (strpos($file_path, "../") !== false) {
exit("blocked");
}
$file_path = urldecode($file_path); // 여기서 ../ 로 변함
readfile($file_path);
1차 URL 디코딩:
%252e%252e%252f
→ %2e%2e%2f
이 상태에서 필터가 검사하면 문자열에 ../가 없다.
%2e%2e%2f
그래서 ../ 필터를 통과한다.
그 다음 서버 코드 어딘가에서 한 번 더 디코딩된다.
%2e%2e%2f
→ ../
결국 실제 파일 접근 시점에는 이렇게 된다.
../../../../answer3/down.txt
그래서 성공한다.
정답
GET /exam28/down/download2.php?file_path=%252e%252e%252f%252e%252e%252f%252e%252e%252f%252e%252e%252f%252e%252e%252f%252e%252e%252f%252e%252e%252f%252e%252e%252fanswer3%252fdown.txt HTTP/1.1

4번: 단순 path traversal
파일 다운로드 취약점을 이용하여 '/data/files/qna/a.txt'를 다운받아 정답을 확인하시오.
file_path=../data/files/qna/20260701/2c9a50fc-e47a-4caf-88ba-e24d19a4109e.png
어떤 파일 다운로드 요청을 잡아서 봤더니 경로가 이렇게 생겼다
../data/files/qna/날짜/파일명
이 경로가 서버 기준 상대경로이므로 이 경로를 참고 + 우리가 구해야 하는 파일 경로를 잘 조합해서 막 넣어보면 된다.
../data/files/qna/a.txt
../data/files/qna/20260701/a.txt
...

성공!
대응방안: fileId만 받기
파일 다운로드에서 사용자가 주면 안 되는 값은 filePath다.
GET /download?filePath=../../../../answer/down.txt
이런 방식은 위험하다. 사용자가 서버 내부 경로를 조작할 수 있기 때문이다.
안전한 방식은 사용자가 fileId만 보내는 것이다.
GET /download?fileId=2
서버는 fileId로 DB를 조회한다.
node js 서버 코드 예시
app.get("/download", async (req, res) => {
const fileId = req.query.fileId;
const file = await db.files.findByPk(fileId);
if (!file) {
return res.status(404).send("File not found");
}
const filePath = path.join(__dirname, "uploads", file.storedName);
return res.download(filePath, file.originalName);
});
사용자로부터 fileId를 받고 그걸 내부 DB테이블에서 매핑해서 실제 파일을 찾는 방식이다.
DB에는 이런 정보가 들어간다.
+----+---------------+-----------------------------+-----------------+-------+----------+
| id | original_name | stored_name | mime_type | size | owner_id |
+----+---------------+-----------------------------+-----------------+-------+----------+
| 2 | report.pdf | 6f8a1c2e-91d3-4b7f.pdf | application/pdf | 12345 | 10 |
+----+---------------+-----------------------------+-----------------+-------+----------+
여기서 originalName은 사용자가 업로드한 원래 파일명이다.
report.pdf
storedName은 서버에 실제 저장된 파일명이다.
6f8a1c2e-91d3-4b7f.pdf
다운로드할 때는 이렇게 된다.
사용자 요청
GET /download?fileId=2
서버 처리
1. fileId=2 받음
2. DB에서 2번 파일 조회
3. 권한 확인
4. uploads/6f8a1c2e-91d3-4b7f.pdf 파일 읽음
5. 사용자에게는 report.pdf 이름으로 다운로드시킴
Express의 multer 기능: 파일명 다르게 저장
const express = require("express");
const multer = require("multer");
const mysql = require("mysql2/promise");
const path = require("path");
const app = express();
const db = mysql.createPool({
host: "localhost",
user: "root",
password: "password",
database: "testdb"
});
// 업로드 저장 설정
const upload = multer({
dest: "uploads/",
limits: {
fileSize: 10 * 1024 * 1024 // 10MB
}
});
// 임시 로그인 사용자라고 가정
app.use((req, res, next) => {
req.user = { id: 10 };
next();
});
// 파일 업로드
app.post("/upload", upload.single("file"), async (req, res) => {
try {
const file = req.file;
if (!file) {
return res.status(400).send("파일이 없습니다.");
}
const [result] = await db.query(
`
INSERT INTO files
(original_name, stored_name, storage_path, mime_type, size, owner_id)
VALUES (?, ?, ?, ?, ?, ?)
`,
[
file.originalname, // 사용자가 올린 원래 파일명
file.filename, // multer가 만든 랜덤 파일명
file.destination, // uploads/
file.mimetype, // application/pdf 등
file.size, // 파일 크기
req.user.id // 업로드한 사용자
]
);
res.json({
message: "업로드 성공",
fileId: result.insertId
});
} catch (err) {
console.error(err);
res.status(500).send("서버 오류");
}
});
// 파일 다운로드
app.get("/download/:id", async (req, res) => {
try {
const fileId = req.params.id;
const [rows] = await db.query(
"SELECT * FROM files WHERE id = ?",
[fileId]
);
if (rows.length === 0) {
return res.status(404).send("파일 없음");
}
const file = rows[0];
if (file.owner_id !== req.user.id) {
return res.status(403).send("권한 없음");
}
const uploadDir = path.resolve(__dirname, file.storage_path);
const filePath = path.resolve(uploadDir, file.stored_name);
if (!filePath.startsWith(uploadDir + path.sep)) {
return res.status(400).send("잘못된 파일 경로");
}
res.download(filePath, file.original_name);
} catch (err) {
console.error(err);
res.status(500).send("서버 오류");
}
});
app.listen(3000, () => {
console.log("server running on http://localhost:3000");
});
multer를 사용하면 사용자가 올린 파일명 / multer가 만든 랜덤 파일명을 따로 저장할 수 있다.
'보안 > SK 쉴더스 루키즈' 카테고리의 다른 글
| [SK 쉴더스 루키즈] 사용자 인증 취약점 실습문제(쇼핑몰) (0) | 2026.07.03 |
|---|---|
| [SK 쉴더스 루키즈] 파일 업로드 취약점 실습 문제(1~6번) (0) | 2026.07.02 |
| [SK 쉴더스 루키즈] SSRF 실습문제 1~4번 (0) | 2026.07.01 |
| [SK 쉴더스 루키즈] CSRF 실습문제 3번 (0) | 2026.06.30 |
| [SK 쉴더스 루키즈] CSRF 실습문제 2번 (0) | 2026.06.29 |