안녕하세요. 스틸리언 선제대응팀 연구원 윤석찬입니다. 이전에는 R&D팀으로 인사드렸었는데, 부서 개편 후 처음으로 선제대응팀으로 인사드리게 되었습니다. 스틸리언 선제대응팀에서는 Offensive Security를 연구하며 유의미한 가치를 도출하기 위해 노력하고 있습니다.
__
1. Introduction
2023년 7월 3일, 제 첫 CVE가 공개되었습니다.
- https://www.djangoproject.com/weblog/2023/jul/03/security-releases/
- https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-36053
위 링크에서 제 취약점이 어떤 내용인지 간략히 확인하실 수 있습니다. 이 글에서는 제가 이 취약점을 어떻게 찾게 되었는지 생각의 흐름을 공유하고, (제가 대단한 실력자도 아니고 파급도 높은 취약점을 찾은 것도 아니지만 😅) Offensive Security Researcher로서 어떤 마음가짐을 갖고 코드를 보면 좋을지 저만의 생각을 공유하고자 합니다. 이번 글은 코드 분석 위주의 글보다는 그냥 말로 취약점의 root cause와 제 생각들을 써보았습니다.
Django security releases issued: 4.2.3, 4.1.10, and 3.2.20 (reported by ME!)
__
2. Core Engine 분석
Offensive Research를 하시는 분들이라면 다들 공감하시겠지만 어떤 소프트웨어를 분석하기 위해선 코어 엔진(주요 부분)을 먼저 분석할 필요가 있습니다. 제가 생각하는 Django의 주요 기능은 아래와 같았습니다.
- Django의 Core 서비스를 로드하는 기능
- HTTP Request/Response를 파싱해서 Object에 매핑하는 부분
- HTTP 상에서 session을 유지시키는 부분
- 파일 송신을 처리하는 부분
- Django ORM을 통해 SQL 쿼리를 생성하는 부분
그래서 Django의 전반적인 구조를 분석하고자 위 항목들을 중점적으로 분석하기 시작했습니다.
__
3. 1-day 버그케이스 분석
주요 기능을 분석한 뒤에는 전반적인 프로그램의 구조가 어느 정도 눈에 익기 시작했습니다. 이후 제가 이해한 프로그램 구조를 기반으로 그동안 공개된 1-day vulnerabilities를 찾아보았습니다. 작년에는 특히 ORM Injection (ORM 기능에서의 SQL Injection 취약점)이 3건이나 발생된 만큼 Django에서 SQL Injection을 찾아보고자, 지금까지 Django에 report된 모든 SQL Injection 1-day 버그케이스를 분석했습니다.
아래 링크에서 Django ORM Injection에 대해 분석한 내용을 확인하실 수 있습니다.
SQL Injection 류의 버그를 찾고자 노력했지만 더 이상의 SQL Injection 취약점을 발견할 수 없었습니다. 😓 이후 학업과 회사 업무를 병행하면서 Django 보안 취약점 연구에 크게 시간을 쏟지 못하다가, 군입대로 인한 휴직 전 얻은 여유시간에 Django를 다시 분석하기 시작했고 우연히 보안 취약점을 찾게 되었습니다.
제가 이번에 제보한 취약점은 아래 1-day vulnerability (CVE-2023-23969)와 다소 비슷합니다.
요약하면, 이 버그 케이스는 복잡한 정규표현식이 매우 큰 문자열을 처리해야 하는 상황에서 DoS 공격에 취약할 수 있다는 점을 지적하고 있습니다. 각 언어마다 정규표현식을 구현하는 방식은 더 복잡하겠지만, 기본적으로 정규표현식의 검색은 처음부터 끝까지 모든 문자열을 검색해야 한다는 점에서 선형 검색(Linear Search)과 비슷한 양상을 띤다고 볼 수 있습니다. 따라서 정규표현식에 거대한 양의 텍스트가 regex 검사 인자로 입력된다면 상당한 컴퓨팅 자원이 소모될 수 밖에 없으며, 정규표현식의 이러한 점을 노리는 공격이 바로 ReDoS attack입니다.
위 CVE-2023-23969 (Potential denial-of-service via Accept-Language headers) 취약점은 Django에 내부적으로 구현된 get_language_from_request()
함수에서 발생합니다. 이 함수는 이용자의 HTTP Request 패킷으로부터 Accept-Language
헤더를 가져와 이용자의 언어 환경 값을 가져오는데, 이때 Accept-Language
헤더를 파싱하기 전 길이 제한이 없어서 ReDoS에 취약했습니다.
위 취약점에 대한 자세한 설명을 보시려면 제 개인블로그 글을 참고해주시길 바랍니다. 애드블럭을 끄고 방문해주시길 바랍니다. (농담임 😉)
__
4. 시나리오 기획
작년 SQL Injection류의 취약점을 찾고자 소스코드를 분석할 때는 시나리오를 생각하지 않고, 무작정 코드만 보다가 빠르게 지쳐버린 경험이 있었습니다. 그래서 이번에는 원데이 취약점을 분석하고나서 시나리오가 생각날 때만 소스코드를 들여다보기 시작했습니다. 문득 머릿속에 떠오른 시나리오를 평소 사용하는 노트 앱에 기록해두고 시간적으로 여유가 날 때마다 기록해 둔 시나리오와, 이와 비슷한 시나리오들이 가능한지 분석했고, 이 방식으로 분석의 능률이 크게 향상되었던 것 같습니다. (이런 방법론은 해커마다 개인차가 있는 부분이기 때문에 제 방식은 그냥 참고 정도만 해주시길 바랍니다.)
제가 이번에 발견한 CVE-2023-36053 취약점도 버그케이스를 분석하고 시나리오를 생각하는 과정에서 발견한 취약점입니다.
CVE-2023-23969를 분석하면서 위와 같은 ReDoS 취약점이 여럿 있을 것 같다는 생각이 들었습니다. 그래서 이러한 시나리오를 갖고 Django에서 사용되는 모든 정규표현식 문자열을 모두 찾아 ReDoS로부터 안전한지 분석해보았습니다. 저는 CVE-2023-23969 취약점이 어떻게 패치되었는지, 그리고 제가 지금까지 Django Security 팀에 보낸 Security Report들을 통해 취약점으로 인정받으려면 어떤 항목을 만족시켜야 하는지 생각해보았습니다.
+
,*
,{0,n}
등의 정규표현식 notation이 중복적으로 나타나 finite automata 상 반복이 많이 발생하는 경우- 위 정규표현식을 사용할 때 길이 검증이 선행되지 않은 경우
- 이용자가 송부한 값이 그대로 해당 함수에 인자로 들어갈 개연성이 높은 경우
__
5. 분석
앞서 기획한 시나리오 대로 검증하는 과정에서 2건의 취약점을 발견할 수 있었습니다. EmailValidator
를 예로 들어 이 취약점이 어떻게 발생되는지 설명하도록 하겠습니다.
EmailValidator
는 django.core.validators
에 존재하는 Validator 클래스로 사용자로부터 입력받은 문자열이 이메일 형식을 만족하는지 확인합니다.
# django/core/validators.py
@deconstructible
class EmailValidator:
# ...
domain_regex = _lazy_re_compile(
# max length for domain name labels is 63 characters per RFC 1034
r"((?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+)(?:[A-Z0-9-]{2,63}(?<!-))\Z",
re.IGNORECASE,
)
# ...
이 Validator에서는 domain_regex
라는 내부 변수로 도메인의 유효성을 검사하기 위한 정규식을 저장합니다. 이 내부 변수는 이메일의 도메인을 검사하기 위해 사용됩니다. 이 정규표현식 문자열은 {0,61}
과 +
notation을 중첩하여 사용하기 때문에, 매우 큰 크기의 문자열을 검사하게 될 여지가 있습니다.
그렇지만 만약 이 정규표현식 문자열로 특정 입력을 검사하기 전 길이 검증 로직을 두어 매우 큰 크기의 문자열이 정규표현식 검사의 인자로 들어가지 못하도록 막는다면 사실상 ReDoS를 트리거하기 어려워집니다. 제 개인 노트북(Apple Macbook Pro 13” / M2, 16GB)으로 테스트해본 결과 Email Validator
의 경우 1MB 이상의 문자열이 인자로 들어가야 정규표현식 연산 과정에서 100ms 이상의 유의미한 시간 지연이 발생했기 때문입니다.
따라서 적절한 길이 검증 로직이 존재했었다면 ReDoS 공격에서 안전하다고 볼 수 있겠지만, 이번 취약점의 타겟인 EmailValidator
와 URLValidator
에는 정규식 검사 이전 적절한 검사 코드가 부재하여 ReDoS 공격에 취약했다고 볼 수 있겠습니다. 이메일 주소와 URL 주소 모두 RFC 상으로 규격화된 형식이 있기 때문에, ReDoS 공격도 막을 겸 길이 검증 로직이 추가되면 충분히 ReDoS 공격도 막을 수 있었습니다.
__
6. 제보
대부분의 대형 오픈소스 프로젝트나 소프트웨어는 Security Policy를 갖고 있습니다. Django의 경우에는 아래 링크에서 관련 정보를 확인할 수 있었습니다.
https://docs.djangoproject.com/en/dev/internals/security/
총 2건의 취약점을 찾은 후, Django Security Team에 이메일로 Security Report를 보냈고 이후 약 2주 정도의 시간이 지나 CVE 넘버를 할당받을 수 있었습니다. 👏👏 이 과정에서 Django Security Team은 취약점 접수부터 패치, 피드백까지 아주 프로페셔널한 팀이구나 느꼈습니다.
__
7. 마무리
지금까지 제가 Django에 대해 취약점을 분석한 과정과 Security Report를 보내고 CVE 코드를 할당받기까지의 과정을 작성해보았습니다. 제게 큰 용기와 동기부여를 주셨던 예랑님과 상호님, 그리고 스틸리언 R&D팀 식구들에게 대단히 감사드립니다. 🙇♂️