Skip to content

Multi-Surface Web Exploitation and LLM Abuse – NahamCon CTF 2025: The Mission

TL;DR

  • Large web app with REST, GraphQL, Java Actuator, and internal dashboards.
  • Endpoint discovery reveals deprecated and internal APIs.
  • GraphQL introspection leaks user IDs and report metadata.
  • Java actuator and heapdump expose internal tokens.
  • Second-order path traversal enables report state manipulation.
  • LLM chat widget abused via impersonation to extract bonus flag.

Description

HackingHub has provided this CTF challenge!

Welcome to the BugBounty platform, you've submitted a report for Yahoo but oh no!! So has STÖK and it looks like you're going to get dupped!

Hack into the platform and change STÖK's report to duplicate first so you can grab the bounty!

Solution

Recon & First Steps

Platform looks familiar 😼 We've got a /hackers page with 8 usernames and a /login. Tried basic creds (admin:admin, admin:password) and a couple lazy ' OR 1=1 -- attempts. Nothing.

BugBounty platform login page with list of hackers

We're also given a wordlist.txt, so I assumed it's for brute-forcing the login. But the contents don't look like passwords — they're endpoint names. So I pivot to fuzzing.

Endpoint fuzzing results using provided wordlist

Endpoint Fuzzing

Used the wordlist to brute endpoints, found:

/api
/api/v1
/api/v2

All of them mention this .war file:

{ "server": "openjdk:19-jdk:bountyapi.war", "message": "BugBountyPlatform API" }

/api/v1 warns it's deprecated — maybe that means vulnerable?

{ "server": "openjdk:19-jdk:bountyapi.war", "message": "Deprecated, please use v2" }

Also found /dashboard and /settings — both redirect to /login.

/uploads exists too — fuzzed inside it with the same wordlist. Might be useful for insecure file upload or privilege escalation later.

At this point, I remembered the challenge literally says we need to mess with STÖK's report. Guessed /reports. Nothing on /api or /api/v1, but /api/v2 returns:

{ "error": "Missing Required Fields" }

Tried fuzzing keys like:

/api/v2/reports?fuzz=1

But all the responses were the same length. Then I tried REST-style:

/api/v2/reports/fuzz

Still nothing — always got "Invalid Unknown Endpoint". Out of desperation I asked ChatGPT (lol), and it suggested:

/api/v2/reports?user_id=123

That actually worked — gave Invalid User ID. Tried some IDs, usernames, numbers, fuzzed 1–1000 — still nothing.

Then I re-read the challenge and realized I'm an idiot. They gave creds in the description:

hacker:password123

Logged in — finally. The dashboard shows a pending SSRF -> RCE report.

User dashboard showing submitted bug report

Account Access & GraphQL

You can click into your profile and try to edit account details, but it errors out saying updates are disabled.

Account settings page showing updates disabled error

While messing with that, I saw GraphQL requests in Burp. Right-click → GraphQL → Introspection query, then save the queries to site map.

GraphQL introspection query executed in Burp

Now we can actually see the GraphQL queries and endpoint structure. Found:

/api/reports?user_id=...

Never would've guessed that during brute-force.

GraphQL query revealing report-related endpoints

Sending one of the queries in Repeater returns all the top hackers and their UIDs. One of them has a flag.

GraphQL response listing users and user IDs

Flag: flag_4{253a82878df615bb9ee32e573dc69634}

Apparently there are 6 total though 🤔

With the UIDs from the GraphQL output, I try:

/api/v2/reports?user_id=15ee453d-18c7-419b-a3a6-ef8f2cc1271f

Checked each user — nothing else. Tried adding password to the GetUser query — causes a schema error.

GraphQL schema error when querying unsupported field

LLM Hacking

Spotted a JS file: /assets/js/chat-widget.js

const response = await fetch("/api/v2/chat", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ message }),
});

