From a Sequential ID to a Stranger’s eSIM: An Unauthenticated IDOR Walk-Through

TLP: CLEAR · 2026 · Field Note · Confidence: High · Disclosed under coordinated terms

Summary

This note walks through an unauthenticated access-control failure on the inventory API of a mobile provisioning platform. Two endpoints that were never meant to be reachable without a session let an anonymous attacker walk a sequential identifier space and, for each record, retrieve the eSIM activation code belonging to a SIM that was provisioned for someone else.

The bug itself is the oldest one in the book — an object reference with no authorization check behind it. What makes it worth writing up is not the technique but two things that are usually glossed over in IDOR write-ups: how you recognise that a returned blob is actually a credential (the response was a PNG, not an obvious secret), and how you scope the impact honestly once you have the primitive — including the reasoning that kept this at High rather than letting me oversell it as Critical.

The target, host names, and vendor-specific identifiers are redacted; this was reported through a coordinated programme and is published with permission after remediation. The mechanics below are reconstructed faithfully, with anything that would make it copy-paste reproducible against the live estate removed.

1. How the Target Surfaced

The programme was a wide wildcard — thousands of assets under a single *.<corp> root. On a scope that size you do not test “the application”; you test the attack surface, and the highest-yield filter on a wildcard is hostname semantics. I pulled the resolved subdomains and sorted them by name, looking specifically for back-office vocabulary: admin, ops, internal, provisioning, boss, and — the one that mattered here — inventory.

Hosts like that are interesting for a structural reason. Internal tooling tends to be written under the assumption that only authenticated staff (or another internal service) will ever call it, and that assumption frequently leaks into the code as an absent authorization layer rather than a present-but-bypassable one. When such a host ends up published to the internet — usually because it sits on the same wildcard and shared ingress as the public product — the gap between “who the developers pictured calling this” and “who can actually reach it” is exactly where unauthenticated IDOR lives.

The host in question answered on HTTPS with a small JSON API and no front-end to speak of. No login wall, no redirect to an SSO provider — an anonymous request to a sensible-looking path returned structured data rather than a 401. That, on its own, is not a finding. It is the precondition for the hypothesis I tested next.

2. Forming the Hypothesis

The hypothesis was specific and falsifiable: this API exposes per-SIM records keyed by a sequential integer, and the read path performs no check that the caller is entitled to the record it returns.

Two cheap observations pushed me toward it. First, a record-lookup endpoint took an id parameter that was a small, dense integer — the kind of value that is almost always a database primary key rather than an opaque, unguessable token. Sequential keys are a prerequisite for cheap enumeration: if the identifier had been a UUID or a signed token, the same missing-authorization bug would still exist but would be far harder to exercise at scale. Second, the responses changed shape as the id changed — some records reported one state, some another — which told me the parameter was a live lookup against real inventory, not a static fixture.

The thing I did not yet know was whether the data behind those keys was actually sensitive. An inventory API returning “SIM #N exists, status active” is a boring information leak. The question was whether any endpoint on this host would turn an enumerable key into something that carried real-world authority.

3. Confirming the Primitive

The host exposed a second endpoint that took a SIM identifier (an ICCID) and returned a QR code as an image. In the provisioning world a SIM’s QR code is not decorative — for an eSIM it is the activation artefact. So the chain was obvious to test: enumerate inventory records on the first endpoint, lift the SIM identifier from each, and ask the second endpoint to render its QR.

# 1) enumerate an inventory record (no auth)
curl 'https://inventory.<redacted>/api/<svc>/check-if-activation-is-needed?id=<seq>'

# 2) for that record's SIM identifier, fetch the activation QR (no auth)
curl 'https://inventory.<redacted>/api/<svc>/get-qr-code?simCardNumber=<ICCID>' \
     -o qr.png

Both returned 200 to an anonymous client. The first leaked the SIM identifier and provisioning state; the second returned a valid PNG. The trap here — and the reason a scanner would have walked straight past this — is that a 200-with-an-image looks like nothing. There is no token in the JSON, no obvious secret in the headers. You have to decode the picture to realise what you are holding.

Decoding the QR returned a string of the form:

LPA:1$<smdp-plus-host>$<MatchingID>

That is a GSMA LPA activation string (the format defined by the SGP.22 consumer-eSIM specification). It has three parts: the literal LPA:1, the address of the SM-DP+ server that holds the prepared eSIM profile, and a Matching ID — the per-profile claim token an eUICC presents to the SM-DP+ to download and install that profile. In other words, the “harmless image” decoded to the exact credential a device uses to pull down a cellular subscription. Confirming that the SM-DP+ host was a real, resolving provisioning endpoint (not a placeholder) closed the loop: these were live activation codes, retrievable for inventory I had no relationship with, with no authentication at any step.

