> ## Documentation Index
> Fetch the complete documentation index at: https://writeups.dudji.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Vantage

# Vantage - HTB Sherlock Writeup (DFIR / Network Forensics)

<img src="https://mintcdn.com/dudji-2c6e4328/iS0VVf3n3y9Vpti2/images/sherlocks/vantage.png?fit=max&auto=format&n=iS0VVf3n3y9Vpti2&q=85&s=14150afa32f2d4afba4ef6d7eff20b9c" alt="Vantage Banner" width="1536" height="1024" data-path="images/sherlocks/vantage.png" />

## Challenge Description

> A small company moved some of its resources to a private cloud installation. The developers left
> the redirect to the dashboard on their web server. The security team got an email from the alleged
> attacker stating that the user data was leaked. It is up to you to investigate the situation.

**Difficulty:** Very Easy\
**Category:** DFIR / Network Forensics\
**Evidence Files:** `web-server_2025-07-01.pcap` (15 MB), `controller_2025-07-01.pcap` (6.9 MB)

***

## Initial Analysis Methodology

Before diving into the challenge questions, the goal is to understand what happened using only the
evidence. This section walks through a repeatable approach you can apply to any unknown PCAP - the
same steps, in the same order, every time.

We have two PCAP files from two different machines. We don't know yet what those machines are, how
they relate to each other, or what happened. The methodology below will answer all three.

***

### Step 1 - Protocol Hierarchy: What kind of machines are these?

The very first thing to do with any unknown PCAP is find out what protocols it contains. This tells
you what kind of machine was captured and what kind of activity to expect - before you read a single
packet.

**Wireshark:** `Statistics > Protocol Hierarchy`

#### 📁 web-server\_2025-07-01.pcap

Open the web-server PCAP first and go to `Statistics > Protocol Hierarchy`. Here's what stands out:

| Protocol              | Frames | What it tells you                                      |
| --------------------- | ------ | ------------------------------------------------------ |
| TCP                   | 21,586 | Nearly all traffic is TCP-based                        |
| HTTP                  | 7,482  | A web server - this is almost entirely HTTP            |
| HTML Form URL Encoded | 8      | **Only 8 form submissions out of 7,482 HTTP requests** |

**Why the 8 form submissions matter:** The content type `application/x-www-form-urlencoded` is what
browsers send when you submit a login form via POST. Out of 7,482 HTTP requests, only 8 were form
POSTs - meaning the vast majority of traffic was automated GETs (scanning/fuzzing), and only a
handful were deliberate human interactions (login attempts). That ratio is the first red flag, and
you've spotted it without reading a single packet.

**Why 8 and not 4?** You might expect 4 login attempts = 4 POSTs. The count is doubled because this
machine acts as a **reverse proxy**: it captures both the external POST from the attacker
(`117.200.21.26 to 157.230.81.229`) and the internally forwarded POST from proxy to backend app
(`10.116.0.3 to 10.116.0.4`). Each login attempt is seen twice in the one capture file. This matters
in Step 5 when we filter for login packets.

#### 📁 controller\_2025-07-01.pcap

Now open the controller PCAP and check the same view:

| Protocol           | Frames | What it tells you                  |
| ------------------ | ------ | ---------------------------------- |
| HTTP + JSON        | 626    | REST API traffic                   |
| MySQL              | 8,978  | Database backend                   |
| AMQP               | 849    | Message queue (RabbitMQ)           |
| Data on port 2379  | 1,418  | etcd - distributed key-value store |
| Data on port 11211 | -      | memcached - in-memory cache        |

**What can we say from this?** The protocol hierarchy alone does *not* tell you what platform this
machine runs. HTTP + MySQL + AMQP + etcd + memcached could be many things - a Django app with Celery
workers, a Kubernetes control plane, or any message-driven platform with a database backend.

What it *does* tell you is the general shape of this machine: it's an **application controller
node** - something that exposes a REST API, stores state in a relational database, coordinates work
through a message queue, and uses an in-memory cache. That's a useful narrowing. The actual platform
will become clear in Step 3 when we read the HTTP request paths.

**The takeaway after Step 1:** We have a *web server / reverse proxy* (almost entirely HTTP) and a
*multi-service controller node* (API + database + message queue + cache). The web-server PCAP has the
suspicious traffic volume, so that's where we focus next.

***

### Step 2 - Endpoints & Conversations: Who's talking, and how much?

Now we have a rough picture of each machine. Next question: *who is communicating with them*, and is
any of that traffic abnormal?

There are two related views in Wireshark that answer this differently:

* **`Statistics > Endpoints > IPv4`** - One row per IP, total packets aggregated across all
  connections. Best for quickly identifying dominant talkers.
