While a fairly straight forward exploitation on a somewhat crude front-end, this was an interesting box with a couple of valuable teaching points. Initial access is derived through bypassing an upload filter to send and trigger an unprivileged PHP shell. We then laterally move through the system, leveraging a cronjob misconfiguration to invoke another reverse shell as our target user. Finally, we escalate to root by exploiting a poorly developed networking configuration script. In short, we see practical examples of both file upload bypass and command injection, on a relatively clean Linux environment. Great box for beginners.
|Original Base Points||20|
|Release||24 August 2019|
|Address||10.10.10.146 on /23|
Standard starting point on this one; nmap enumeration with the typical flags. As seen in figure 2.1, we kick off with a nmap query on the box using flags -sC for default scripts, -sV for version detection, and -oA to output the results in all formats. We identify two services: ssh and apache, on their standard ports, and versioning tells us both appear up to date at the time of this writeup. Let the enumeration continue.
Since both applications are up to date, we will skip vulnerability inquiry with searchsploit/MSF Search/Google, and start by diving deeper into what Apache is presenting us on http://10.10.10.146:80/.
Further Enumeration of Apache
When we visit http://10.10.10.146 we are presented a basic webpage that appears to be a developer’s placeholder, see Figure 2.2. We can further view the source with ctrl+u and find a note, “<!– upload and gallery not yet linked –>,” which gives us the idea that there are possibly darknet pages which we can’t get to simply due to a lack of linkage.
Let’s uncover those pages with dirbuster–a web page identification tool. We break it out with -r flag for non-recursive, and -o flag to output the results to a file. It’s important that we use the non-recursive flag for the sake of time management. If you include it then the tool will run the wordlist against every item it finds, and for something like the js directory which has a bunch of subdirectories, that means the tool might take days to complete its query. As seen in figure 2.3, we have a couple of interesting hits.
Just based on names, I’m thinking two things right away. First that we may be able to find either keys/passwords or code in the backup directory, and secondly that we may be able to place scripts or executables in the uploads directory through an uploads page and get code execution. Let’s start with the obvious “upload.php” possibility, as we saw that Apache is running PHP/5.4.16. That succeeds in getting us to a page pictured in figure 2.4.
Before we try to upload a shell, however, let’s check out the backup directory that dirb kicked back to us earlier. We find a tarball and are able to download and extract it without a password. Interestingly, we find some PHP files. Importantly, index.php is probably our index file, photos.php is probably the “gallery not linked” page mentioned in the notes before, and upload.php might be responsible for the upload page we saw in figure 2.4. It’s also likely that lib.php contains some imports. See figure 2.5 for the extraction output.
Let’s check out some of this code. For the sake of brevity, I’ll leave out index.php as it was indeed our landing page, and we’ve already seen its source. The lib.php is a library file, but we’ll come back to it in a minute. Navigating to photos.php we identify the gallery mentioned in the comments of the source on the landing page, noting that we may be able to use this page to execute our file inclusion later. For starters, however, we will ignore the other three and begin some code review on uploads.php, to see exactly what’s going on when we submit a post request to the page.
Quickly taking some notes, we find that on line 2 in figure 2.6, this document requires the import of lib.php, which we do have the code for. In that syntax, we are able to identify that the live copy of that code is located on the server at /var/www/html/lib.php. Further on line 4, we see that an upload directory variable is defined as /uploads after the root of the webserver. There is a high probability that this is where files are placed by the server on upload, but we’ll know for sure once we’re in the box.
Last but most important item of note for figure 2.6, we see that lines 6-8 are processing a POST request with the file upload into the variable $myFile when the submit button is clicked. Then on lines 10-12 a check is performed on the file type and the file size. The check_file_type function does not exist anywhere else in upload.php, so we are going to have to look at lib.php to find it, but the second check is simply ensuring the file is smaller than 60KB, so we also make a note of that for our eventual shellcode.
Still examining upload.php, we find that the file must contain a specific extension to be considered an image file (jpg, png, gif, jpeg). This not only hints at what the filetype check is looking for, but tells us another important note for our inclusion. We see that the file must end with the extension to pass the check, it can’t simply contain it. If not, then it will set the valid variable to false, and display an error message. It’s critical to note that the error messages which are sent back are different for those two conditionals; I’ll demonstrate this later, but for now, just see the difference between line 11 in figure 2.6, and line 32 in figure 2.7.
If it succeeds both validation checks then we dodge the exit on line 34, and see the name alteration on line 36 in figure 2.8 prior to saving the file locally to the server. This command is taking the address of the uploader, as in our IP address as the uploader, and replacing the decimal octet separators with underscores, then appending the file extension. This would turn example.jpeg into 12_435_728_01.jpg or something respectively similar. We need to note this so that we have the correct filename when we go to invoke the shell.
Finally, let’s take a step away from upload.php and see what check_file_type is doing within lib.php. The long story cut down to a few lines, is that on line 35 the mime type is identified through the PHP native @finfo_file class, and then line 59 checks that output with a strpos to see if its a /image type.
Invoking an Unprivileged Reverse Shell
We’ve done our basic recognizance, and are ready to attempt our exploit. Through our recon, we know that the shellcode needs to be smaller than 60KB, and pass a file extension check, as well as a MIME-type check. None of those requirements are all that challenging to bypass, so let’s get cracking. I’ll first show the traffic with Burp Suite, and then modify the shellcode by editing the binary with a hex editor and show the traffic again so you can see the difference.
We first need PHP reverse shellcode that we can trigger and receive. You can write this out if you want, but I always like to use established and respected shells from Github so I don’t have to waste time forgetting end-quotes or semicolons. In this case, since the webserver is running PHP, we need a PHP shell, and I have chosen to use pentestmonkey’s code, which can be had over at GitHub with a simple git clone or curl -O: https://github.com/pentestmonkey/php-reverse-shell. In figure 3.1 you see the retrieval and necessary code modifications so that it will reach back to my box on a port that is open. I am currently assigned 10.10.14.10/23 and am going to open the shell on 13338.
We open Burp Suite and turn on the proxy in our browser so that our traffic gets sent through for intercept. We then click “go!” on the uploads page and get the following traffic in Burp: figure 3.2. See the Content-Type and filename as myFile. Essentially, it detected the content as a PHP application. Let’s change that.
We head over to https://www.iana.org/assignments/media-types/media-types.xhtml#font to take a look at the file framework for JPEG, so that we can replicate it. IANA has all the media types for known files, and very importantly will tell us the magic bytes we need to lead with. This brings us to the RFC for JPEG files, https://tools.ietf.org/html/rfc2045, from which we can derive the leading magic bytes for a JPEG file, which are 0xFFD8FFEE.
If we run $file against the script, a built-in Linux command to identify files based on their magic bytes, we find that it’s a PHP script. However, once we insert the previously mentioned magic bytes, $file now thinks its an image file. See figure 3.3 and figure 3.4. This can be done with any old hex editor, but I used the xxd plugin in the vim editor.
You may also observe the magic bytes in the hex of the updated file, figure 3.5. Ultimately Linux doesn’t care about extensions, filetypes are typically identified via magic bytes, which is quite frankly more dependable.
We still need to pass the file extension check, so we just rename the file to “php-reverse-shell.php.jpeg,” and upload it to the site. In figure 3.6 you can see the Burp Interception, and specifically note that the Content-Type is now updated as well as the file name.
All that’s left now is to trigger the shell and receive it. In figure 3.7 you see that our file now appears as an upload in the gallery, and is named just as we understood the translation code to work before. In order to trigger the shell, we navigate to the file within the uploads directory, while running a listener of our choosing. I’m using $nc -lvnp 13338, to listen to tcp/13338 as we set earlier.
In figure 3.8 you can see the reception of the shell and also note that we are running as the webserver’s user. The next step is to break out and move to a user with greater permissions on the system.
Lateral Movement to the Target User
Once we have a shell, we can start poking around. We find that there is only one user in /home, named guly. In his directory, we find two interesting files. We also see the user flag, but can’t read it since we are apache and it is set to owner read exclusively (400), and the owner is guly, See figure 4.1.
When we take a look at the crontab.guly file we find that cron is executing the check_attack.php script in guly’s home directory every three minutes.
We then go into the check_attack.php script and find a few interesting lines. The purpose of this script is to go through the uploads folder and check by filename to see if there are any invalid items. On line 29, however, we see that this is done using the exec() function, which executes code in the brackets (typically variables) as the owner of the file. This poor programming practice often results in vulnerabilities, and this case is no exception. See figure 4.4.
The script does NOT sanitize the input in the $value variable so while line 29 is well-intentioned, it is vulnerable to command injection. The line that should remove bad files: “exec(“nohup /bin/rm -f $path$value > /dev/null 2>&1 &”);” can be injected into by putting shellcode in /uploads resulting in the exec running “nohup /bin/rm -f $path;cmd > /dev/null 2>&1 &” when the filename is checked. cmd is effectively going to be the filename.
Exploiting the Cronjob
Since we can’t use slashes(“/”) in a filename, we will need to base64 encode the filename. We do this on the local system so we have better control over the terminal. See figure 4.4.
This simple one-liner should give us a reverse tcp bash shell from the user guly, as that user is the one who owns the cronjob it is triggered from. Finally, we touch a file with that name along with the syntax to decode, and open a local listener to receive it. Figure 4.5.
The shell shows up right on time within those three minutes, and we are able to read the user flag, proven with a wc -c (32 byte hash with a line break). See figure 4.6. I don’t publish hashes, so wc is used to prove access, as it will fail with the same permission denied that cat would if we didn’t have access.
Privilege Escalation to Root
Not uncommon to HackTheBox challenges, privilege escalation to root might be the easiest part of this box. This involves another command injection technique, however this time the user input is validated… at least to a small degree.
On initial access as guly, we check our sudo privilege with $sudo -l, and find that we can execute /usr/local/sbin/changename.sh as root. See figure 5.1
We then go to examine that script and see that it creates a network interface, finally using “/sbin/ifup guly” at the end to activate the interface to guly. While the input is validated, it’s still vulnerable to injection through the technique described here at seclists.org. Essentially unexpected input termination. Funny enough, the CentOS developers didn’t/don’t consider this to be a real vulnerability even though if this case exists attackers can obviously easily pwn a given box.
This is a super easy injection as everything after a space is run, since scripts are serviced by the underlying service. We inject /bin/bash to elevate and boom: we are root upon completion of the script. See figure 5.3
(1) Sanitize your input…. please, please, please, sanitize your input, and for the love of God don’t use eval or exec unless it is ABSOLUTELY necessary–which it is very rarely is. Eval was solely responsible for the vulnerability in python 2.7 and why raw_input() should be used over input(). This was fixed in Python 3 but is still a blatant security issue in python 2.7 which is used heavily around the world today because Python 3 was not developed to be backward compatible.
(2) Understand linking cronjobs and file permissions. This is a critical point, and a technique often used to move laterally in systems or even privilege escalate in many cases.
(3) File inclusion is a very common technique for breaking into web servers. If the webserver has an upload function, you can bet I’m going to spend considerable effort trying to get around its filters to perform arbitrary code execution via file inclusion. Filter properly, and be careful to store uploaded files somewhere non-obvious to the front-end. While this box renamed the files, it still presented the renamed entity to the user, so it effectively defeated its own security measure. Store files under a handler name, and present them back to the user as the original name, this way its harder to explicitly trigger code on the backend of the server.