파일 · 권한 · 사용자 관리 17 / 50 · Part 2. Linux 파일 종류와 속성
실습 환경: Rocky Linux 9.8 · Ubuntu 24.04.5 (Docker 격리 컨테이너, 테스트 계정analyst)
기초편 연계: 「리눅스 시스템 기초」 13. Linux 파일 종류
Windows에서는 확장자가 파일 형식을 결정한다. .exe를 .txt로 바꾸면 더블클릭해도 실행되지 않는다. Linux는 다르다. 확장자는 사람과 일부 응용 프로그램을 위한 힌트일 뿐이고, 커널과 대부분의 도구는 파일 내용 으로 형식을 판단한다.
이 차이는 웹 업로드 공격의 근본 원리다. 이미지 확장자를 가진 PHP 파일이 서버에서 실행되는 웹셸 사고가 여기서 시작된다. 이번 글에서는 확장자와 내용이 다른 파일을 직접 만들어 Linux가 확장자를 신뢰하지 않는다는 것을 확인한다.
| 확장자를 보지 않는 것 | 확장자를 보는 것 |
|---|---|
커널의 실행 (execve) — shebang/ELF 매직으로 판단 | 데스크톱 파일 관리자 (아이콘·연결 프로그램) |
cat, cp, chmod 등 대부분의 명령 | 일부 응용 프로그램 (확장자로 처리 방식 선택) |
file 명령 (내용 기반) | 웹 서버의 핸들러 매핑 (.php → PHP 처리기) |
특히 웹 서버 는 확장자로 처리기를 고르는 경우가 많다. 그래서 "커널은 내용을 보고, 웹 서버는 확장자를 본다"는 인식 차이 가 공격 표면이 된다.
대부분의 파일 형식은 시작 부분에 고유한 바이트 시그니처(Magic Number)를 가진다. file 명령은 이 시그니처를 데이터베이스(/usr/share/misc/magic)와 대조해 형식을 판정한다. (자세한 시그니처는 18편)

커널이 파일을 실행할 때(execve)는 확장자를 전혀 보지 않는다. 파일 앞부분을 읽어서 다음을 판단한다.
#!로 시작하면 → 뒤에 지정된 인터프리터로 실행 (스크립트)\x7fELF로 시작하면 → ELF 바이너리로 실행Exec format error웹 서버는 반대로 동작한다. Apache의 AddHandler나 .php 매핑은 파일 이름의 확장자 를 보고 PHP 처리기로 넘길지 결정한다. 그래서 업로드된 avatar.jpg가 어떤 이유로든 PHP로 해석되는 경로에 저장되면, 그 안의 PHP 코드가 실행된다.
실습 파일
avatar.jpg에는 실제 공격 코드 대신echo "upload test"만 담긴 무해한 PHP 예시를 넣었다. 목적은 형식 판정 동작 확인 이다. 격리된 테스트 계정에서만 진행한다.
mkdir ~/lab17 && cd ~/lab17
# 1) 확장자와 내용이 다른 파일들 만들기
printf '<?php echo "upload test"; ?>\n' > avatar.jpg # jpg 이름, PHP 내용
base64 -d <<< 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGP4z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC' > real.png # 진짜 PNG
cp real.png report.txt # txt 이름, PNG 내용
cp avatar.jpg shell.php.jpg # 이중 확장자
cp /bin/true invoice.pdf # pdf 이름, ELF 내용
# 2) 내용으로 형식 판정
file avatar.jpg real.png report.txt shell.php.jpg invoice.pdf
# 3) MIME 타입
file --mime-type avatar.jpg real.png invoice.pdf
# 4) 위험 패턴 내용 검색 (이름 무시)
grep -lE '<\?php|system\(|eval\(' * 2>/dev/null