* **`Statistics > Conversations > TCP`** - One row per TCP stream (unique source port to destination
  port pair). Best for understanding *how* the traffic is structured.

**Start with Endpoints for triage, then drill into Conversations for detail.**

#### 📁 web-server\_2025-07-01.pcap - Endpoints view

Open `Statistics > Endpoints > IPv4 tab` and sort by the **Packets** column (descending):

| Address           | Packets    | % of total | Role                      |
| ----------------- | ---------- | ---------- | ------------------------- |
| 157.230.81.229    | 21,348     | -          | Web server (this machine) |
| **117.200.21.26** | **20,897** | **97.9%**  | **??? (suspicious)**      |
| 10.116.0.3        | 256        | 1.2%       | Internal IP               |
| 10.116.0.4        | 256        | 1.2%       | Internal IP               |
| 60+ other IPs     | 2–243 each | under 1%   | Internet background noise |

**The red flag is immediate:** One external IP - `117.200.21.26` - accounts for 97.9% of all
traffic. Every other external IP has negligible volume. That ratio alone identifies the attacker
before you've looked at a single HTTP request.

#### 📁 web-server\_2025-07-01.pcap - Conversations view

Now switch to `Statistics > Conversations > TCP tab` to understand how that traffic is structured.
You'll see approximately **187 rows** from `117.200.21.26`, all connecting to `157.230.81.229:80`,
each from a different source port:

| Address A               | Direction | Address B         | Packets | Duration |
| ----------------------- | --------- | ----------------- | ------- | -------- |
| 117.200.21.26:**64728** | to        | 157.230.81.229:80 | 497     | 22.6s    |
| 117.200.21.26:**64727** | to        | 157.230.81.229:80 | 489     | 22.7s    |
| 117.200.21.26:**64756** | to        | 157.230.81.229:80 | 487     | 22.6s    |
| *... 184 more rows ...* |           |                   |         |          |

**What this pattern means:** 187 parallel TCP connections, all nearly identical in duration (\~22
seconds) and packet count (\~480–500), all hitting port 80. This is not a human browsing - this is a
*tool running concurrent threads*. Each thread opens its own TCP connection to fire HTTP requests in
parallel. Tools like ffuf, gobuster, and dirsearch all behave exactly this way.

There are also \~10 smaller streams between `10.116.0.3` and `10.116.0.4:80`. These two internal IPs
have nearly identical packet counts in the Endpoints view, and their timing aligns with the external
wave - this is the hallmark of a reverse proxy chain (e.g., Nginx or Apache fronting a backend app
server).

#### 📁 controller\_2025-07-01.pcap - What about this one?

We'll come back to the controller PCAP after we understand the web-server traffic. The methodology
is: **follow the attacker's footsteps in order**. The web server is where the attack started, the
controller is where it escalated. Analysing them chronologically tells a coherent story.

***

### Step 3 - Filter HTTP Requests: What was the attacker doing?

We've identified `117.200.21.26` as suspicious. Now let's see *exactly* what they requested.

**Wireshark display filter (in `web-server_2025-07-01.pcap`):**

```
http.request && ip.src == 117.200.21.26
```

**Tip:** To make the packet list more readable, add custom columns. Right-click any column header >
`Column Preferences` > add:

* `http.host` (titled "Host")
* `http.request.uri` (titled "URI")
* `http.user_agent` (titled "User-Agent")

With those columns visible, the pattern jumps out immediately:

| #     | Host                        | Method | URI                    |
| ----- | --------------------------- | ------ | ---------------------- |
| 1024  | 1.vantage.tech              | GET    | /                      |
| 1031  | 2.vantage.tech              | GET    | /                      |
| 1038  | a.vantage.tech              | GET    | /                      |
| 1052  | abc.vantage.tech            | GET    | /                      |
| ...   | *3,579 unique subdomains*   | GET    | /                      |
| 1072  | cloud.vantage.tech          | GET    | /                      |
| -     | *(User-Agent changes here)* |        |                        |
| 18201 | cloud.vantage.tech          | GET    | /dashboard/            |
| 18215 | cloud.vantage.tech          | GET    | /dashboard/auth/login/ |
| 18240 | cloud.vantage.tech          | POST   | /dashboard/auth/login/ |

**Two distinct phases are visible:**

1. **Fuzzing phase:** Thousands of GETs, each to a different `*.vantage.tech` subdomain, all
   requesting `/`. The Host header cycles through a wordlist alphabetically. This is subdomain
   enumeration.
2. **Exploitation phase:** The Host locks onto `cloud.vantage.tech` and the attacker starts browsing
   the dashboard manually - navigating to the login page, submitting POST requests.

