OAuth 2.0 취약점은 치명적 등급의 버그 바운티 보상 순위에서 꾸준히 상위권을 차지합니다. OAuth 구성 오류 하나로 발생한 계정 탈취가 조직 전체의 침해로 번질 수 있으며, 이러한 버그는 수십억 명이 사용하는 애플리케이션에서도 발견됩니다.
대상이 어떤 흐름을 사용하는지 알아야 테스트할 공격을 정할 수 있습니다. 인가 코드 흐름 (PKCE 적용)은 가장 안전하고 널리 쓰입니다. 현재는 폐기된 암시적 흐름 에는 많은 취약점이 있습니다. 클라이언트 자격 증명 (머신 간 통신)은 고유한 공격 표면을 지닙니다. 운영 환경에서 절대 사용하지 말아야 할 방식은 리소스 소유자 암호 자격 증명.
인가 서버가 redirect_uri를 충분히 엄격하게 검증하지 않으면 공격자는 인가 코드를 자신이 통제하는 서버로 보낼 수 있습니다.
# 정상 요청
https://auth.example.com/oauth/authorize?
client_id=app123&
redirect_uri=https://app.com/callback&
response_type=code&state=xyz
# 공격 시도
# 1. 경로 순회
redirect_uri=https://app.com/callback/../../../evil.com/steal
# 2. 추가 경로
redirect_uri=https://app.com.evil.com/callback
# 3. 프래그먼트 우회
redirect_uri=https://app.com/callback%23@evil.com
# 4. 쿼리 문자열 우회
redirect_uri=https://app.com/callback?next=https://evil.com
OAuth 흐름에서 CSRF 공격을 막는 것은 state 매개변수입니다. 이 값이 없거나 예측 가능하면 공격자는 피해자가 자신의 계정을 공격자 통제 신원에 강제로 연결하도록 만들 수 있으며, 이는 계정 탈취로 이어집니다.
# 다음 state 속성을 확인합니다.
# 1. 인가 요청에 존재하는가
# 2. 콜백에서 검증되는가
# 3. 암호학적으로 무작위인가(순차적이거나 예측 가능하지 않은가)
# 4. 일회용인가(재사용한 state가 거부되는가)
# 테스트: 같은 state 매개변수 재사용
curl -s "https://app.com/oauth/callback?code=VALID_CODE&state=PREVIOUSLY_USED_STATE"
# 예상 결과: 오류(state가 이미 사용됨)
# 인가 코드는 일회용이어야 합니다
# 첫 교환 뒤 코드를 다시 전송해 테스트합니다.
# 첫 사용(정상)
POST /oauth/token
code=AUTH_CODE_123&grant_type=authorization_code&...
# 두 번째 사용(실패해야 함)
POST /oauth/token
code=AUTH_CODE_123&grant_type=authorization_code&...
# 예상 결과: {"error": "invalid_grant"}
PKCE는 인가 코드 가로채기 공격을 방지합니다. PKCE를 지원하는 서버가 이를 강제하지 않으면 공개 클라이언트가 취약해집니다.
# Start flow WITH PKCE (legitimate)
code_verifier = generate_random_string(64)
code_challenge = base64url(sha256(code_verifier))
GET /authorize?
code_challenge=abc123&
code_challenge_method=S256&...
# Attack: exchange code WITHOUT providing code_verifier
POST /token
grant_type=authorization_code&
code=INTERCEPTED_CODE&
redirect_uri=https://legitimate-app.com/callback
# Missing: code_verifier
# If this succeeds -> PKCE not enforced -> 치명적 취약점
# S256에서 보안성이 낮은 plain으로 다운그레이드를 시도합니다
GET /authorize?
code_challenge=ACTUAL_CODE_VERIFIER&
code_challenge_method=plain&...
# 허용된다면 서버가 S256 대신 plain을 사용할 수 있습니다
# 그런 다음 code_verifier = 원래 평문으로 교환합니다
POST /token
code_verifier=ORIGINAL_PLAIN_TEXT&...
암시적 흐름은 폐기됐지만 여전히 발견됩니다. 이 흐름에서 URL 프래그먼트(#access_token=...)의 액세스 토큰은 Referer 헤더로 전송되지 않지만 쿼리 매개변수(?access_token=...)의 토큰은 전송됩니다. 토큰이 URL에 어떤 방식으로 나타나는지 반드시 확인하십시오.
# 최소 범위 요청
GET /authorize?scope=read:email&...
# 액세스 토큰을 받은 뒤 권한 없는 API 호출 테스트
GET /api/user/delete
Authorization: Bearer ACCESS_TOKEN_WITH_READ_SCOPE
# 200이 아니라 403을 반환해야 합니다
# If access tokens are JWTs, test:
# 1. Algorithm confusion (RS256 -> HS256)
header = {"alg": "HS256", "typ": "JWT"}
# 공개 키를 HMAC 비밀 키로 사용해 서명
# 2. None 알고리즘
header = {"alg": "none", "typ": "JWT"}
# 일부 라이브러리는 서명 없는 토큰을 허용함
# 3. Kid 인젝션('kid'를 사용하는 경우)
header = {"alg": "HS256", "kid": "' OR 1=1--"}
# kid 매개변수의 SQL 인젝션
# 도구:
# jwt_tool: python3 jwt_tool.py TOKEN -T (변조)
# jwt-cracker: 취약한 비밀 키 검사
# Test if old refresh tokens are invalidated after rotation
POST /token
grant_type=refresh_token&refresh_token=OLD_REFRESH_TOKEN
# If this returns a new token -> 이전 토큰이 무효화되지 않음
# 이전 리프레시 토큰을 훔친 공격자가 계속 새 액세스 토큰을 받을 수 있음
피해자가 이메일과 암호로 가입했고(이메일: victim@gmail.com), 공격자가 소유권을 Google에서 확인하지 않은 채 같은 이메일의 Google OAuth 계정을 연결해 피해자 계정을 탈취할 수 있다면 치명적 계정 탈취입니다. 여러 인증 제공자 연결을 허용하는 플랫폼에서 특히 흔합니다.
# 공격 시나리오:
# 1. 피해자가 가입하기 전에 공격자가 victim@example.com으로 계정을 생성
# 2. 공격자가 자신의 OAuth 제공자를 이 이메일에 연결
# 3. 피해자가 나중에 victim@example.com으로 Google OAuth 가입
# 4. 피해자가 공격자가 미리 만든 계정에 접근하거나 그 반대 상황 발생
# 테스트 방법:
# 1. 피해자 계정이 존재하기 전에 피해자 이메일로 가입
# 2. 다른 이메일의 OAuth 연결
# 3. 피해자의 향후 계정에 접근 시도
# Some implementations allow specifying access_token in request
# or don't properly bind tokens to sessions
# Test: force a known token value
GET /oauth/callback?access_token=KNOWN_VALUE
# If the app accepts and uses this token -> 토큰 고정
# 애플리케이션이 sub가 아니라 이메일에 계정을 연결하는 경우:
# 악성 OAuth 제공자의 email 클레임에 피해자 이메일을 넣어 등록
# 일부 제공자는 임의의 이메일 클레임 설정을 허용함
# 사용자 지정 OAuth 제공자를 만들어 테스트:
{
"sub": "attacker-unique-id",
"email": "victim@example.com",
"email_verified": true
}
# Confidential clients must authenticate with client_secret
# Test if client_secret is optional:
POST /token
grant_type=authorization_code&
code=CODE&
client_id=CONFIDENTIAL_CLIENT_ID&
# Missing: client_secret
# If this works -> 클라이언트 인증이 강제되지 않음
# 입력이 제한된 기기용 Device Flow는 피싱에 악용될 수 있음
POST /device/code
client_id=LEGITIMATE_CLIENT
# 피해자가 verification_uri에 입력할 user_code 반환
# 공격자는 이를 사회공학에 사용함:
# "accounts.target.com/activate에서 코드 XXXX-XXXX를 입력하세요"
| 버그 유형 | 플랫폼 | 영향 | 보상금 |
|---|---|---|---|
| redirect_uri 우회 | 계정 탈취 | 25,000달러 | |
| state 매개변수 누락 | Airbnb | CSRF → 계정 탈취 | 8,000달러 |
| 코드 재사용 | Microsoft | 재전송 공격 | 15,000달러 |
| JWT none 알고리즘 | Auth0 | 인증 우회 | 10,000달러 |
| 이메일 혼동 계정 탈취 | Slack | 계정 탈취 | 20,000달러 |
| PKCE 미강제 | 다수 | 코드 가로채기 | 5,000달러 이상 |
KENSAI의 AI 기반 스캐너는 OAuth 구현에서 인가 코드 가로채기, PKCE 우회, 토큰 취약점과 계정 탈취 시나리오를 자동으로 테스트합니다.
무료 스캔 시작 →보안은 선택이 아닙니다.
🗡️ KENSAI 팀