web-webauthn-software-authenticator
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 attest
SKILL.mdskills/web/web-webauthn-software-authenticator/SKILL.md
GitHubForge 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/beginreturns 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-bypassand 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,pubKeyCredParamsincl. -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) + attestedCredentialDatawhereflags = 0x45(UP|UV|AT) andattestedCredentialData = 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}.
- EC P-256 keypair. COSE public key =
POST /register/finishwith{id:b64u(credId), rawId:b64u(credId), type:"public-key", response:{clientDataJSON:b64u, attestationObject:b64u}}→ credential created.POST /auth/begin→ newchallenge.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/finishwith 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), andrp.idis the host WITHOUT the port.sha256(rp.id)uses the host only. challengeinclientDataJSONis 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/beginreturns emptyallowCredentials; you MUST send the correctuserHandleatauth/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.