**And here's where the platform reveals itself.** While scrolling through the exploitation-phase
requests, you'll spot URIs like `/dashboard/i18n/js/horizon+openstack_dashboard/`. That path
literally contains the words `horizon` (OpenStack's web dashboard) and `openstack_dashboard`.
Remember the controller PCAP from Step 1 that we couldn't identify beyond "application controller
node"? Now we know - this is an **OpenStack** deployment. The web server is fronting an OpenStack
Horizon dashboard, and the controller is the OpenStack control plane behind it.

The transition between phases is also visible in the User-Agent column (which we'll examine next).

***

### Step 4 - Check User-Agents: What tools were used?

User-Agent strings are one of the quickest wins in web traffic analysis. Legitimate browsers all send
`Mozilla/5.0...` headers. Attack tools often identify themselves - either by name or by sending
unusual/missing User-Agents.

**Wireshark (in `web-server_2025-07-01.pcap`):** Click any packet from the fuzzing phase, expand
`Hypertext Transfer Protocol > User-Agent` in the packet detail pane.

Two User-Agents appear from the attacker's IP:

| Phase                | User-Agent                                                                                             | Meaning                                |
| -------------------- | ------------------------------------------------------------------------------------------------------ | -------------------------------------- |
| Fuzzing (3,695 reqs) | `Fuzz Faster U Fool v2.1.0-dev`                                                                        | **ffuf** - a Go-based web fuzzing tool |
| Manual browsing      | `Mozilla/5.0 (Macintosh Intel Mac OS X 10_15_7) AppleWebKit/537.36 ... Chrome/137.0.0.0 Safari/537.36` | Real Chrome browser on macOS           |

**The pivot moment:** The switch from ffuf to Chrome is the exact moment the attacker discovered
`cloud.vantage.tech` returned something interesting (the Horizon dashboard) and decided to explore it
manually.

To filter for just the non-browser traffic:

```
http.user_agent && !(http.user_agent contains "Mozilla")
```

This isolates every request made by tools rather than browsers - useful for quickly spotting
automated activity in any PCAP.

***

### Step 5 - Follow TCP Streams: Read the full conversations

So far we've been looking at metadata - source IPs, URIs, User-Agents. Now we need to see the actual
*content* of the most interesting requests: what credentials were submitted, what files were
downloaded, what the server responded with.

#### Reading the login attempts

**Wireshark filter (in `web-server_2025-07-01.pcap`):**

```
http.request.method == POST && http.request.uri contains "/dashboard/auth/login" && ip.src == 117.200.21.26
```

> **Why the `ip.src` clause is essential:** Without it, this filter returns **8 packets, not 4**.
> Because this machine is a reverse proxy, every inbound request from the attacker
> (`117.200.21.26 → 157.230.81.229`) is immediately forwarded internally to the backend app server
> (`10.116.0.3 → 10.116.0.4`). Both hops are captured in this one PCAP file, so each login attempt
> appears twice. Adding `ip.src == 117.200.21.26` limits results to the original attacker-side
> packets only, giving you the complete request bodies.

This gives you exactly **4 POST packets** - one per login attempt. Click the first one, then
right-click > **Follow > TCP Stream**.

**What you see in the stream window:** Wireshark colours the two sides of the conversation
differently. By default, the client's data (the attacker's request) is shown in **red** and the
server's response in **blue**. The stream shows everything in one continuous scroll: the HTTP
request headers, then a blank line, then the POST body, then the server's response headers, then
the response body.

The POST body you will see (line-wrapped here for readability):

```
csrfmiddlewaretoken=UX4dve2DwuDo171EEgnpZaxni395EA5ijYsOoOoHwPUbGqAOKcR7Y4qns2yWIb74
&fake_email=
&fake_password=
&next=%2Fdashboard%2F
&region=default
&username=admin
&password=admin
```

**What each field means:**

* `csrfmiddlewaretoken` - Django's Cross-Site Request Forgery protection token. It rotates with
  every new session and is not useful for our investigation. Ignore it.
* `fake_email` / `fake_password` - Honeypot fields that OpenStack Horizon adds to its login form
  to catch naive credential-stuffing bots. A real browser always leaves them empty. Ignore them.
* `username` / `password` - The credentials you're looking for.
* `region` - The OpenStack region name, always `default` here.

**How to tell success from failure:** The server's response appears in blue in the same stream
window, immediately below the request. You do not need to read the HTML body. Just look at the
first line of the response:

* **`HTTP/1.1 200 OK`**: the server re-rendered the login page. **Login failed.**
* **`HTTP/1.1 302 Found`** with `Location: /dashboard/`: the server redirected to the dashboard.
  **Login succeeded.**

Repeat this for all four POST packets to build the full picture:

| Attempt  | Credentials               | Response  | Result  |
| -------- | ------------------------- | --------- | ------- |
| 09:39:16 | admin / admin             | 200 OK    | Failed  |
| 09:39:27 | demo / demo               | 200 OK    | Failed  |
| 09:39:33 | root / root               | 200 OK    | Failed  |
| 09:40:07 | admin / StrongAdminSecret | 302 Found | Success |

#### Reading the OpenRC file download

**Filter (in `web-server_2025-07-01.pcap`):**

```
http.request.uri contains "openrc" && ip.src == 117.200.21.26
```

One packet matches. Click it, then right-click > **Follow > TCP Stream**.

The request headers are clearly readable in red. You can see the GET to
`/dashboard/project/api_access/openrc/`. Scrolling down to the blue response, the headers confirm:

```
Content-Disposition: attachment; filename="admin-openrc.sh"
Content-Encoding: gzip
```

After the headers, the response body is binary garbage (compressed bytes that Wireshark cannot
display as text). **Do not try to read the file content from the TCP stream window.** This is the
most common point of confusion for this task.

To read the actual file, use Wireshark's HTTP object export, which automatically decompresses the
response:

1. Go to **File > Export Objects > HTTP...**
2. Find the row with `admin-openrc.sh` in the Filename column.
3. Click **Save**, then open the file in a text editor.

The decompressed content confirms everything we've identified so far:

```bash theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
export OS_AUTH_URL=http://134.209.71.220/identity
export OS_PROJECT_ID=9fb84977ff7c4a0baf0d5dbb57e235c7
export OS_PROJECT_NAME="admin"
export OS_USERNAME="admin"
export OS_REGION_NAME="RegionOne"
export OS_IDENTITY_API_VERSION=3
```

This file is the **bridge between the two PCAPs**: it gives the attacker the controller's direct IP
address (`134.209.71.220`) and everything needed to authenticate to the OpenStack API without going
through the web dashboard. Everything that happens in the controller PCAP after this moment is a
consequence of this one download.

***

### Step 6 - Cross-Reference PCAPs: Connect the timeline

This is where the two PCAP files merge into a single story. The question is: *does the activity in
the web-server PCAP lead to activity in the controller PCAP, and can we prove it?*

**Method:** Compare timestamps. Look at when the last meaningful action occurs in the web-server
file, then look at when new activity begins in the controller file.

#### 📁 web-server\_2025-07-01.pcap: Last meaningful action

The attacker's last request in this file is the OpenRC file download at **09:40:29 UTC**. After
that there is no more attacker-initiated traffic in the web-server PCAP.

#### 📁 controller\_2025-07-01.pcap: What's in this file before the attacker arrives?

Open the controller PCAP and apply:

```
http.request && ip.src == 117.200.21.26
```

Before `09:41:44` there are zero results. The attacker has no presence in this file yet.

Without the filter, the controller PCAP contains only internal background traffic, just the OpenStack
services talking to each other. You'll see periodic `POST /v3/lease/keepalive` requests from
`134.209.71.220` to itself on port 2379 (etcd, a key-value store used internally by OpenStack).
These are automated heartbeats with no relation to the attacker. Everything else is MySQL and AMQP
traffic between backend services. There is no Horizon dashboard traffic in this file.

#### The pivot moment

The first packet in the controller PCAP with the attacker's IP is at **09:41:44 UTC**:

```
GET /identity HTTP/1.1
Host: 134.209.71.220
User-Agent: openstacksdk/4.6.0 keystoneauth1/5.11.1 python-requests/2.32.4 CPython/3.13.5
```

This is the OpenStack CLI's initial service discovery call, which is the first thing it does every time you
run any `openstack` command. The attacker is no longer using a browser. They are now running
command-line tools directly against the controller using the credentials from the OpenRC file.

**The 75-second gap** between 09:40:29 (OpenRC download) and 09:41:44 (first CLI call) is exactly
the time it takes to: open a terminal, run `source admin-openrc.sh`, enter the password when
prompted, and type the first `openstack` command. The causal chain is unambiguous.

#### What the attacker sequence looks like in the controller PCAP

Apply this filter to isolate only the attacker's CLI activity:

```
http.request && http.user_agent contains "openstacksdk"
```

The full sequence of commands plays out in order, with timestamps 30–90 seconds apart, at the pace
of a human exploring an environment manually:

| Time (UTC) | Request                                               | What the attacker ran        |
| ---------- | ----------------------------------------------------- | ---------------------------- |
| 09:41:44   | `GET /identity`                                       | First CLI command, discovery |
| 09:41:45   | `POST /identity/v3/auth/tokens`                       | Authentication               |
| 09:41:45   | `GET /identity/v3/services`                           | List all services            |
| 09:42:11   | `GET /identity/v3/users`                              | List all users               |
| 09:42:27   | `GET /compute/v2.1/servers/detail`                    | List VMs                     |
| 09:43:27   | `GET /v1/AUTH_9fb84977...?format=json`                | List Swift containers        |
| 09:43:47   | `GET /v1/AUTH_.../user-data?format=json`              | List user-data contents      |
| 09:44:05   | `GET /v1/AUTH_.../employee-data?format=json`          | List employee-data contents  |
| 09:45:23   | `GET /v1/AUTH_.../user-data/user-details.csv`         | Download user data           |
| 09:45:47   | `GET /v1/AUTH_.../employee-data/employee-details.csv` | Download employee data       |
| 09:48:02   | `POST /identity/v3/users`                             | Create backdoor user         |
| 09:49:15   | `PUT /identity/v3/projects/.../users/.../roles/...`   | Grant admin role             |

Each row is a separate `openstack` CLI command. The fact that every request carries the same
`openstacksdk` User-Agent and the same auth token confirms they all belong to the same session.

***

### Methodology Summary

Every time you open an unknown PCAP, follow this sequence:

1. **Protocol Hierarchy** tells you *what kind of machine* this is
2. **Endpoints** tell you *who's suspicious* (disproportionate traffic volume)
3. **Conversations** tell you *how* the traffic is structured (parallelism, ports)
4. **Build protocol-specific filters** tell you *what they did* (HTTP paths, DNS queries, SMB
   shares, RDP sessions, etc.)
5. **Inspect client/application metadata** tells you *what tool or app generated the traffic*
   (User-Agent strings, protocol banners, client names, request patterns)
6. **Reconstruct key sessions and relevant artifacts** tell you *the full content and sequence* of
   interesting exchanges (`Follow TCP Stream`, `File > Export Objects > HTTP` for compressed
   responses, key request/response bodies)
7. **Cross-reference timestamps across hosts/files** connects activity into one timeline and
   separates cause from effect

Each step narrows your focus. You go from "I have 24,000 packets" to a complete narrative of the
attack, even when the protocol is not HTTP.

***

## Attack Timeline

| Time (UTC)  | Source PCAP | Event                                                                       |
| ----------- | ----------- | --------------------------------------------------------------------------- |
| 09:38–09:40 | web-server  | Subdomain fuzzing with ffuf (3,579 subdomains tested, 3,695 total requests) |
| 09:39:07    | web-server  | Attacker discovers `cloud.vantage.tech`, switches to Chrome                 |
| 09:39:16    | web-server  | Login attempt: admin / admin failed (HTTP 200)                              |
| 09:39:27    | web-server  | Login attempt: demo / demo failed (HTTP 200)                                |
| 09:39:33    | web-server  | Login attempt: root / root failed (HTTP 200)                                |
| 09:40:07    | web-server  | Login attempt: admin / StrongAdminSecret succeeded (HTTP 302)               |
| 09:40:29    | web-server  | Downloads admin-openrc.sh from API Access page                              |
|             |             | *- 75-second gap: attacker reads file, sources it, enters password -*       |
| 09:41:44    | controller  | First direct API call - OpenStack CLI service discovery (`GET /identity`)   |
| 09:41:45    | controller  | Enumerates services and endpoints via Keystone                              |
| 09:42:11    | controller  | Lists all users via Keystone (`GET /identity/v3/users`)                     |
| 09:42:27    | controller  | Lists compute servers (`GET /compute/v2.1/servers/detail`)                  |
| 09:43:27    | controller  | Lists Swift containers - finds 3: dev-files, employee-data, user-data       |
| 09:43:47    | controller  | Lists contents of user-data container                                       |
| 09:44:05    | controller  | Lists contents of employee-data container                                   |
| 09:45:23    | controller  | Downloads user-details.csv (28 PII records)                                 |
| 09:45:47    | controller  | Downloads employee-details.csv                                              |
| 09:48:02    | controller  | Creates backdoor user "jellibean" with password "P@\$\$word"                |
| 09:49:15    | controller  | Grants jellibean admin role on the project                                  |

***

## Questions & Answers

### Task 1: What tool did the attacker use to fuzz the web server?

**PCAP:** `web-server_2025-07-01.pcap`\
**Filter:** `http.request && ip.src == 117.200.21.26`\
**Where to look:** Click any packet from the early GETs to expand `Hypertext Transfer Protocol >
User-Agent` in the packet detail pane.

The User-Agent field reads `Fuzz Faster U Fool v2.1.0-dev` - this is ffuf, a popular web fuzzing
tool written in Go.

> **Answer:** `ffuf@2.1.0-dev`

***

### Task 2: Which subdomain did the attacker discover?

**PCAP:** `web-server_2025-07-01.pcap`\
**Filter:** `http.request && ip.src == 117.200.21.26 && http.user_agent contains "Chrome"`

After the ffuf scan, the attacker switched to Chrome and manually browsed to the discovered
subdomain. Every Chrome request targets the same Host header value.

> **Answer:** `cloud.vantage.tech`

***

### Task 3: How many login attempts did the attacker make before successfully logging in?

**PCAP:** `web-server_2025-07-01.pcap`\
**Filter:** `http.request.method == POST && http.request.uri contains "/dashboard/auth/login" && ip.src == 117.200.21.26`

Four POST packets appear. The response codes distinguish success from failure: HTTP 200 means the
login page was re-rendered (failed), while HTTP 302 means authentication succeeded and the server is
redirecting to the dashboard. The first three returned 200, and the fourth returned 302.

| Attempt      | Credentials               | Response  | Result  |
| ------------ | ------------------------- | --------- | ------- |
| 1 (09:39:16) | admin : admin             | 200 OK    | Failed  |
| 2 (09:39:27) | demo : demo               | 200 OK    | Failed  |
| 3 (09:39:33) | root : root               | 200 OK    | Failed  |
| 4 (09:40:07) | admin : StrongAdminSecret | 302 Found | Success |

> **Answer:** `3`

***

### Task 4: When did the attacker download the OpenStack API remote access config file?

**PCAP:** `web-server_2025-07-01.pcap`\
**Filter:** `http.request.uri contains "openrc" && ip.src == 117.200.21.26`

The OpenRC file is a shell script that sets environment variables for OpenStack CLI authentication.
The attacker downloaded it from `Project > API Access > Download OpenStack RC File` in the Horizon
dashboard. The response header confirms the filename in `Content-Disposition` as `admin-openrc.sh`.

To read the file content itself, use **File > Export Objects > HTTP** (the response is
gzip-compressed and unreadable in the Follow TCP Stream window). The decompressed file contains the
auth URL (`http://134.209.71.220/identity`), the project ID, and the username - everything needed to
pivot from the web server to direct API access on the controller.

> **Answer:** `2025-07-01 09:40:29`

***

### Task 5: When did the attacker first interact with the API on the controller node?

**PCAP:** `controller_2025-07-01.pcap`\
**Filter:** `http.request && http.user_agent contains "openstacksdk"`

Before 09:40:29, controller API traffic is mostly dashboard-generated background activity. After a
75-second gap (the attacker reading and sourcing the OpenRC file), the first request with the
OpenStack SDK User-Agent appears - `GET /identity` at **09:41:44 UTC** - which is the SDK's service
discovery call, always the first thing the CLI does when you run any `openstack` command.

> **Answer:** `2025-07-01 09:41:44`

***

### Task 6: What is the project ID of the default project accessed by the attacker?

**PCAP:** Both - appears in `web-server` (OpenRC file) and throughout `controller` (API URLs)\
**Easiest source:** Export the OpenRC file via File > Export Objects > HTTP (Task 4)

```bash theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
export OS_PROJECT_ID=9fb84977ff7c4a0baf0d5dbb57e235c7
```

It also appears embedded in dozens of API URLs throughout the controller PCAP: compute usage
queries, Swift storage paths (`/v1/AUTH_9fb84977...`), and networking quota requests.

> **Answer:** `9fb84977ff7c4a0baf0d5dbb57e235c7`

***

### Task 7: Which OpenStack service provides authentication and authorization?

**PCAP:** `controller_2025-07-01.pcap`\
**Filter:** `http.request.uri == "/identity/v3/services" && http.user_agent contains "openstacksdk"`

Click the matching packet and follow the TCP stream (`Follow > TCP Stream`). The response body is a
JSON service catalog. The entry with `"type": "identity"` is **Keystone** - OpenStack's
authentication and authorization service. Every API call hitting the `/identity/` path in the
controller PCAP is talking to Keystone.

The relevant JSON entry:

```json theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
{"name": "keystone", "type": "identity", "enabled": true, ...}
```

> **Answer:** `Keystone`

***

### Task 8: What is the endpoint URL of the Swift service?

**PCAP:** `controller_2025-07-01.pcap`\
**Filter:** `http.request.uri == "/identity/v3/endpoints" && http.user_agent contains "openstacksdk"`

Follow the TCP stream on the matching packet. The response body is a JSON endpoint catalog listing
all service endpoints. Find the entries with `service_id` matching the Swift service
(`f9194820052d4788b09157bf0a0dfdd0`). The public-interface endpoint URL is a template:

```
http://134.209.71.220:8080/v1/AUTH_$(project_id)s
```

When the `$(project_id)s` template variable is resolved using the admin project ID from the OpenRC
file, the full URL becomes:

> **Answer:** `http://134.209.71.220:8080/v1/AUTH_9fb84977ff7c4a0baf0d5dbb57e235c7`

***

### Task 9: How many containers were discovered by the attacker?

**PCAP:** `controller_2025-07-01.pcap`\
**Filter:** `http.request.uri contains "AUTH_9fb84977ff7c4a0baf0d5dbb57e235c7?format=json" && ip.src == 117.200.21.26`

One packet matches: the attacker's GET to the Swift proxy at port 8080 listing all containers.
Follow the TCP stream. The response includes:

* `X-Account-Container-Count: 3` in the headers (confirms the count)
* A JSON body listing all three containers:

```json theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
[
  { "name": "dev-files", "count": 0, "bytes": 0 },
  { "name": "employee-data", "count": 0, "bytes": 0 },
  { "name": "user-data", "count": 0, "bytes": 0 }
]
```

> **Answer:** `3`

***

### Task 10: When did the attacker download the sensitive user data file?

**PCAP:** `controller_2025-07-01.pcap`\
**Filter:** `http.request.uri contains "user-details.csv" && ip.src == 117.200.21.26`

One packet matches: the attacker's GET to the Swift proxy at port 8080. (You may also see an
internal forwarding request from the Swift proxy to the object storage server on a path beginning
with `/sdb1/` - filter by `ip.src == 117.200.21.26` to see only the original request.)