4. Why a Matching ID Is a Credential, Not an Identifier

It is worth being precise about why this crosses from “information disclosure” into “credential exposure,” because the distinction is what justifies the severity.

In the consumer eSIM model, possession of the LPA string is largely sufficient to claim the prepared profile. The Matching ID is the secret the SM-DP+ uses to decide which pending profile to hand out, and the SM-DP+ address tells you exactly where to present it. The protocol layers in front of it — QR scanning, the local profile assistant on the handset — are user-experience, not access control. An attacker who holds LPA:1$host$matchingID for an unconsumed profile is in a position to attempt to download that subscription onto a device they control, which for a mobile line is the digital equivalent of a SIM swap without ever contacting the carrier’s help desk.

So the returned blob was not “a SIM’s ID number.” It was, for each enumerable inventory record, a provisioning credential plus the address at which to redeem it — handed to an anonymous caller.

5. Scoping the Impact — and Where I Stopped

This is the section that decides whether a report is accepted at the severity you claim, and it is the part most write-ups skip. Having the primitive is not the finding; the finding is the defensible statement of what an attacker can do with it, demonstrated, not asserted.

Three properties combined to make the impact real:

  1. It was unauthenticated. No session, no API key, no referer or origin gating that survived scrutiny. The same class of API call is authenticated elsewhere in the product; here the control was simply absent.
  2. It was enumerable. The sequential key meant this was not “leak one record if you already know its ID” — it was “walk the inventory.” I sampled across the key space rather than harvesting it (you demonstrate the shape of the population, you do not exfiltrate it), and confirmed that records returned consistent, real, per-SIM data across widely separated bands. That establishes scale: tens of thousands of records’ worth of telecom identifiers and, for the eSIM subset, activation credentials — squarely GDPR-reportable personal data for a telecoms operator.
  3. It returned a credential, not just PII. As above: the Matching ID is sensitive irrespective of consumption state, because it is a GSMA-recognised provisioning secret.

And here is where I deliberately did not oversell it. To push this to Critical I would have wanted to demonstrate a clean integrity impact — an unconsumed, activatable profile that an attacker could actually install. So I went looking for one: I sampled broadly and walked dense ranges hunting for a record that was both an eSIM and still pending activation. Across everything I sampled, the activatable-eSIM combination did not surface — the eSIM records I could see were already consumed, and the rest were physical SIMs whose QR endpoint returned no LPA payload. Two things follow from that, and I reported both honestly:

Critically, at no point did I attempt to install a retrieved profile onto a device. Retrieving an activation credential proves exposure; redeeming one would consume a real subscription and cross from “demonstrated read” into “caused effect.” The first is a proof of concept; the second is damage. The line between them is the whole of responsible testing, and it is non-negotiable.

6. Root Cause

Reconstructed from the observed behaviour, the read path was structurally an authenticated controller missing its guard — something equivalent to:

# intended (elsewhere in the product): caller is a session/service
# that has already been authorised for this tenant's inventory
record = inventory.get(id)            # <-- id straight from the query string
return render_qr(record.sim_number)   # <-- no check that THIS caller
                                      #     may read THIS record

There is no exotic flaw here. The lookup trusts that anyone able to reach the endpoint is entitled to its output — an assumption that held while the host was internal and silently became false the moment it was published on the wildcard. The sequential key did not cause the bug; it only set the cost of exploiting it to near zero. A UUID would have lowered the severity of enumeration while leaving the missing-authorization defect entirely intact.

7. Remediation

The fix is the one the bug class always demands: enforce authorization at the object, on every read path, server-side.

8. Closing Note

An IDOR on an inventory API is about as unglamorous as bugs get, and a casual scan would have filed the QR endpoint under “returns an image, ignore.” The value was never in the technique; it was in two judgements. The first was recognising that a 200-with-a-PNG decoded to a GSMA provisioning credential — that the boring blob was the secret. The second was scoping the impact to exactly what I could demonstrate: large-scale, unauthenticated exposure of telecom PII and live activation credentials, framed on credential sensitivity multiplied by population — and no further, because the activatable-profile takeover was not something I had reproduced.

That second discipline is what gets a report paid rather than disputed. Impact is the product of what you can prove, not what you can imagine; the strongest finding is the one where every noun in the impact sentence is something you actually held in your hands.