The Consumer Key That Opened Enterprise Mail — Java Bug Hunt

Modelled on Storm-0558 (disclosed July 2023): a China-based threat actor forged authentication tokens with an acquired Microsoft account (MSA) consumer…

  • Language: Java
  • Layer: Backend
  • Difficulty: Medium
  • Concepts: Security, Auth
  • Modelled on: Microsoft · Storm-0558 (2023)
  • Visible tests: a genuine enterprise token validates; a consumer key cannot sign an enterprise token
  • Reward: 50 XP for a complete fix

Briefing

Modelled on Storm-0558 (disclosed July 2023): a China-based threat actor forged authentication tokens with an acquired Microsoft account (MSA) consumer signing key and used them to read enterprise email at a number of organisations, including US government agencies. Microsoft's investigation found that a token-validation flaw allowed the consumer key to be trusted for enterprise tokens.

This project is a reconstruction. TokenValidator checks the token's issuer and signature, but it looks the signing key up in the whole key ring — so a key that belongs to a different issuer is happily accepted as long as the signature verifies.

Fix validate so a token is only verified with a key that its own issuer publishes.

Bug report

BUG-S0558 · Priority: Critical (token forgery) · Reported by: incident response

TokenValidator.validate(ring, token, expectedIssuer) returns, checking in order:

  1. "wrong-issuer" if token.iss != expectedIssuer
  2. "untrusted-key" if token.kid is not one of ring.keysFor(token.iss) — a key held in the ring for ANOTHER issuer does not count
  3. "bad-signature" if Signer.sign(key.secret, token.payload()) != token.sig
  4. "ok" otherwise

Observed: a token signed with the consumer key "msa-1" but claiming the enterprise issuer validates as "ok" at the enterprise endpoint.

Logs

[owa] token kid=msa-1 iss=https://sts.windows.net/contoso sub=ceo@contoso -> ok
[audit] mailbox access by forged enterprise token signed with consumer key

The code as shipped

src/auth/TokenValidator.java (editable)

class TokenValidator {
    // Returns "ok" or the reason the token is refused.
    static String validate(KeyRing ring, Token t, String expectedIssuer) {
        if (!t.iss.equals(expectedIssuer)) return "wrong-issuer";
        SigningKey key = ring.find(t.kid);
        if (key == null) return "untrusted-key";
        if (!Signer.sign(key.secret, t.payload()).equals(t.sig)) return "bad-signature";
        return "ok";
    }
}

Read-only context: src/auth/KeyRing.java, src/auth/Token.java.

Open the hunt to edit the files, run the visible tests and submit against the hidden ones. More Java bug hunts.