> **Answer:** `2025-07-01 09:45:23`

***

### Task 11: How many user records are in the sensitive user data file?

**PCAP:** `controller_2025-07-01.pcap`\
**Method:** Follow the TCP stream on the `user-details.csv` GET packet from Task 10.

The response headers confirm:

```
Content-Type: text/csv
Content-Length: 1367
```

The response body is the file itself - readable plaintext (Swift object storage does not compress
responses). The file contains:

* A header row: `Full Name,Email,Phone Number`
* 28 data records (John Doe through Zoe Lee), each containing full name, email address, and phone
  number - personally identifiable information.

> **Answer:** `28`

***

### Task 12: What is the username of the new user created for persistence?

**PCAP:** `controller_2025-07-01.pcap`\
**Filter:** `http.request.method == POST && http.request.uri == "/identity/v3/users"`

Follow the TCP stream. The request body is a JSON payload sent to Keystone:

```json theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
{
  "user": {
    "name": "jellibean",
    "password": "P@$$word",
    "enabled": true,
    "default_project_id": "9fb84977ff7c4a0baf0d5dbb57e235c7"
  }
}
```

The server responded with `HTTP 201 CREATED`, confirming the account was successfully created. A
subsequent `PUT` request at 09:49:15 then granted this user the admin role on the project.

