Proving Grounds Practice —
MZEEAV (CTF-200–02)

Dpsypher
15 min readMar 2, 2024

--

This is an intermediate box on Offsec’s PG Practice and the community agrees that it is of intermediate difficulty.

Start with a Nmap scan:

sudo nmap -Pn -n $IP -sC -sV -p- --open

I start nearly every box this way because it quickly returns a wealth of information. Sudo as it defaults to the faster half-open SYN scan, then -Pn to ignore ping and assume it is up, -n to ignore DNS, the IP address, -sC for default scripts, -sV for version information, -p- to scan all ports, and MOST importantly the — open argument to apply scripts and version scans to found open ports only.

└─$ sudo nmap -Pn -n $IP -sC -sV -p- --open          
[sudo] password for kali:
Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-02-29 09:21 EST
Nmap scan report for 192.168.194.33
Host is up (0.090s latency).
Not shown: 63421 closed tcp ports (reset), 2112 filtered tcp ports (no-response)
Some closed ports may be reported as filtered due to --defeat-rst-ratelimit
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

When the TCP scan finishes, immediately run a UDP scan.

sudo nmap -Pn -n $IP -sU --top-ports=100 --reason

Even if it comes up empty, scanning the UDP ports as well is best practice. When it hits, it’s usually a big deal, maybe Simple Network Management Protocol (SNMP) is available. Note that I am only interested in the top 100 ports. UDP scans take a long time if we are not precise. Lastly we add — reason to understand why ports are returned as open, open|filtered, or closed.

Swing and a miss. This happens often but we have to check.

What do we see with our initial TCP Nmap scan?

Only two ports open, a web server on port 80 and SSH on port 22. It’s always a bit of a relief to see this output since we know that access will be though the Web server. In that sense the port determination is uncomplicated.

As we do, let’s start Gobuster against the web server first.

sudo gobuster dir -w '/home/kali/Desktop/wordlists/dirbuster/directory-list-2.3-medium.txt' -u http://$IP:9000 -t 42 -b 400,401,403,404 --no-error

I run it with 42 threads (-t 42) because it’s a good balance of speed and caution. If you go too fast, directories and files can be missed. Also, I’m not bug hunting, so I’m not going to dive deeply into fuzzing different 400 codes. They are just visual noise to me, so I eliminate them (-b 400,401,403,404). Lastly, I loathe seeing the ‘Time Exceeded’ errors so we eliminate them with — no-errors.

While this runs we looks at the other port (mostly for notes).

Port 22 — SSH, OpenSSH 8.4p1 Debian 5+deb11u2 (protocol 2.0)

SSH exploits are extremely rare and difficult to exploit if they exist (i.e. Terrapin). My process is to grab the type of accepted key in case it comes up later.

Port 80 — Web Server, Apache 2.4.56 ((Debian))

Let’s browse there now.

We are greeted with immediate upload functionality, gotta love that! The door is partially open already. I am not familiar with this tool, let’s see of we can find the source code on Github. The only time I’ve ever encountered the work ‘Mzee’ is on Words with Friends. It’s a tricky high-scoring word there.

Google was also uncharacteristically silent on this, I gather this is custom code or it was buried with the dead bodies on page two of the Google search.

Before I start playing with uploads, I would like to know what other technology the Apache Web Server is using.

No help here
Also useless.

Gobuster found some useful directories which we can look at in a minute but first I want to scan again with some file extensions to get a better grasp on the language used. Is this PHP? I add -x php,txt,html,pdf to search for likely file types.

sudo gobuster dir -w '/home/kali/Desktop/wordlists/dirbuster/directory-list-2.3-medium.txt' -u http://$IP:80 -t 42 -b 400,401,403,404 --no-error -x php,txt,html,pdf

Best practice is to also fire up Burp Suite in case we need to do some proxy manipulation of file extension or mime type. I’ll leave interception ‘off’ for now and set FoxyProxy to Burp. This will create a history for us if we need it.

Gobuster has already answered my primary question, this application does indeed use PHP. Also it discovered an important endpoint ‘listing.php’. I’m glad we ran this, I would not have know it was there otherwise.

I’m going to move on for now while this finishes in the background. It’s best practice to let it finish. You never know what may be lurking at the end of the list, it could be a zip.php endpoint.

We are going to investigate all of these end points now.

Then the source:

They give us the entire script logic and the two PHP endpoints.

Let’s ask Google Gemini what this is doing exactly:

My Prompt —

Speak with me as a cyber security professional. The template I am using for my website contains an upload script. What is it doing? Is it vulnerable and if so, how? Please explain every step in detail. Here is the script:

<script> 
async function uploadFile() {
let formData = new FormData();
formData.append("file", fileupload.files[0]);
await fetch('/upload.php',
{ method: "POST",
body: formData });
alert('The file has been uploaded successfully.');
window.location = "listing.php"; }
</script>

Gemini’s Answer —

Analyzing the website upload script:

Function:

This script defines an asynchronous function named uploadFile that handles file uploads to the server. Let's break down the steps:

let formData = new FormData();:

This line creates a new FormData object, which is specifically used to construct multipart/form-data requests suitable for file uploads.

formData.append("file", fileupload.files[0]);:

This line attaches the user-selected file to the FormData object.

"file": This is the key used to identify the uploaded file on the server-side script.

fileupload.files[0]: This refers to the first file selected by the user in the "fileupload" element (likely an HTML file input element).

await fetch('/upload.php', { method: "POST", body: formData });:

This line initiates a file upload request to the server using the fetch API.

/upload.php: This specifies the URL of the server-side script that handles the upload process.

method: "POST": This indicates that the request is a POST request, commonly used for submitting form data like file uploads.

body: formData: This sets the body of the request to the previously created formData object, containing the file data.

await: This keyword signifies that the function pauses execution at this point until the fetch operation completes (the file upload is finished).

alert('The file has been uploaded successfully.');:

This line displays an alert message to the user indicating successful file upload (assuming the server-side script processes the upload successfully).

window.location = "listing.php";:

This line redirects the user’s browser to the “listing.php” page after the upload is complete.

Vulnerabilities:

1. Insecure file type handling:

The script doesn’t perform any validation on the uploaded file type. This means a malicious user could potentially upload a script or executable file disguised as an image or document, posing a security risk.

2. Lack of input validation:

The script doesn’t validate the filename or any other user-provided data within the FormData object. This could allow attackers to inject malicious code or manipulate the server-side logic through techniques like directory traversal attacks.

3. Missing access control:

The script doesn’t implement any access control mechanisms to restrict who can upload files. This could allow unauthorized users to upload files to your website.

4. Potential for server-side vulnerabilities:

Even if the client-side script is secure, vulnerabilities in the server-side script that handles the uploaded file (/upload.php) could still be exploited.

Thanks Gemini!

Note that this is useful for learning to understand the code we encounter but as of this writing, the use of AI is not permitted on the OSCP exam.

We will explore how to weaponize this knowledge in a minute but for now let’s complete our checklist by looking at every endpiont.

Source page is empty.

It’s worthwhile to run a Gobuster against this directory. I am going to also look for PHP and HTML files types since we have encountered those elsewhere in the application.

Truly nothing to see here.

Moving on.

We see an error message when there is no file forwarded here from the script on /index.html

This is interesting. I wonder if all of these files can be directly accessed? If so, where are they? In the root directory or in /upload maybe? I did not search for file type EXE. We can make a note to try that in a second round of enumeration in the event that nothing else pans out. I check the source page here as a matter of completion but it is only HTML, not useful.

Well well well, what do we have here? A backup you say?

Okay good, let’s review our position. Our two most likely access potentialities are to exploit the upload logic with a PHP rev-shell and access it or find something valuable, like and SSH key, in the backup file which appears to be quite large. It makes the most sense to have all the information before we go exploiting, so I’m going to grab the ZIP file now.

It’s not even locked with a password and it turns out there were EXE files in the upload directory, so this is also a confirmation that uploaded file goes to the upload directory.

I know this seems obvious, but this type of step by step confirmation will serve you in the long run. In my opinion, this is what solid process looks like. The directory containing uploaded files will not always be so obviously named.

We can also guess by this that there may be a cron job running on the box that regularly creates a backup of /var/www/html/. We should look for that when we get access, if it’s being run as root, we could have our escalation path.

This is a good time to use my favorite grep command given to me by Krishna Tiwadi. It is good for download scenarios like this one where we have to sift through a lot of data.

grep -rinE '(password|username|user|pass|key|token|secret|admin|login|credentials)' /path/to/downloaded/files
Nothing of interest

Since we have this backup we can look at the exact logic of upload.php to see where it is vulnerable.

┌──(kali㉿kali)-[~/…/TEMP-publish/var/www/html]
└─$ 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);



?>

We can again ask Gemini about this file. Here what she said:

Analysis of upload.php

This script implements a basic check to block a specific type of file during the upload process. Here’s a breakdown:

Functionality:

Retrieving file information:

The script retrieves the uploaded file’s name ($filename) from the $_FILES superglobal variable.

Temporary storage:

It defines temporary ($tmp_location) and final ($location) storage locations for the uploaded file.

Moving the uploaded file:

The move_uploaded_file function is used to move the uploaded file from its temporary location to the designated temporary storage ($tmp_location).

Checking magic bytes:

The script opens the temporarily stored file ($tmp_location) and reads the first two bytes (fread(F, 2)) into a variable named $magic.

It then converts the raw bytes to a hexadecimal string using bin2hex($magic), extracts the first four characters (substr(..., 0, 4)), and converts them to uppercase (strtoupper(...)). This process essentially retrieves the "magic bytes" of the file, which are characteristic bytes that can identify the file type.

The script stores the extracted magic bytes in the $magicbytes variable.

It logs the magic bytes using error_log for potential debugging or monitoring purposes.

Blocking specific file type:

The script checks if the $magicbytes string does not contain the specific sequence '4D5A'. This sequence is the magic byte signature for Portable Executable (PE) files, commonly used for executable programs in Windows systems.

If the magic bytes don’t match the expected signature, the script displays an error message (“Error no valid PEFILE”), logs additional information for debugging, and terminates the script using exit().

Renaming and final storage:

If the magic bytes match the expected signature (indicating a non-PE file), the script renames the temporarily stored file ($tmp_location) to its final location ($location).

Basically the upload script is using a Magic Bytes Whitelist that only allows the Portable Executable signature of 4D5A.So, as long as our file contains the magic bytes 4D5A, then the script should allow us to upload it.

Great! We know how to do that.

First in a file editor open up your favorite PHP rev-shell, either Pentest Monkey or the Ivan Sincek version we can find on revshells.com.

I use this one.

At the beginning of the file, add two uppercase A’s then save and close.

In hexadecimal this becomes 41. We can see this when we open the file again in a hex editor.

These are the first four bytes of the file

We wan to change this to match our whitelist.

If you look to the right in the hex editor, you can see that the ASCII value becomes ‘MZ’.

Very clever.

Press CTRL+X to close and save the file.

Now we should be able to upload or file.

This looks good, now let’s set up our listener. If you also use Peneope, give yourself a pat on the back.

Now we will try to navigate to the file and cross our fingers.

Your browsers should hang.

Check the listener.

It connected but we see that our shell type is BASIC, meaning Penelope did not automatically upgrade our shell for us and we will have to do it manually. This also means our arrow key functionality is not preserved and we cannot use the F12 menu key. This rarely happens.

Here we go:

python3 -c 'import pty;pty.spawn("/bin/bash")'

export TERM=xterm

(CTRL+Z to background)

stty raw -echo; fg
(Press enter)
We did it! Fully upgraded.

We landed in the upload directory and since there are no surprises there, I’m going to go to /home.

Check for permissions. (There was a long pause here. The server failed to resolve it’s own hostname)

We do not have it.

After this my attempt to bring enumeration scripts over to victim machine failed and I encountered unexplained persistent shell errors. I canceled out of it.

I was trying to get the file from a python server I had on port 8000 but I was not able to type a Zero in the shell…

Forget it, we will do it again a little bit differently with a new listener. Sorry Penelope!

Use rlwrap to preserver arrow key functionality upon connection.

All you have to do is refresh the (now errored out) rev-shell browser page.

We will have to manually upgrade again.

Unfortunately this time we do not get tab auto-completion. We have not matched the window size but I am not using a text editor at the moment so I will let it slide. This is not a perfectly upgraded shell. The last step for me is always hit or miss and I have never quite figured out why. Do you know? Should I have started from a Bash and not a Zsh shell?

Moving on:

Transfer and make your enumeration scripts executable.

Run them and I will post the sections I find interesting.

Is this an unmounted drive? /dev/sr0
I have had decent success with this exploit.
The PHP session cleaners (e2scrub_all & php) can sometime be exploited for escalation when they are cron jobs run by root.
To my point this runs every half an hour.
We are currently wwwdata, we may wish to move laterally and become avuser before attempting to go for root privileges.
I am currently hoping to find a SSH key somewhere. This isn’t it, but it seems a likely path in this scenario.
I don’t know what this. It may be custom and the permissions are funky. I mean, shat kind of Binary ends in a capital letter? Maybe it’s a customization of the File command…
Is this always there? I’ll have to keep an eye out for it. Run by root.
We already have this.
I am still wondering I I can manipulate that PHP session cleaner.
This is the second time this has stuck out. We will definitely investigate.

Now for the other enumeration script. Be sure to run it with the ‘-l1’ argument to go one level deeper.

I’m thinking we can go either Here
or Here

Okay, let’s see what we can do with what we have learned.

The permissions for this are super weird. Is this a misconfiguration? Who puts write but not read access and why?

Let’s look at the other item of interest, like custom binary /opt.fileS

Playing around with it gives us a clue as to its functionality. It appears to list all the directory and subdirectory contents including hidden files.

Can we use it to read directories that are other wise unreadable?

Yes it can. Now for our unreadable directory.

That did not help. It is running as root, can I get it to run another command? Does it stay elevated?

What does this do exactly? I need help.

Hey there we go!

That did not work as I wished. I’m going to look at the help again.

This seems to be implying it is a binary similar to find.

GTFOBins already has a solution to escalate via a find SUID binary.

Might as well try it. Look there is that ‘exec’ argument! Should I have been using the absolute path before? (Always, it is always best practice to use absolute paths.)

So the command fitted to our purpose is:

/opt/fileS . -exec /bin/bash -p \; -quit
Ignore the weird echo.

We did it!

I am thankful for everyone that read my exploration of this challenge. I do this because I enjoy it, hopefully you do as well.

Thanks always to Offensive Security.

--

--

Dpsypher

Google IT Support/CompTIA A+, CompTIA Security+, IBM Security Analyst, AWS Cloud Practitioner, CCNA, CEH: Master, OSCP (93% in class, practicing for test)