Initially thought the chat button was hidden. Turns out I'm just blind — bottom-right of the screen.

Chat widget button in bottom-right corner of the page

Looks like it's powered by an LLM?

Chat interface powered by an LLM assistantInitial interaction attempts with the LLM chat assistant

I start with usual prompts, asking it to forget all previous instructions and give me the flag, convincing it to give me it's original prompt etc. Remembering the Portswigger LLM labs, I ask it what APIs it has access to.

LLM response indicating access to internal APIs

It says it does, so I try to convince it to leak reports, change report status etc. I'll not paste all the responses here, but so far it gives me nothing.

“Nice try! The details of specific endpoints are part of the challenge.”

Eventually though…

LLM response after successful privilege escalation prompt

Realised the goal is to convince it I'm Adam Langley.

LLM revealing bonus flag after impersonation

That got me flag 6!

Flag: flag_6{9c2001f18f3b997187c9eb6d8c96ba60}

Turns out flag 6 was a bonus — back to enumeration.

Robots.txt

Found /robots.txt:

/internal-dash

Flag: flag_1{858c82dc956f35dd1a30c4d47bcb57fb}

Discovered /internal-dash/login shortly after. Tried fuzzing more stuff inside it — not much yet.

Reviewed GraphQL schema again, tried loading it in Voyager — didn't get anything new.

Missed Flags (Summary)

Didn't finish the challenge, but looked up the paths for 2, 3, and 5 afterward. For the sake of completeness, here's what was missed.

Flag 2

Remember the openjdk:19-jdk:bountyapi.war we found in the API response? Since this hinted a Java application, we should have tried /api/v1/actuator. It would trigger a WAF, which we could bypass by URL encoding. We'd get flag 2, alone with an new endpoint to explore: /actuator/heapdump

Flag 3

Using the new heapdump endpoint, we'd see a request from Inti on a new endpoint: /api/v1/internal-dashboard/token including an auth token:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImludGkifQ.YeqvfQ7L25ohhwBE5Tpmqo2_5MhqyOCXE7T9bG895Uk

We can make a POST request to this endpoint, and it will create us an Internal dashboard token: a1c2860d05f004f9ac6b0626277b1c36e0d30d66bb168f0a56a53ce12f3f0f7a

API response returning internal dashboard token

From here, we go to the /internal-dash we discovered earlier and /logout, identifying a new token: int-token.

Internal dashboard logout page exposing int-token

Now we just update that token with the one we recovered and return to the /internal-dash.

Internal dashboard showing restricted administrative content

There's flag 3, one more to go!

Flag 5

On /internal-dash/, the "lookup report" feature is vulnerable to a second-order path traversal.

Submitting ../ reveals two hidden endpoints:

/search
/my-reports

Since we already have STÖK's user_id, we can get their reports:

/api/v2/reports?user_id=15ee453d-18c7-419b-a3a6-ef8f2cc1271f

One of them is the Yahoo report — grab the ID:

c03dd42e-d929-4a50-9a8e-1ab6b2dd5e8a

Now use /search with that ID:

../search?q=c03dd42e-d929-4a50-9a8e-1ab6b2dd5e8a

That returns a change_hash:

2ea965e581705b5678fb9c95ab743b0b

Use it to change the status of STÖK's report via:

POST /internal-dash/api/report/status
{
    "id": "c03dd42e-d929-4a50-9a8e-1ab6b2dd5e8a",
    "status": "DUPLICATED",
    "change_hash": "2ea965e581705b5678fb9c95ab743b0b"
}

Do the same with your own report (ID: f9aa28ef-7008-424e-86fb-4271b131b155) to get a change hash.

Then promote yourself to confirmed:

{
    "id": "f9aa28ef-7008-424e-86fb-4271b131b155",
    "status": "CONFIRMED",
    "change_hash": "da80eece7d2e15e210ebf02fc94b072e"
}

After that, check your dashboard — the flag is in the title of your report status.