> **Answer:** `jellibean`

***

### Task 13: What is the password of the new user?

**PCAP:** `controller_2025-07-01.pcap`\
**Method:** Same TCP stream as Task 12 - the password is in the JSON request body in cleartext.

> **Answer:** `P@$$word`

***

### Task 14: What is the MITRE ATT\&CK technique ID for the persistence method?

The attacker created a new user account in a cloud platform (OpenStack Keystone) and granted it
administrative privileges. This maps to:

* **Tactic:** Persistence
* **Technique:** T1136 - Create Account
* **Sub-technique:** .003 - Cloud Account

T1136.003 specifically covers adversaries creating accounts in cloud environments (AWS IAM, Azure AD,
GCP IAM, OpenStack Keystone) to maintain persistent access, distinguishing it from local account
creation (.001) or domain account creation (.002).

> **Answer:** `T1136.003`

***

## Key Wireshark Filters Reference

| Purpose                         | PCAP       | Display Filter                                                                                |
| ------------------------------- | ---------- | --------------------------------------------------------------------------------------------- |
| All attacker HTTP requests      | web-server | `http.request && ip.src == 117.200.21.26`                                                     |
| Attacker fuzzing traffic only   | web-server | `http.request && ip.src == 117.200.21.26 && http.user_agent contains "Fuzz"`                  |
| Attacker browser traffic only   | web-server | `http.request && ip.src == 117.200.21.26 && http.user_agent contains "Chrome"`                |
| Login attempts (attacker only)  | web-server | `http.request.method == POST && http.request.uri contains "login" && ip.src == 117.200.21.26` |
| OpenRC download (attacker only) | web-server | `http.request.uri contains "openrc" && ip.src == 117.200.21.26`                               |
| Direct OpenStack CLI calls      | controller | `http.request && http.user_agent contains "openstacksdk"`                                     |
| Keystone API calls              | controller | `http.request.uri contains "/identity/v3"`                                                    |
| Swift account listing           | controller | `http.request.uri contains "AUTH_" && http.request.uri contains "format=json"`                |
| CSV downloads (attacker)        | controller | `http.request.uri contains ".csv" && ip.src == 117.200.21.26`                                 |
| User creation                   | controller | `http.request.method == POST && http.request.uri == "/identity/v3/users"`                     |
| Role assignment                 | controller | `http.request.method == PUT && http.request.uri contains "roles"`                             |

