> ## Content Index
> Fetch the complete content index at: https://niklas-heringer.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Pentesting Passkeys: A Practical Methodology for WebAuthn Deployments
- URL: https://niklas-heringer.com/penetration-testing/pentesting-passkeys-a-practical-methodology-for-webauthn-deployments/
- Published: 2026-08-15T12:41:44.000Z
- Updated: 2026-08-15T12:41:44.000Z
- Description: Recon, server-side Relying Party tests, and the Windows-focused OS and authenticator attacks: end-to-end, tool by tool.
- Author: Niklas Heringer
- Tags: penetration-testing, passkeys, blackhat, defcon

## Introduction

Last week (Aug of 2026) I sat in a Mandalay Bay conference room watching Michael Grafnetter, Principal Security Researcher at SpecterOps, walk an audience through a family of attacks against the "phishing-resistant" authentication technology the whole industry has spent the last two years telling us to adopt. The talk, *Pass-the-Passkey Family of Attacks*, was one of the more consequential things I saw at Black Hat USA 2026\. It wasn't a break of the WebAuthn cryptography (that still holds), but a demonstration that the *implementations* around it: Microsoft Entra ID, Windows 11's WebAuthn stack, several popular password managers, had left enough surface area for attackers to pass, relay, replay, and phish credentials that are supposed to be unphishable.

