반응형
CVE-2021-3281
1. 환경 구성
- Window
- python 3.6.8 버전
- Django 3.1.5 설치
- pip install django==3.1.5
- 가상환경 구성
- pyenv update
- pyenv install -l
- pyenv install 3.6.8
- pip install virtualenv
- virtualenv --python=C:/{python.exe path newenv
- cd newenv/Scripts > activate
2. PoC
- tar 파일을 언아카이빙(압축해제)할 때
from django.utils import archive
를 통해 모듈을 로드한 후archive.extract('압축해제할 파일', '압축해제할 경로')
의 형태로 명령을 실행한다. - 공격자는 '압축해제할 파일'의 test.tar 파일 내부에 다른 경로로 이동하도록 하는 파일 명을 삽입(d:game.exeee)하여 다른 경로에 압축이 해제되도록 한다.
test.tar 파일 구조
- PoC와 동일하게 발생되는지 확인
from django.utils import archive
archive.extract('test.tar', '.')
- 실행전
- 실행후
3. 원인 분석
취약성이 발생하는 위치인 django/utils/archive.py 파일의 TarArchive클래스 코드라인 중 os.path모듈을 사용하여 filename을 정의하고 있다.
class TarArchive(BaseArchive):
def __init__(self, file):
self._archive = tarfile.open(file)
def list(self, *args, **kwargs):
self._archive.list(*args, **kwargs)
def extract(self, to_path):
members = self._archive.getmembers()
leading = self.has_leading_dir(x.name for x in members)
for member in members:
name = member.name
if leading:
name = self.split_leading_dir(name)[1]
filename = os.path.join(to_path, name) # 취약성 발생 위치
if member.isdir():
if filename:
os.makedirs(filename, exist_ok=True)
...
join 함수는 경로명 조작을 처리하고 있는 함수이며 인수에 전달된 2개의 문자열을 조합하여 1개의 경로로 정의할 수 있다.
import os
os.path.join("/A/B/C", "file.py")
# /A/B/C/file.py
하지만 결합하려는 경로명에 디렉터리 구분 문자("") 등이 포함되어 있는 경우 다음과 같이 동작한다.
import os
os.path.join('dirA', 'dirB', '\dirC') # \dirC
os.path.join('dirA', '\dirB', '\dirC') # \dirB\dirC
os.path.join('\dirA', 'dirB', 'dirC') # \dirA\dirB\dirC
join() 의 인자에 디렉터리 구분 문자가 있으면, 그것을 root로 보는 성질이 있다.
Django에서는 이 함수를 tar 파일 내 여러 파일들을 상위 경로와 연결하여 압축해제하기 위한 로직에 사용하고 있다.
import os
os.path.join('ccc', 'D:\game.exeee') # D:\game.exeee
따라서 압축해제의 결과가 D드라이브 최상단에 위치하게 된다.
이 취약성은 파이썬 TarFile 모듈의 extractall 함수에도 동일하게 발생한다.
https://docs.python.org/3/library/tarfile.html#tarfile.TarFile.extractall
4. 대응 방안
Django에서는 취약성의 해결을 위해 파일명 인자를 검증하는 함수를 추가하였다.
def target_filename(self, to_path, name):
target_path = os.path.abspath(to_path)
filename = os.path.abspath(os.path.join(target_path, name))
if not filename.startswith(target_path):
raise SuspiciousOperation("Archive contains invalid path: '%s'" % name)
return filename
...
filename = self.target_filename(to_path, name)
5. 참조
반응형