텍스트 원본(실제 출력):
[analyst@rocky9-lab ~]$ mkdir ~/lab17 && cd ~/lab17
[analyst@rocky9-lab lab17]$ printf '<?php echo "upload test"; ?>\n' > avatar.jpg
[analyst@rocky9-lab lab17]$ base64 -d <<< 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGP4z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC' > real.png
[analyst@rocky9-lab lab17]$ cp real.png report.txt
[analyst@rocky9-lab lab17]$ cp avatar.jpg shell.php.jpg
[analyst@rocky9-lab lab17]$ cp /bin/true invoice.pdf
[analyst@rocky9-lab lab17]$ file avatar.jpg real.png report.txt shell.php.jpg invoice.pdf
avatar.jpg: PHP script, ASCII text
real.png: PNG image data, 1 x 1, 8-bit/color RGB, non-interlaced
report.txt: PNG image data, 1 x 1, 8-bit/color RGB, non-interlaced
shell.php.jpg: PHP script, ASCII text
invoice.pdf: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=bde0209a3faec51dc64dbdbb9f2b5cd733ccae6b, for GNU/Linux 3.2.0, stripped
[analyst@rocky9-lab lab17]$ file --mime-type avatar.jpg real.png invoice.pdf
avatar.jpg: text/x-php
real.png: image/png
invoice.pdf: application/x-pie-executable
[analyst@rocky9-lab lab17]$ grep -lE '<\?php|system\(|eval\(' * 2>/dev/null
avatar.jpg
shell.php.jpg
| 출력 | 해석 |
|---|---|
avatar.jpg: PHP script | .jpg 이름이지만 내용은 PHP. file은 이름을 무시한다 |
real.png: PNG image data, 1 x 1 | 진짜 PNG (Magic Number 89 50 4E 47) |
report.txt: PNG image data | .txt 이름이지만 내용은 PNG |
shell.php.jpg: PHP script | 이중 확장자 우회 시도. 내용은 PHP |
invoice.pdf: ELF 64-bit LSB pie executable | .pdf 이름이지만 실제로는 실행 파일 |
avatar.jpg: text/x-php | MIME 판정도 내용 기준 |
invoice.pdf: application/x-pie-executable | 실행 파일 MIME |
grep -l → avatar.jpg, shell.php.jpg | 이름과 무관하게 PHP 코드가 든 파일을 찾아냄 |
핵심은 grep -lE다. 업로드 디렉터리에서 위험 파일을 찾을 때 *.php로만 검색하면 avatar.jpg로 위장한 웹셸을 놓친다. 이름이 아니라 내용으로 검색 해야 한다.
| 공격 | 원리 | 방어 |
|---|---|---|
| 이미지로 위장한 웹셸 | .jpg 확장자로 업로드 필터 통과 후 PHP로 해석되는 경로에 저장 | ① 내용 검사(file/MIME) ② 업로드 경로에서 스크립트 실행 차단 ③ 저장 시 이름·확장자 재생성 |
| 이중 확장자 | shell.php.jpg — 서버 설정에 따라 .php로 해석 | 마지막 확장자만 신뢰하지 않음, 허용 목록(allowlist) 방식 |
| 매직 바이트 삽입 | 파일 앞에 GIF89a를 붙여 이미지처럼 보이게 한 뒤 PHP 코드 | 확장자·매직만으로 판단하지 않고 실행 경로 자체를 차단 |
| 위장한 실행 파일 | invoice.pdf가 ELF | 다운로드·첨부 파일은 내용 기준 검사 |
핵심 방어 원칙: 입력 검증(내용 확인) 과 실행 환경 차단(업로드 경로에서 코드 실행 불가) 을 함께 적용한다. 어느 하나만으로는 부족하다.
[Detection] WAF/웹로그: /uploads/avatar.jpg 에 대한 POST 후 GET 요청 반복
↓
[Hunt] find /var/www/html/uploads -type f -exec file {} + | grep -iE 'php|executable|script'
grep -rlE '<\?php|eval\(|base64_decode\(|system\(' /var/www/html/uploads
↓
[Evidence] 해당 파일 stat(생성 시각·소유자=웹 계정) + sha256sum
↓
[Timeline] 웹 액세스 로그에서 그 파일의 최초 업로드(POST)·실행(GET) 요청 IP
↓
[Response] 파일 격리, 업로드 경로 실행 차단 확인, 취약 업로드 기능 패치
| 관점 | 내용 |
|---|---|
| Hunt | 업로드 디렉터리는 확장자가 아니라 file·내용 패턴으로 점검 |
| IOC | 웹셸은 이름이 매번 다르므로 해시·코드 패턴 이 이름보다 유효한 지표 |
| 오탐 주의 | 이미지 메타데이터(EXIF)에 우연히 매칭되는 문자열이 있을 수 있다. file 판정과 실제 코드 블록을 함께 확인 |
| 실수 | 결과 | 예방 |
|---|---|---|
| 업로드를 확장자만으로 검증 | 위장 파일 통과 | 내용(MIME/매직) 검사 병행 |
*.php로만 웹셸 검색 | 위장 파일 누락 | file + 내용 grep |
| 업로드 경로에 실행 권한 허용 | 웹셸 실행 | 업로드 디렉터리 스크립트 실행 차단 |
| MIME를 클라이언트가 보낸 값으로 신뢰 | 위조 가능 | 서버에서 file로 재확인 |
| 확장자만 바꿔 "안전"하게 만들려 함 | 내용은 그대로 | 형식은 내용이 결정 |
[ ] 확장자와 내용이 다른 파일을 여러 개 만들었다
[ ] file 이 이름을 무시하고 내용으로 판정하는 것을 확인했다
[ ] MIME 타입도 내용 기준임을 확인했다
[ ] grep 으로 이름과 무관하게 PHP 코드를 찾아냈다
[ ] 이중 확장자 우회 원리를 이해했다
[ ] 업로드 방어의 두 축(내용 검사 + 실행 차단)을 설명할 수 있다
file과 MIME 판정은 모두 내용 기준이다.*.php가 아니라 file·내용 패턴으로 한다.다음 글 「18. file 명령어와 Magic Number」 에서는 file이 어떻게 형식을 알아내는지, ELF·PNG·gzip·ZIP·PE의 실제 매직 바이트를 xxd로 직접 확인하고, file이 틀릴 수 있는 경우까지 살펴본다.