This blog post is my attempt to turn that research, together with the excellent [foundation article by Alf Løkken](https://alflokken.github.io/posts/understanding-fido2-passkeys/?ref=niklas-heringer.com), into a **methodology a pentester can actually use** (told you in the last post i'd do so hihi).

I will use these tests for myself and see if i can further improve the methodology (that'll be the case lol) and i will update this post accordingly!

> **Everything** technical here rests on Grafnetter's work; if you only read one thing after this post, make it his [whitepaper](https://specterops.io/wp-content/uploads/sites/3/2026/08/Pass-the-Passkey%5FA4%5Fv2.pdf?ref=niklas-heringer.com).

### What a passkey actually is

A passkey is a **public-key credential** for a specific website, generated and held by an "authenticator" you own: the TPM in your laptop, a YubiKey on your keychain, a password-manager vault synced across your devices. When you sign in, the website sends a random challenge; the authenticator signs it with the private half of your credential; the website verifies the signature against the public half it stored when you registered. 

> No shared secret is ever typed, transmitted, or reused. If a phishing site tries to trick you into signing in on the wrong domain, the ceremony fails cryptographically, there is no password for the user to type into the wrong box.

That is the pitch. It is largely true. It is also (as Grafnetter's research shows in painstaking detail) **not the whole story**. The signature only proves that the bytes **weren't altered in transit**. Whether the relying party should actually honor those bytes is a 25-step verification procedure, most of which the RP alone is responsible for, and any one of which a busy engineering team can quietly skip.

This indepth post walks that surface.

---

# Part 1: Passkey Fundamentals

Before you can meaningfully test a passkey implementation, you need to know exactly *what* the specification promises, *where* those promises come from, and *who* is responsible for enforcing them. Most passkey vulnerabilities are not cryptographic breaks but rather missed steps in a long verification procedure, or misplaced trust in a component that never actually made the guarantee everyone assumed it did.

This section builds that model. It's deliberately spec-anchored: every claim points to a section of the [W3C WebAuthn Level 3 specification](https://www.w3.org/TR/webauthn-3/?ref=niklas-heringer.com) or the [CTAP2 spec](https://fidoalliance.org/specs/fido-v2.1-ps-20210615/?ref=niklas-heringer.com), because those are the documents you'll cite back to a developer when referring to an implementation error.

## The four actors

The formal WebAuthn process between a user, a browser, and a website or app to create or use a secure passkey is called a **ceremony.**

A passkey ceremony always involves four parties:

- **User:** the human at the keyboard.
- **Authenticator:** the logical component that generates, stores, and uses cryptographic key material. Can be hardware ([YubiKey](https://www.yubico.com/?ref=niklas-heringer.com)), platform-integrated (Windows Hello, iCloud Keychain), or software (a password manager).
- **Client:** the browser or platform software that mediates the ceremony. Chrome, Safari, Edge, the Windows WebAuthn API, iOS's Authentication Services framework.
- **Relying Party (RP):** the web application or service the user is authenticating to. Verifies the credential and stores the public key.

Two specifications govern how they talk to each other:

- **WebAuthn** (W3C): the browser-facing JavaScript API (`navigator.credentials.create()` and `navigator.credentials.get()`) that a relying party's web page calls.
- **CTAP2** (FIDO Alliance): the wire protocol between the client and an external authenticator, over USB, NFC, or BLE.

> **FIDO2** is the umbrella term for the two together. **Passkey** is the user-facing marketing term for a WebAuthn public-key credential, originally for discoverable/multi-device credentials specifically, though the term is now used more broadly to include device-bound ones too.

## Two ceremonies: registration and authentication

Everything in WebAuthn is one of two ceremonies.

**Registration** creates a new credential:

1. The RP sends a challenge and creation options to the browser.
2. The browser passes them to an authenticator.
3. The authenticator prompts the user for verification (biometric, PIN, or touch), generates a fresh keypair, and returns the public key plus signed authenticator data.
4. The RP stores the public key against the user's account.

![](https://storage.ghost.io/c/a1/92/a19235e1-df52-43a1-9f25-1151fdb92c82/content/images/2026/08/image-1.png)

The private key **never** leaves the authenticator.

**Authentication** proves possession of an existing credential:

1. The RP sends a fresh, single-use challenge.
2. The browser assembles the challenge, the true origin of the calling page, and the request type into a `clientData` structure, then hashes it into `clientDataHash`.
3. The authenticator signs the concatenation of its own `authenticatorData` and `clientDataHash` with the private key.
4. The browser returns the signed assertion; the RP verifies the signature against the stored public key.

![](https://storage.ghost.io/c/a1/92/a19235e1-df52-43a1-9f25-1151fdb92c82/content/images/2026/08/image-2.png)

Full details in WebAuthn §7.1 (registration) and §7.2 (authentication verification).

## Where phishing resistance actually comes from

Passkeys are called "phishing-resistant" because of two design properties, not because of any inherent property of public-key cryptography:

**Origin binding.** The `origin` field in `clientDataJSON` is filled in by the *browser*, based on the address of the page that actually invoked the WebAuthn API. It is not a value the page can forge. The authenticator then binds that origin into the signed assertion. A phishing site cannot obtain an assertion that the legitimate RP will accept: the signed origin won't match, and the credential itself is scoped to the RP's `rpId`. There is no shared secret for the user to type into the wrong box.

**Challenge freshness.** Each ceremony uses a fresh, single-use challenge issued by the RP. The RP is expected to remember it, and to reject any assertion whose challenge it has already seen or never issued.

That's it. Those are the two properties. Everything else (attestation, signature counters, user verification flags) is defense-in-depth on top of them.

## Where the guarantees can break: the 25-step verification

The WebAuthn Level 3 specification defines a 25-step assertion verification procedure that the RP must perform on every incoming assertion. Several of the most important anti-replay checks are the RP's job, not the authenticator's:

- Verifying the challenge is one the RP actually issued and hasn't accepted before.
- Binding the challenge to the current session.
- Tracking the signature counter and rejecting regressions.
- Validating the `origin` and `rpId` against the expected values.
- Checking the User Verification (UV) and User Presence (UP) flags in `authenticatorData` against policy.

When an RP skips any of these, the unphishable property "quietly degrades into something an attacker can pass, relay, or replay" ([Grafnetter](https://specterops.io/wp-content/uploads/sites/3/2026/08/Pass-the-Passkey%5FA4%5Fv2.pdf?ref=niklas-heringer.com), p.12., 2026). 

> This is where most passkey vulnerabilities live, and it's the first place your pentest should look.

## The signature counter (and why it matters)

Hardware authenticators maintain a monotonically increasing counter and include its current value in every signed assertion. The RP is supposed to store the last-seen counter per credential and reject any assertion whose counter is equal to or lower than the last one. Its purpose is to detect **cloned credentials:** if two copies of a private key exist, sooner or later one of them will produce an assertion with a counter that has gone backwards.

Two important gotchas the spec allows and pentesters exploit:

- Software authenticators (most password managers) often send a counter of `0` and never increment it. This is legal per the spec but destroys the clone-detection guarantee.
- Windows Hello, on Entra ID registered devices, always sends `0` for the same reason.

The counter-of-zero behavior isn't laziness but a design surrender to a real conflict. Synced credential managers (iCloud Keychain, Google Password Manager, 1Password, Bitwarden) all face the same problem: **the same private key exists on N devices at once, by design.** If each device incremented its own local counter independently, the RP would see values bouncing chaotically and flag every login as a clone. 

> Synchronizing the counter through the cloud vault would mean a round-trip write on every sign-in, killing latency and requiring the vault to be online. Faced with that, sync providers punt and hardcode `0`.

The spec allows the punt, which puts the RP in a bind: track counters strictly and you break every synced-passkey user; ignore counters and you can never detect clones. Most RPs take the compatible-but-weaker middle path. Microsoft's May 2026 Entra ID fix is the pragmatic version: track counters strictly for authenticators that provide them (hardware keys), accept `0` unchecked from those that don't (Windows Hello, most password managers). It restores the guarantee for users who could benefit, keeps compat for those who can't, at the cost of leaving Windows Hello passkeys as a soft spot that later attacks in this series exploit.

## Authenticator attachment: platform vs roaming

The specification distinguishes two attachment types based on the authenticator's *logical relationship to the client*, not its physical form factor:

- **Platform authenticators** are built into the OS or device. The platform owns the credential lifecycle. Examples: Windows Hello for Business, iCloud Keychain, Google Password Manager, Android hardware-backed keystore.
- **Roaming authenticators** (also "cross-platform") manage their own credentials independently of any client, and are accessed over CTAP2 via USB, NFC, or BLE. Examples: YubiKey, SoloKey, Google Titan.

Both use the same cryptography. The difference is *who controls key storage and lifecycle*, which determines what evidence they can produce about themselves at registration (attestation) and what happens when the user changes devices.

## Synced vs device-bound: the trust boundary shift

WebAuthn calls this a "multi-device credential"; nobody uses that term. In practice you'll hear **synced passkey** (we just discussed them above in regards to the signature counter) vs **device-bound passkey**.

A **device-bound passkey** is generated on a single authenticator and its private key never leaves that hardware. YubiKey credentials, and Windows Hello credentials backed by a TPM, are device-bound.

A **synced passkey** has its private key copied to a cloud vault by a credential manager (iCloud Keychain, Google Password Manager, 1Password, Bitwarden) so the same credential is usable from the user's phone, tablet, and laptop.

Synced passkeys are strictly less secure than device-bound ones, and the reason is important: **the phishing-resistant credential is now bootstrapped from, and only as strong as, the (phishable) password protecting the cloud vault.** Anyone who compromises the vault gets a working copy of the key.

> The RP has no visibility into that compromise and no way to prevent the credential from being restored on a new device.

For consumer scenarios this is a fine trade-off. For a Global Administrator on a cloud tenant, it is not. Enterprise RPs can restrict registration to device-bound authenticators through policy, using attestation.

## Attestation and AAGUID: the "who made this authenticator" story

**Attestation** is an *optional* mechanism during registration: the authenticator can produce a signed statement about itself (model, certification level, hardware-backed key storage) that the RP can verify against a trusted metadata source such as the FIDO Metadata Service (MDS).

**AAGUID** (Authenticator Attestation GUID) is a 128-bit identifier embedded in every authenticator's data, identifying the model or implementation family. 

On its own it proves nothing (the authenticator supplies it and could lie) but combined with a valid attestation statement, an RP can cryptographically verify what kind of authenticator it's talking to.

Consequences for your pentest:

- Roaming hardware keys usually support strong attestation. RPs *can* enforce "only YubiKey 5 series with FIPS certification".
- Platform authenticators may or may not attest, depending on OS and hardware.
- **Synced passkeys cannot meaningfully attest:** once a credential is copied to a cloud vault, no attestation statement can honestly assert "this key lives on hardware X".

If an RP wants device-bound-hardware assurance, it must enforce attestation *and* validate the AAGUID against a policy list. Most don't.

## Same-device vs cross-device (hybrid / caBLE)

These are UX distinctions, not spec-defined ceremonies.

**Same-device authentication** happens when the authenticator is on the same device the user is signing in from: Windows Hello on the laptop, or a YubiKey plugged into it.

**Cross-device authentication** uses CTAP2 **hybrid transport** (still often called **caBLE**, Cloud-Assisted BLE). The user's phone acts as the authenticator for a sign-in on their laptop. Two channels are involved:

- A **BLE proximity check** proves the phone is physically near the laptop.
- CTAP2 messages travel through an end-to-end-encrypted tunnel relayed by the authenticator vendor's tunnel service. The tunnel service cannot read the traffic.

From the WebAuthn layer, hybrid ceremonies are identical to same-device ceremonies. From an attacker's perspective, they open a distinct attack surface: the RDP-style pass-through variants, and prompts that appear on a phone the user isn't currently looking at.

## What passkeys do *not* protect against

Passkeys eliminate credential phishing and password reuse. They do not eliminate:

- **Session cookie theft** after authentication succeeded.
- **OAuth token theft or replay**, including malicious consent grants.
- **Endpoint compromise:** malware on the client machine can, as later sections will show, coerce or hijack the ceremony itself.
- **Recovery flow abuse:** most RPs still allow a password-plus-email-OTP fallback that entirely bypasses the passkey.
- **Administrative registration abuse:** an attacker with the right role can enroll a passkey against someone else's account (the Shadow Passkey attack).

Phishing-resistant authentication is a foundation, not a finished building. The rest of this series is a tour of where the building is still exposed.

---

# Part 2: The Attack Surface Map, and Server-Side Passkey Tests

*While part 1 gave you the mental model, this part aims to give you the map, the five layers where a passkey ceremony can be attacked, aswell as the concrete server-side tests that catch the majority of real-world bugs. Part 3 will cover the client and operating-system layers.*

If Part 1 had one thesis, it was: **the WebAuthn signature only proves the bytes weren't altered. It doesn't tell the Relying Party whether the bytes represent a request it should honor.** Enforcing that "should honor" judgment is the 25-step verification procedure that the RP alone is responsible for. This is where the majority of exploitable passkey vulnerabilities live, and it's where you should spend the majority of your test time.

## The attack surface map

A single passkey login touches many components across several trust boundaries. Adapted from Grafnetter's SpecterOps whitepaper:

![](https://storage.ghost.io/c/a1/92/a19235e1-df52-43a1-9f25-1151fdb92c82/content/images/2026/08/image-3.png)

Each layer has a trust assumption it hands upward. Your methodology walks the layers top-down and, for each one, asks: *what does this layer promise, and what happens when it doesn't deliver?*

**Layer A is by far the richest hunting ground for external pentesters.** You can test it from a browser and Burp, without touching the endpoint. Layers B–C are narrower but relevant when the scope includes the web-app perimeter. Layers D–E are for red-team and endpoint-in-scope engagements and are covered in Part 3.

---

## Recon: know what you're testing before you test it

Before the first probe, capture a normal registration and a normal authentication with your browser's DevTools open or using Burp, and extract the WebAuthn options the RP is actually using. You care about:

- **Allowed algorithms:** the `pubKeyCredParams` array (COSE algorithm identifiers). ES256 (-7), RS256 (-257), EdDSA (-8) are the common ones. Weaker or exotic algorithms in this list are worth investigating.
- **Attestation policy:** the `attestation` field on registration: `none`, `indirect`, `direct`, or `enterprise`. Most consumer RPs send `none`. Enterprise flows that want device-bound assurance need `direct` and an AAGUID allowlist.
- `**authenticatorSelection**`: `authenticatorAttachment` (`platform` / `cross-platform`), `residentKey` (`required` / `preferred` / `discouraged`), `userVerification` (`required` / `preferred` / `discouraged`).
- **`allowCredentials` list;** during authentication, the credential IDs the RP is willing to accept. Enumerating this can leak which authenticators a user has registered.
- **Related Origin Requests:** the `.well-known/webauthn` file on the RP's domain, if any, listing additional origins bound to the same `rpId`.
- **Challenge format and length:** is it opaque random bytes, or a structured token? Entra ID, for example, uses signed JWTs as challenges to avoid server-side storage. That design choice creates the challenge-replay window described below.
- **Response endpoint behavior:** what happens if you resend the exact same successful assertion body a second time? Different session, different browser, different IP?

Save two full request/response pairs (one registration, one authentication) as your reference baseline. Every test below is a controlled deviation from those baselines.

---

## Layer A: Relying Party tests

Seven core tests. Each has: **the trust assumption, the failure mode, how to test, what tool, and a note on reporting.**

### Test A1: Challenge reuse (replay)

**Assumption:** The RP tracks issued challenges and rejects any assertion that carries a challenge it has already accepted (or never issued).

**Failure mode:** The RP forgets the challenge (either never stored it, or stored it in a way that survives being marked "used"). An attacker who captures a single valid assertion (from a proxy, from a browser log, from the Windows event log, see Part 3) can replay it and impersonate the user.

**How to test:**

1. Complete a normal login and capture the full request body containing `id`, `rawId`, `type`, and the `response` object (`authenticatorData`, `clientDataJSON`, `signature`, `userHandle`).
2. Wait for the session cookies to expire or clear them.
3. Replay the exact same assertion body against the assertion endpoint in a new session.
4. If you get an authenticated session back, the RP failed the check.
5. Vary: replay from a different IP, different User-Agent, different browser session. Any of these succeeding is a finding.

**Tool:** Burp Repeater for a first pass. For payload construction and mutation, the [**Passkey Injector**](https://github.com/SpecterOps/pass-the-passkey?ref=niklas-heringer.com#passkey-injector) tool (SpecterOps) lets you feed a captured `PublicKeyCredential` JSON straight into a WebView2 browser and submit it as if it were freshly generated.

**Reporting note:** Vendors sometimes triage this as "Information Disclosure" (see the Entra ID replay story (MSRC VULN-171325, partially fixed May 2026)) rather than authentication bypass. Push back with a demonstrated impersonation. CVSS-wise this is `AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H` territory, cite CWE-294 (Authentication Bypass by Capture-Replay).

### Test A2: Challenge-to-session binding

**Assumption:** The challenge the RP issued to Session X is only accepted when assertion Y is submitted from Session X. Sessions are typically identified by a pre-authentication cookie, a CSRF-style token, or a login-flow state parameter.

**Failure mode:** The RP issues challenges but doesn't bind them to the session that requested them. An attacker who obtains a valid assertion from Victim's ceremony can submit it from Attacker's own session and be signed in as Victim. This is the precondition for the SpecterOps *Passkey Circuit Breaker* attack (§3.3, p. 31): malware freezes the victim's browser mid-ceremony, exfiltrates the freshly signed assertion, and the operator submits it from their own machine before the challenge expires.

**How to test:**

1. In Browser A (attacker session), start a login flow and note the pre-auth cookies / state token.
2. In Browser B (victim session), start a separate login flow, get a fresh challenge, and complete authentication all the way to a signed assertion, but intercept and hold the assertion response.
3. Take Victim's assertion and submit it to the assertion endpoint using Attacker's session cookies and state.
4. If you get Victim's authenticated session back in Browser A, the challenge wasn't session-bound.

Compare vendor behavior: GitHub has session-bound challenges since at least January 2026 (assertion intercepted in victim's session cannot be replayed from operator's computer). Microsoft Entra ID, at the time of the SpecterOps research, did not.

**Tool:** Two browser profiles + Burp for the swap. Passkey Injector is convenient because it can hold and re-fire an assertion at a chosen endpoint.

**Reporting note:** CWE-384 (Session Fixation) or CWE-287 (Improper Authentication) depending on flavor. Impact is the same as A1 in practice; the two failures often coexist.

### Test A3: Signature counter regression

**Assumption:** The RP stores the last-accepted `signCount` per credential and rejects any assertion whose counter is *equal to or lower than* the stored value.

**Failure mode:** Two flavors:

1. The RP doesn't store counters at all, and cloned credentials go undetected.
2. The RP stores the initial counter at registration but never updates it, which looks like (1) at runtime.

**How to test:**

1. Register a hardware authenticator that increments counters (a YubiKey is ideal).
2. Perform three logins; observe the counter values embedded in `authenticatorData` (bytes 33–36, big-endian uint32) go up each time.
3. Replay an *earlier* captured assertion (lower counter) after a *later* one has been accepted.
4. If the RP accepts the replay, it isn't tracking counters. Some RPs will accept counter=0 assertions (from software authenticators) alongside a strict track for hardware. Verify **both** paths.

**Tool:** [DSInternals Passkey UI](https://github.com/SpecterOps/pass-the-passkey?ref=niklas-heringer.com#dsinternals-passkey-ui) shows counter values inline as you exercise a real authenticator. The Passkey Injector's Software Signer lets you produce assertions with attacker-chosen counter values, which is useful for probing whether the RP even reads the field.

**Reporting note:** Even if the RP claims "we don't track counters because we allow synced passkeys", that's a policy choice, but it should be an *informed* one. Combine this finding with A1: if the RP tracks neither challenges nor counters, replay is trivially indefinite.

### Test A4: Origin and RP ID validation

**Assumption:** During verification, the RP checks that `clientDataJSON.origin` is in its allowlist, and that `authenticatorData.rpIdHash` equals SHA-256 of its expected `rpId`.

**Failure mode:** The RP accepts assertions signed for the wrong origin or wrong `rpId`. This is rare in modern libraries (the FIDO reference conformance suite catches it) but does still appear in hand-rolled implementations and in custom "we needed to support N related brands" code.

**How to test:**

1. Use the Software Signer (component of the Passkey Injector) to produce an assertion where `clientDataJSON.origin` is a different origin the RP might trust (`https://staging.example.com`, `https://example.co.uk`, `http://example.com`: HTTP variant is a common oversight).
2. Submit against the production endpoint.
3. Independently, test the RP's Related Origin Requests handling: fetch `https://<rpId>/.well-known/webauthn`. If it lists origins, verify they're all under the organization's control. A stale entry pointing at an expired domain is game over.
4. Test `rpId` scope: register a credential for `login.example.com` and see whether the RP will accept it for `example.com` or vice versa (per spec, subdomain-broader `rpId` can serve narrower origins but not the reverse).

**Tool:** Passkey Injector (arbitrary origin), DSInternals Passkey UI (arbitrary `rpId`).

**Reporting note:** CWE-346 (Origin Validation Error). Even a "harmless" acceptance of the wrong scheme (http vs https) is worth reporting.

### Test A5: UV / UP flag enforcement

**Assumption**: If the RP required User Verification (`userVerification: "required"`), it must actually check the UV bit in `authenticatorData.flags` on every assertion and reject if unset. **Requesting UV in the options is not the same as verifying it on the response**, this is the whole vulnerability class.

**Failure mode**: The RP asks for UV in the assertion options but never inspects the flag in the returned `authenticatorData`. An attacker who can produce an assertion with the UV bit cleared, by any means, is signed in as the user, without user verification ever happening.

The historical "silent passkey assertion" attack ([publicly demonstrated in 2020](https://web.archive.org/web/20250527134756/https://hwsecurity.dev/2020/08/webauthn-pin-bypass/)) achieved this by MITM'ing the CTAP2 request over NFC to a hardware key in the victim's pocket, stripping the UV requirement, and letting the key sign silently. That variant is real but exotic, it needs the security key to be reachable and to be one that respects request-level UV rather than enforcing UV always. For a **server-side pentest, you don't need any of that**.

**How to test:**

1. Register a credential against the RP with `userVerification: "required"` in the registration options.
2. Use the Passkey Injector's Software Signer to produce an assertion with the UV bit in `flags` cleared (bit 2 = 0). No MITM, no NFC, no real authenticator involved, you fabricate the bytes directly.
3. Submit the assertion. If the RP accepts, it isn't checking the flag on the response. Server-side finding confirmed.
4. Repeat with the UP bit (bit 0) cleared.

Also test the registration-time dual: does the RP register the credential as **always-require-UV**? A credential enrolled with `alwaysUv` set forces the authenticator to enforce UV regardless of what any request says, the correct belt-and-suspenders fix. Verify by observing whether a UV-cleared *request* against a legitimate authenticator produces a UV-set *response* (correct: authenticator overrode the request) versus a UV-cleared response (broken: authenticator honored the tampered request).

**Tool**: Passkey Injector's Software Signer for producing arbitrary-flag assertions. DSInternals Passkey UI for probing registration-side handling with a real authenticator.

**Reporting note**: The fix is dual:

1. the RP must check UV/UP flags on every assertion,
2. the RP should register credentials with always-require-UV so the authenticator enforces UV regardless of any in-transit tampering.

Report both, even if only one is missing.

### Test A6: Algorithm and attestation acceptance

**Assumption:** The RP only accepts a curated list of algorithms and attestation formats, and enforces attestation policy when the deployment requires device-bound authenticators.

**Failure mode:** Several distinct sub-cases:

- The RP's `pubKeyCredParams` list includes weak or deprecated algorithms.
- The RP requests `attestation: "direct"` but doesn't validate the attestation statement's signature chain against a trusted root.
- The RP accepts `none` attestation when policy says only certified hardware should register.
- The RP doesn't validate the AAGUID against an allowlist despite policy claiming it does.

**How to test:**

1. Enumerate `pubKeyCredParams` from the registration options. Flag any non-standard entries.
2. During a registration, present a self-attestation with a mismatched AAGUID (e.g. an AAGUID that the FIDO MDS lists as a specific vendor when your authenticator is not that vendor). If the RP accepts, it isn't checking MDS.
3. Try registering with `attestation: "none"` (client-side tamper) against an RP whose docs claim to require direct attestation.
4. Present an attestation statement with an intentionally invalid signature; the RP must reject.

**Tool:** [FIDO MDS Explorer](https://opotonniee.github.io/fido-mds-explorer/?ref=niklas-heringer.com) for AAGUID lookup. DSInternals Passkey UI for crafting attestation variations.

**Reporting note:** Impact ranges from "informational" (weak alg in list, unused) to "high" (enterprise policy bypass: a synced passkey registers where only hardware should). Grade accordingly.

### Test A7: Administrative registration abuse (Shadow Passkey)

**Assumption:** Only the account owner can add a passkey to their account. If administrative registration exists, it is auditable, alerted on, and limited to appropriate roles.

**Failure mode:** An identity platform's admin API allows privileged users to register credentials on behalf of other users. An attacker who obtains sufficient role (through phishing, token theft, or role escalation) plants a persistent passkey against a high-value target account. The passkey survives password resets. Notification emails are ignored or filtered.

**How to test:** In an authorized engagement where you've reached a role that could plausibly do this:

1. Enumerate the admin API surface for passkey registration. On Entra ID, `UserAuthenticationMethod.ReadWrite.All` or `UserAuthMethod-Passkey.ReadWrite.All` Graph permissions (they are *beta)*. On Okta, `okta.users.manage`.
2. Register a passkey against a test account under your control (never against a real high-value target without explicit scope) using `DSInternals.Passkeys` PowerShell module, which handles the Graph and Okta pre-registration APIs.
3. Verify: does the target user receive an email? Does an audit event fire? Is the AAGUID of the attacker-controlled authenticator flagged?
4. Sign in as the target with the new passkey. Persistence confirmed.

**Tool:** `DSInternals.Passkeys` PowerShell module.

**Reporting note:** This is a *persistence* finding, not an authentication bypass: the attacker already needed the admin role. Report it as post-exploitation risk with recommendations for (a) admin-role scope reduction, (b) alerts on passkey registration events, (c) enforced attestation with AAGUID allowlist so attacker-registered credentials must come from approved hardware.

---

## Recon and testing checklist (Layer A)

Printable one-pager:

```
[ ] Capture baseline registration and authentication (Burp + browser)
[ ] Enumerate pubKeyCredParams, attestation policy, authenticatorSelection
[ ] Fetch /.well-known/webauthn if present; verify origin allowlist
[ ] Enumerate allowCredentials response for username enumeration leaks

Server-side verification:
[ ] A1  Replay a valid assertion → new session
[ ] A2  Swap assertion between two sessions
[ ] A3  Replay lower-counter assertion after higher one
[ ] A4  Assertion with wrong origin / wrong scheme / stale ROR
[ ] A5  Strip UV/UP flags; verify RP rejects
[ ] A6  Register with wrong AAGUID / bad attestation sig / weak alg
[ ] A7  Admin-API passkey registration on behalf of another user
[ ] +   Recovery flow: password + email-OTP fallback still available?
[ ] +   Session cookie theft post-passkey (out of WebAuthn scope but in RP scope)

```

---

## Layer B: Transport (TLS)

Brief, because in a healthy deployment there isn't much here. Two things worth verifying:

- The RP's HSTS policy and certificate pinning posture on native clients: a compromised CA or a corporate MITM proxy that terminates TLS in front of the RP re-enters the game.
- Any legacy or dev endpoints that accept WebAuthn ceremonies over HTTP. Some frameworks make this too easy in local mode and it survives into staging.

If you're testing an internal RP with a private PKI, an in-scope MITM changes everything downstream and you should note that in your threat model even if you don't exploit it.

---

## Layer C: Browser and WebAuthn API

The browser is what makes origin binding real. If the browser is subverted, so is origin binding and everything below it. Two categories:

**Malicious browser extensions.** An extension with `webRequest` or content-script access to the RP's origin can observe or tamper with the WebAuthn ceremony. Some password-manager extensions autofill passkeys by intercepting the ceremony before the platform WebAuthn API is called at all (1Password's behavior described in the SpecterOps paper). Even if not malicious, this creates an alternate credential path that bypasses OS-level protections and telemetry.

**Test:** In an endpoint-in-scope engagement, enumerate installed extensions on the target profile. For unfamiliar extensions, check requested permissions; anything with `<all_urls>` or the RP's origin, plus `webRequest`, is a candidate. There is no reliable protocol-level defense against a malicious extension; the mitigation is enterprise extension allowlisting.

**Injected JavaScript / compromised page.** If XSS is present on the RP, an attacker's script runs at the correct origin and can freely invoke `navigator.credentials.get()`. Because the origin binding is *satisfied* (the script really is running at the right origin), the resulting assertion will authenticate. The attacker script can then use the assertion, for example, submitting it back to the RP to grant themselves a session, or exfiltrating it to their C2.

**Test:** Look at how the RP would authorize a WebAuthn ceremony triggered by non-user-initiated code. Does it require a `crossOrigin: false` check? Is there any UI proof that the *user* initiated the ceremony rather than a script? (In most browsers, WebAuthn requires a transient user activation for `create` but not always for `get`: worth checking per browser.)

**Test:** Verify the RP's Content-Security-Policy blocks arbitrary script sources; verify subresource integrity on third-party scripts. An RP that ships a WebAuthn login page with a broad `script-src` policy is one script-injection away from having its own users' passkeys turned into session cookies.

**Reporting note:** Layer C findings are RP-web-app findings that happen to hit WebAuthn. Frame them as "our phishing-resistant authentication is defeated by an ordinary XSS in the login flow". That framing tends to raise priority appropriately.

---

# Part 3: Attacking the OS and the Authenticators

Part 2 mapped the attack surface and walked through server-side Relying Party tests. This last part covers the two remaining layers: the operating system that sits between the browser and the authenticator, and the authenticators (and stored credentials) themselves. 

> *Most of the research here is Windows-specific, because that is where the current, richest body of public work lives: *Michael Grafnetter*'s* Pass-the-Passkey Family of Attacks *(SpecterOps, Black Hat USA 2026).*

The mental shift for this part is straightforward: **if you already have code execution on the endpoint, the "phishing-resistant" property of passkeys no longer holds in any meaningful sense.** The private keys stay safely in the TPM or the secure element, but you don't need them. You need a *signed assertion*, and the operating system will produce one for any program that asks nicely.

Everything in this part is scoped to red-team and endpoint-in-scope engagements. If your engagement is web-app-only, the tests in Part 2 are your beat.

---

## Layer D: Operating System (Windows focus)

### The Windows passkey flow, in one paragraph

When Chrome or Edge calls `navigator.credentials.get()`, it does not talk to the authenticator itself. It forwards the request to the Windows WebAuthn API (`webauthn.dll`) via `WebAuthNAuthenticatorGetAssertion()`. `webauthn.dll` enumerates available authenticators through the Cryptographic Services (`CryptSvc`) system component, displays the authenticator picker via `CredentialUIBroker.exe`, and routes the ceremony to one of: Windows Hello (platform), a CTAP2 roaming key (USB/NFC/BLE), a phone via hybrid transport, or a third-party authenticator plugin (MSIX package implementing `IPluginAuthenticator` COM). Throughout, `webauthn.dll` and `CryptSvc` write diagnostic events to `Microsoft-Windows-WebAuthN/Operational`. **Almost every arrow in that flow is a place where the ceremony can be observed or tampered with**, which is the entire reason this layer is interesting.

### Test D1: Assertion mining via Windows Event Log

**What it is:** Until July 14, 2026, Windows 11 wrote the full `PublicKeyCredential` (including `signature`, `authenticatorData`, `clientDataJSON`, `userHandle`, and credential ID) into the `Microsoft-Windows-WebAuthN/Operational` event log on every passkey authentication. This log is readable by low-privilege users. Combined with an RP that doesn't defend against replay (Test A1), it's a full identity-impersonation primitive that satisfies phishing-resistant MFA policies and generates no XDR alerts.

**Patch status:** CVE-2026-34348 (Microsoft classifies it as "Windows Event Logging Service Information Disclosure"). Patched Windows truncates the `signature` field in logged assertions to 6 bytes, breaking replay while preserving the log for debugging. Older/unpatched Windows 11 remains vulnerable.

**How to test:**

1. Confirm the target is Windows 11 and check patch level (`Get-HotFix`, or inspect an event `2106` payload. If `signature` is full-length base64 it's unpatched).
2. From a low-privilege user on the box, run:

```powershell
   .\Get-PasskeyAssertionEvent.ps1 -ComputerName TARGET

```

The script extracts recent WebAuthn assertions with their origin, user, and process ID. Remote read requires membership in Event Log Readers, Remote Desktop Users, Remote Management Users, or Administrators.

1. Feed a captured `PublicKeyCredential` JSON into the Passkey Injector to impersonate the user against the RP.

**Exceptions the log does *not* capture:**

- Private browsing sessions (Edge InPrivate, Chrome Incognito): no events written.
- Password manager extensions that autofill passkeys directly (1Password's browser-side path) bypass the Windows WebAuthn API entirely.
- The `prf` extension output is redacted (Microsoft was aware of that leak).

**Reporting note:** Even on patched systems, the *event stream itself* is still a rich telemetry source you can invert into an attack: knowing when a user is authenticating with a passkey (Test D6 and D7 rely on this).

### Test D2: Passkey assertion phishing (malware invokes the WebAuthn API directly)

**The key observation:** The `WebAuthNAuthenticatorGetAssertion` Win32 API is documented and callable by *any* Windows process, not just browsers. Browsers are constrained to fill in the `origin` from the page URL. **Malware calling the API directly is under no such constraint**, it supplies the request origin itself and can request an assertion for any RP: `login.microsoft.com`, `github.com`, whatever the operator wants.

**The victim's experience:** Standard Windows passkey prompt appears. They touch the YubiKey, look at the camera, type the PIN. From their perspective nothing is unusual, they get a prompt that looks legitimate and they confirm it. The malware receives the `PublicKeyCredential` JSON and pipes it to the C2 operator.

This is why it's classified as *phishing*: it requires user interaction, but it exploits a normalized behavior (users confirming Windows Hello prompts reflexively). It doesn't require any Microsoft bug.

**How to test:** In an endpoint-in-scope engagement with a C2 in place:

1. Deploy SharpPasskeys as a payload via your C2 (the paper demonstrates with Mythic/Apollo).
2. `SharpPasskeys.exe prompt --relying-party login.microsoft.com --challenge <op-supplied> --authenticator ClientDevice --credential-id <target>`
3. Optionally pre-select the authenticator and credential ID to reduce the number of clicks the victim has to make (fewer dialog steps → higher success rate).
4. Enumerate available passkeys ahead of time with `SharpPasskeys.exe list hello` (Windows Hello) or `list events` (historical usage from the log).

**Tool:** SharpPasskeys (SpecterOps). Not published as a BOF at time of writing for OPSEC.

**Detection:** WebAuthn API usage by non-browser processes is a strong EDR signal. See §OPSEC notes below.

### Test D3: Application metadata spoofing

**What it is:** The Windows passkey prompt shows the identity of the requesting application (e.g. "Requested by Microsoft Edge (Microsoft Corporation)"), but only if the user clicks the small info glyph. The displayed name and publisher come from the **executable's version-information resource:** the same fields visible on the file's Properties → Details tab.

**Because those fields are attacker-controlled**, a `.NET` malware author sets:

```xml
<AssemblyTitle>Microsoft Edge</AssemblyTitle>
<Authors>Microsoft Corporation</Authors>

```

and produces a prompt indistinguishable from a legitimate one to a security-conscious user who actually bothers to check.

**Note from the SpecterOps paper:** in older Windows 11 builds, the digital signature of the requesting binary was also considered. They aren't sure exactly when that changed, but current builds trust the metadata alone.

**How to test:** Build a signed-metadata clone and pair it with Test D2\. If the info-glyph identity matches the impersonated app, the deployment is vulnerable to this UX-layer trust bypass.

### Test D4: Credential UI window-handle spoofing (unfixed at time of writing)

**What it is:** `WebAuthNAuthenticatorGetAssertion` takes an `HWND hWnd` parameter, the parent window that will host the modal credential dialog. **Windows does not validate that `hWnd` belongs to the calling process.** A malicious process can pass the window handle of any other application on the desktop (a browser, Outlook, PowerPoint), and Windows will parent the passkey dialog to *that* window. The dialog appears to originate from the target application.

Microsoft assessed this as Low / Defense in Depth and closed the case (MSRC VULN-185216). Which is to say: at the time of writing, this is present-tense exploitable on fully patched Windows 11.

**How to test:**

```powershell
SharpPasskeys.exe list hwnd
# pick the target window's handle, e.g. msedge = 2432736

SharpPasskeys.exe prompt --hwnd 2432736 `
    --relying-party github.com `
    --challenge <base64url>

```

The dialog appears as a modal of the target application. Passing `--hwnd 0` makes SharpPasskeys auto-discover a suitable browser process.

**Attack payoff:** combined with Test D3 (metadata spoofing), a rogue prompt is nearly indistinguishable from a legitimate one. Same parent window, same app name, same publisher.

### Test D5: Prompt flooding (MFA fatigue, passkey edition)

**What it is:** When a user dismisses an unexpected passkey prompt, the attacker calls `WebAuthNAuthenticatorGetAssertion` again. And again. Windows Hello face-recognition is fast enough that users often confirm prompts reflexively without pausing to consider what they're confirming. The SpecterOps team notes that even their own security-professional colleagues admitted to reflex-confirming.

> The prompt-flood window is bounded by challenge validity, usually 5–10 minutes.

**How to test:** `SharpPasskeys.exe prompt --flood ...`. Combine with `--hwnd` (Test D4) and metadata spoofing (Test D3) for maximum UX plausibility.

**Reporting note:** No pure technical fix. The defensive story is user training + reducing the frequency of legitimate prompts (session-length policy) so unexpected prompts stand out.

### Test D6: Credential UI overlay (wait-and-imitate)

**What it is:** Three variants explored in the paper, one practical.

1. *Direct overlay* (**impossible**): Windows enforces exclusive access; a second WebAuthn call while one is in progress returns Access Denied.
2. *Kill the legitimate broker mid-ceremony:* technically works after the second kill of `CredentialUIBroker.exe`, but freezes the legitimate application and is noisy.
3. *Wait its turn:* the practical one. The attacker monitors the WebAuthn event log for a legitimate ceremony to *complete*, then immediately fires its own prompt. The victim sees the prompt twice in a row and, particularly with fast Windows Hello, doesn't think twice.

**How to test:**

```powershell
SharpPasskeys.exe wait                # blocks until legit ceremony seen
SharpPasskeys.exe prompt --hwnd 0 --flood --relying-party <target> ...

```

### Test D7: Passkey Detour Attack (hooking `webauthn.dll`)

**What it is:** Inject a native DLL (Microsoft Detours) into a browser process that hooks `WebAuthNAuthenticatorGetAssertion`. The hook communicates with a controlling SharpPasskeys process over `\\.\pipe\WebAuthnHook`. Three operational modes:

- **Capture Mode.** Let the ceremony complete; return the signed assertion to the operator via the named pipe; return a timeout error to the browser. Victim sees a "login failed, please retry"; operator holds a fresh assertion.
- **Inject Mode.** For RPs that bind challenges to sessions (Test A2 passes on this RP). The operator starts their own login, obtains a challenge, the hook replaces the browser's challenge with the operator's before calling `webauthn.dll`. The victim signs the operator's challenge; the resulting assertion authenticates the operator, not the victim.
- **Replay Mode.** Variant of Capture Mode that returns the genuine assertion to the browser too. Both the operator and the victim end up authenticated from the same assertion. Only works against RPs that fail Test A1 or A3.

**How to test:**

```powershell
SharpPasskeys.exe hook attach                                # inject DLL
SharpPasskeys.exe hook wait --capture --rpid login.microsoft.com

```

**Detection:** Named pipe `\\.\pipe\WebAuthnHook` is a hard signal on managed endpoints. Custom pipe names via `--named-pipe` bypass name-based signatures.

### Test D8: Passkey Circuit Breaker

**What it is:** The lower-tech precursor to D7\. A PowerShell script watches the WebAuthn event log; the moment a passkey ceremony begins, the script suspends the browser process (standard user) or blocks its outbound traffic via firewall rules (admin). The signed assertion never reaches the RP; the operator forwards it instead.

Only works against RPs that don't bind challenges to sessions (Test A2 fails on the RP).

**Tool:** `Invoke-PasskeyCircuitBreaker.ps1`.

### Test D9: RDP / Hyper-V WebAuthn pass-through

**What it is:** Windows 11 supports relaying WebAuthn ceremonies from an RDP or Hyper-V enhanced-session guest to the host, via the MS-RDPEWA virtual channel. Enabled by default in the "Local devices and resources" tab of the RDP client.

**Attack:** Malware on the RDP host (which the user is *connected to*, not from) invokes `WebAuthNAuthenticatorGetAssertion`. The prompt appears on the user's *local* machine. Depending on the client Windows build, the dialog may indicate the request originated from a remote session (or may not). Combine with D3 for identity spoofing.

**How to test:** In a scenario where you compromise a jump box, VDI, or shared RDP server, verify that WebAuthn pass-through is enabled in the RDP client policy. If it is, prompts fired from your compromised server land on the connecting user's real desktop.

**Reporting note:** Recommend disabling WebAuthn pass-through in the RDP client (`Local Devices and Resources → WebAuthn (Windows Hello or security keys)`) for connections to untrusted hosts. Enterprise policy via GPO.

### Test D10: Passkey-to-token (finish the exploit chain)

**What it is:** WebAuthn is a JavaScript API. Command-line tools that need Microsoft Entra ID tokens can't directly consume it. The Passkey Injector automates the OAuth 2.0 authorization code flow against `login.microsoft.com`, impersonating a public client like Microsoft Azure PowerShell or Microsoft Edge, feeds in a captured or injected `PublicKeyCredential`, and returns access/refresh/ID tokens. `Invoke-EntraPasskeyInjection.ps1` (based on TokenTactics v2) does the same from PowerShell.

**How to test:** Chain from any of D1/D2/D7 to a full OAuth token set. From there, the compromise is post-authentication and follows the usual Entra ID red-team playbook.

---

## Layer E: Authenticators and credential storage

The private keys inside Windows Hello (VBS + TPM) and hardware roaming keys are the hardest thing on the map to attack. This section is about everywhere else the key material lives.

### The Windows Hello counter quirk

Windows Hello **always sends a counter of 0** for passkeys on Entra-registered devices; it doesn't maintain a per-passkey signature counter. When Microsoft rolled out signature-counter tracking in Entra ID (May 2026, in response to the SpecterOps research), the fix worked for FIDO2 hardware keys but not for Windows Hello, which will keep sending 0 indefinitely. Same story for KeePassXC-managed passkeys, and for 1Password → other-manager migrations that don't preserve counters.

**Test:** Register a Windows Hello passkey against the target RP. Verify counters remain at 0 across successive logins. Then test replay (A1/A3): the counter track can't protect this credential.

### Synced passkeys and their export formats

The threat model is: malware on a user's machine harvests exported passkey files or intercepts the sync path. The private key ends up in the operator's hands and can be signed with using the Passkey Injector's Software Signer. **No access to the authenticator, no user prompt, no counter to worry about.**

#### KeePassXC `.passkey` exports

KeePassXC exports one passkey per JSON file, `.passkey` extension, **cleartext PEM-encoded private key**. The KeePassXC developers know: the export dialog warns explicitly. Payload:

```json
{
  "credentialId": "...",
  "privateKey": "-----BEGIN PRIVATE KEY-----\nMC4CAQAw...\n-----END PRIVATE KEY-----",
  "relyingParty": "webauthn.io",
  "url": "https://webauthn.io",
  "userHandle": "...",
  "username": "..."
}

```

Passkey Injector loads these directly. **A single `.passkey` file on a compromised drive is a full authentication credential.**

**Hunt:** `Get-ChildItem -Path C:\Users\ -Recurse -Include *.passkey -Force -ErrorAction SilentlyContinue`.

#### Bitwarden JSON exports

Three modes:

1. **Unencrypted JSON (default option in the UI).** `fido2Credentials` array with `keyValue` field in cleartext. Passkey Injector consumes directly.
2. **Password-protected JSON.** Encrypted with a user-chosen password. Passkey Injector prompts for the password when loading.
3. **Account-restricted export.** Encrypted against the user's Bitwarden account; re-importable only into that same account. Not supported by Passkey Injector.

**Hunt:** search for JSON files with the Bitwarden export shape (`"passwordHistory"`, `"revisionDate"`, and `"fido2Credentials"` strings).

#### Credential Exchange Format (CXF)

Emerging FIDO Alliance standard for moving credentials between password managers. Single JSON document holding multiple credentials, including passkeys with cleartext private keys. The spec puts protection responsibility on the exporter/orchestrator, not the file at rest. If malware intercepts a CXF payload during an inter-manager transfer, the passkey material is exposed.

> Passkey Injector supports the format. Expect industry-wide CXF adoption over time.. this attack surface **grows**.

**Test methodology for Layer E synced passkeys:**

1. Hunt for exported artifacts on the compromised host (`.passkey`, Bitwarden JSON, CXF).
2. Identify by RP (the `relyingParty` / `rpId` field).
3. Load into Passkey Injector's Software Signer.
4. Configure counter (default 0; matches most software authenticators, but override if the RP tracks strictly), UV flag (default set, matches typical prompt behavior), UP flag (default set).
5. Authenticate as the victim against the RP; no user interaction, no authenticator present.

### Third-party authenticator plugins

Windows 11 supports third-party passkey authenticators as MSIX plugins implementing the `IPluginAuthenticator` COM interface (UUID `d26bcf6f-b54c-43ff-9f06-d5bf148625f7`). The SpecterOps team explored building a rogue plugin ("Evil Authenticator Plugin") but couldn't fully abuse the interface. Windows exposes only `clientDataHash` to the plugin, not `clientDataJSON`, blocking origin-tamper attacks from within a plugin.

The takeaway is defensive: because plugin development is complex and poorly documented, existing plugins (1Password, Bitwarden, KeePassXC) deserve careful audit. A malicious or vulnerable plugin becomes an authenticator every browser will trust.

### User verification bypass (revisit)

Already covered as Test A5, but its natural attack habitat is Layer E: intercept the CTAP2 request over NFC to a security key in the victim's pocket, strip UV/UP requirement bits, get a silent signature. Fixed at registration time by binding the credential to *always require UV*.

The Passkey Injector + DSInternals Passkey UI reproduce the attack against test RPs to verify current posture.

---

## Layer D/E testing checklist

```
Endpoint / Windows OS:
[ ] D1  Patch level for CVE-2026-34348; try Get-PasskeyAssertionEvent.ps1
[ ] D2  Non-browser process invokes WebAuthNAuthenticatorGetAssertion
[ ] D3  Metadata-spoofed prompt indistinguishable from real app
[ ] D4  Window-handle spoofing (HWND of a browser or Office app)
[ ] D5  Prompt flooding until user reflex-confirms
[ ] D6  Wait-and-imitate after legitimate ceremony completes
[ ] D7  Passkey Detour hook (Capture / Inject / Replay modes)
[ ] D8  Circuit Breaker script (only if A2 fails on target RP)
[ ] D9  RDP / Hyper-V WebAuthn pass-through from compromised host
[ ] D10 Chain a captured assertion to OAuth tokens

Authenticators / storage:
[ ] E1  Windows Hello counter=0 quirk against a counter-tracking RP
[ ] E2  Hunt .passkey files (KeePassXC exports)
[ ] E3  Hunt Bitwarden unencrypted JSON exports
[ ] E4  Hunt CXF files during any active inter-manager migration
[ ] E5  Audit installed IPluginAuthenticator plugins
[ ] E6  Silent-assertion UV bypass (usually A5 by proxy)

```

---

## OPSEC and detection notes (from the paper)

Useful to know as an attacker (evade), and to hand to defenders (detect):

- **SharpPasskeys signals on managed endpoints:** WebAuthn API usage by non-browser processes; process reading `Microsoft-Windows-WebAuthN/Operational` event log; enumeration of top-level window handles; DLL injection primitives (`CreateRemoteThread`, `VirtualAllocEx`, `WriteProcessMemory`); the default named pipe `\\.\pipe\WebAuthnHook`.
- **Passkey Injector signals:** because it's a standalone WebView2 app, the tells are in the *page*: the JS bridge `chrome.webview.hostObjects.webAuthnBridge` and WebAuthn calls not served by a genuine platform API. RPs can watch for this client-side.
- **Detection unlikely on private-browsing sessions;** event log gap noted in D1\. Attackers can exploit this; defenders should be aware.

---

## Defensive takeaways (for the blue-team readers who made it here)

Short, because SpecterOps §5 already covers this well. Reduced to the highest-leverage recommendations:

**For RPs / web app developers.** Implement the full 25-step WebAuthn assertion verification. Track challenges (single-use, session-bound, TTL-bound). Track signature counters and be explicit about the compat tradeoff for software authenticators. Enforce attestation and AAGUID allowlists for high-value accounts. Bind credentials to always-require-UV at registration. Alert on registration events. Do not ship a passwordless deployment with a password + email-OTP recovery path.

**For IT administrators.** Prefer device-bound hardware authenticators for privileged accounts. Disable WebAuthn pass-through on RDP for untrusted hosts. Patch CVE-2026-34348\. Ensure `Microsoft-Windows-WebAuthN/Operational` is not readable by non-admins on high-value hosts; audit membership of Event Log Readers, Remote Management Users, Remote Desktop Users. Deploy EDR content that flags the OPSEC signals above.

**For pentesters and red teamers.** Test the RP first, the majority of findings are there. Only use the OS-layer tests in scope. Report replay findings as authentication bypass, not information disclosure, and be ready to demonstrate impact.

---

# Where to go next

- Grafnetter's SpecterOps whitepaper is the primary source and worth re-reading section by section as you actually run these tests. The tools directory (§2) is your starter kit.
- FIDO Alliance's Metadata Service (MDS) documentation, if you want to build AAGUID-allowlist automation into your reports.
- Watch for CXF adoption across password managers. **Layer E** **will grow**.
- On the defensive side, Microsoft's Zero Trust framework is the right framing for the "passkeys are one pillar" conversation with stakeholders who think phishing-resistant MFA is a finish line.

> Passkeys are still a **massive improvement over passwords**. They just aren't the end of authentication security. They're a floor, and the rest of the building still needs walls.

# References

- Michael Grafnetter (SpecterOps): [Pass-the-Passkey Family of Attacks](https://specterops.io/wp-content/uploads/sites/3/2026/08/Pass-the-Passkey%5FA4%5Fv2.pdf?ref=niklas-heringer.com), Black Hat USA 2026
- CVE-2026-34348: Windows Event Logging Service Information Disclosure
- MS-RDPEWA: [Remote Desktop Protocol: WebAuthn Virtual Channel Protocol](https://learn.microsoft.com/en-us/openspecs/windows%5Fprotocols/ms-rdpewa/?ref=niklas-heringer.com)
- FIDO Alliance: [Credential Exchange Format (CXF)](https://fidoalliance.org/specifications-credential-exchange-specifications/?ref=niklas-heringer.com), [Metadata Service (MDS)](https://fidoalliance.org/metadata/?ref=niklas-heringer.com), [Client to Authenticator Protocol (CTAP) 2.1](https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html?ref=niklas-heringer.com) & Metadata Service ([MDS Explorer](https://opotonniee.github.io/fido-mds-explorer/?ref=niklas-heringer.com))
- Tools: [SharpPasskeys / DSInternals.Passkeys / Passkey Injector](https://github.com/SpecterOps/pass-the-passkey?ref=niklas-heringer.com), Mythic C2, TokenTactics v2, Burp Suite
- W3C: [Web Authentication (WebAuthn) Level 3](https://www.w3.org/TR/webauthn-3/?ref=niklas-heringer.com), especially [§7.2 Verifying an Authentication Assertion](https://www.w3.org/TR/webauthn-3/?ref=niklas-heringer.com#sctn-verifying-assertion)
- Alf Løkken: [Understanding FIDO2, WebAuthn, and Passkeys](https://alflokken.github.io/posts/understanding-fido2-passkeys/?ref=niklas-heringer.com)