***

## Tools Identified

| Tool          | Version   | User-Agent String                                                                                      | Purpose                               |
| ------------- | --------- | ------------------------------------------------------------------------------------------------------ | ------------------------------------- |
| ffuf          | 2.1.0-dev | `Fuzz Faster U Fool v2.1.0-dev`                                                                        | Subdomain enumeration                 |
| Chrome        | 137.0.0.0 | `Mozilla/5.0 (Macintosh Intel Mac OS X 10_15_7) AppleWebKit/537.36 ... Chrome/137.0.0.0 Safari/537.36` | Manual dashboard exploitation         |
| OpenStack SDK | 4.6.0     | `openstacksdk/4.6.0 keystoneauth1/5.11.1 python-requests/2.32.4 CPython/3.13.5`                        | API enumeration and data exfiltration |

***

## Network Topology

```
Attacker (117.200.21.26) - macOS, Chrome 137, ffuf 2.1.0-dev
    |
    v  HTTP :80
Web Server / Reverse Proxy (157.230.81.229 / 10.116.0.3)
    |
    v  HTTP :80 (internal forwarding)
OpenStack Controller (134.209.71.220 / 10.116.0.4)
    |- Keystone (Identity)    - port 80  /identity
    |- Nova (Compute)         - port 80  /compute
    |- Neutron (Networking)   - port 80  /networking
    |- Glance (Images)        - port 80  /image
    |- Cinder (Block Storage) - port 80  /volume
    |- Swift (Object Storage) - port 8080
    |- MySQL                  - port 3306 (localhost)
    |- RabbitMQ (AMQP)        - port 5672 (localhost)
    |- etcd                   - port 2379
    '- Memcached              - port 11211 (localhost)
```

