많은 현대 웹 서비스에는 글 만으로 전달하기 힘든 정보를 전달할 수 있도록 이미지나 문서 등의 파일 업로드 기능이 존재한다. 예를 들어, 이용자 간에 사진을 공유할 수 있는 이미지 공유 서비스들은 이미지 파일을 업로드할 수 있는 기능과 업로드 된 이미지를 다를 이용자들이 다운로드 받을 수 있도록 하는 기능을 갖추고 있다.
이러한 파일 공유 서비스를 개발할 때, 이용자가 업로드한 파일을 데이터베이스에 저장하는 것보다는 서버의 파일 시스템에 저장하는 것이 개발하기 쉽고, 관리 효율도 높다. 그러나 반대로, 임의 파일이 다운로드 되는 취약점이나, 악성 웹 셸 파일을 업로드하여 임의 코드를 실행할 수 있는 취약점을 발생시키기도 한다.
파일 업로드와 관련해서 발생하는 취약점은 파일 업로드 취약점 (File Upload Vulnerability)라고 하며, 반대로 내려 받을 때 발생하는 취약점은 파일 다운로드 취약점 (File Download Vulnerability)라고 한다.
File Upload Vulnerability 공격자의 파일을 웹 서비스의 파일 시스템에 업로드하는 과정에서 발생하는 보안 취약점이다. 파일 시스템 상 임의 경로에 원하는 파일을 업로드하거나 악성 확장자를 갖는 파일을 업로드할 수 있을 때 발생한다. 원하는 시스템 커맨드를 실행하는 원격 코드 실행 취약점을 유발할 수 있다.
File Download Vulnerability 웹 서비스의 파일 시스템에 존재하는 파일을 다운로드하는 과정에서 발생하는 보안 취약점이다. 공격자는 웹 서비스의 파일 시스템에 존재하는 임의 파일을 다운로드 받을 수 있다. 설정 파일, 패스워드 파일, 데이터베이스 백업본 등을 다운로드 하여 민감한 정보를 탈취할 수 있고, 2차 공격을 수행할 수 있다.
**파일 업로드 취약점(File Upload Vulnerability)**은 웹 서비스를 통해 이용자의 파일을 서버의 파일 시스템에 업로드하는 과정에서 발생하는 보안 취약점이며, 이용자가 업로드될 파일의 이름을 임의로 정할 수 있을 때 발생한다. 파일 이름에 이용자가 입력한 문자열을 그대로 사용하거나, 이용자의 이메일, 닉네임 등을 포함시키는 등의 소스 코드 패턴이 이러한 취약점을 발생시킬 수 있다.
파일 업로드 취약점은 크게 Path Traversal과 악성 파일 업로드로 분류된다. 파일 업로드를 허용하는 대개의 서비스는 보안을 위해 특정 디렉터리에만 업로드를 허용한다. 만약 이러한 제한이 없다면, 악의적인 이용자가 웹 서버의 소스 코드나 서버에 있는 중요 시스템 파일을 덮어 쓸 위험이 있다. Path Traversal 취약점은 업로드에 존재하는 이러한 제약을 우회하여, 임의 디렉터리에 파일을 업로드할 수 있는 취약점을 말한다.
아래는 파일 업로드 기능에 Path Traversal 취약점이 있는 코드이다. ./fileUpload는 POST 요청을 받으면, 클라이언트가 전송한 파일을 ./uploads에 저장한다. 그런데 이용자가 입력한 파일 이름 f.filename을 그대로 사용하기 때문에 Path Traversal에 취약하다. 예를 들어, 공격자가 ../와 같은 메타문자를 사용하면 uploads를 벗어나 상위 디렉터리에도 파일을 업로드할 수 있다.
| 문자 | 설명 | Example |
|---|---|---|
. |
현재 디렉터리를 나타낸다. | /var/www/./html → /var/www/html |
.. |
상위 디렉터리를 나타낸다. | /var/www/../html → /var/html |
from flask import Flask, request
app = Flask(__name__)
@app.route('/fileUpload', methods = ['GET', 'POST'])
def upload_file():
if request.method == 'POST':
f = request.files['file']
f.save("./uploads/" + f.filename)
return 'Upload Success'
else:
return """
<form action="/fileUpload" method="POST" enctype="multipart/form-data">
<input type="file" name="file" />
<input type="submit"/>
</form>
"""
if __name__ == '__main__':
app.run()
파일을 정상적으로 업로드하면, 아래와 같은 HTTP 요청이 전송된다. 아래 요청의 filename 필드의 값이 웹 서비스 코드 내 f.filename 변수의 값이 된다.
POST /fileUpload HTTP/1.1
Host: storage.dreamhack.io
Origin: <https://storage.dreamhack.io>
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary20y3eoLVSNf9Ns5i
------WebKitFormBoundary20y3eoLVSNf9Ns5i
Content-Disposition: form-data; name="file"; filename="test.txt"
Content-Type: text/plain
upload test !
------WebKitFormBoundary20y3eoLVSNf9Ns5i--
이처럼 요청을 보내면, 아래와 같이 uploads 디렉터리에 test.txt가 생성된다.
$ ls -lR
-rw-r--r-- 1 dreamhack staff 461 1 30 21:52 app.py
drwxr-xr-x 3 dreamhack staff 96 1 30 21:31 uploads
./uploads:
total 8
-rw-r--r-- 1 dreamhack staff 13 1 30 21:31 test.txt