***

## MITRE ATT\&CK Mapping

| Phase          | Technique                                      | Evidence                                                        |
| -------------- | ---------------------------------------------- | --------------------------------------------------------------- |
| Reconnaissance | T1595.003 - Active Scanning: Wordlist Scanning | ffuf subdomain enumeration (3,579 subdomains)                   |
| Initial Access | T1078.004 - Valid Accounts: Cloud Accounts     | Brute-forced admin:StrongAdminSecret on Horizon                 |
| Discovery      | T1580 - Cloud Infrastructure Discovery         | Enumerated services, endpoints, users, VMs, containers          |
| Collection     | T1530 - Data from Cloud Storage                | Downloaded user-details.csv and employee-details.csv from Swift |
| Persistence    | T1136.003 - Create Account: Cloud Account      | Created "jellibean" with admin role                             |

***

## Corrections from Previous Version

The following errors were identified by verifying each claim against the raw PCAP data and have been
corrected in this version:

| Section             | Previous (incorrect)                                                                                                | Corrected                                                                                                                      |
| ------------------- | ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| Step 1              | No explanation for why "8 form submissions" when there are only 4 login attempts                                    | Added explanation: the reverse proxy doubles every POST - both the external and internal hops are captured                     |
| Step 5 login filter | `http.request.method == POST && http.request.uri contains "/dashboard/auth/login"` then "Four POST requests appear" | Added `&& ip.src == 117.200.21.26`, without it the filter returns 8 packets and selecting the wrong one gives the wrong stream |
| Step 5 POST body    | Showed simplified `username=admin&password=admin&region=default`                                                    | Shows the real body: includes `csrfmiddlewaretoken`, `fake_email`, and `fake_password` fields                                  |
| Step 5 OpenRC       | Implied the file is readable in the Follow TCP Stream window                                                        | Corrected: response is gzip-compressed (binary in stream), use **File > Export Objects > HTTP** instead                        |
| Step 4 User-Agent   | Showed partial UA string `Chrome/137.0.0.0`                                                                         | Shows full UA string including macOS platform: `Mozilla/5.0 (Macintosh Intel Mac OS X 10_15_7) ...`                            |
| Attack Timeline     | `09:44:27` - "Lists contents of user-data and employee-data containers" (single entry)                              | Split into two correct entries: `09:43:47` (user-data) and `09:44:05` (employee-data)                                          |
| Attack Timeline     | `09:48:28` - "Grants jellibean admin role on the project"                                                           | Corrected to `09:49:15` - actual timestamp verified in the PCAP                                                                |
