Networked (HTB)
Networked was a great opportunity to dig into scripts, learn how they work, and think creatively about how they can be abused. You don’t need much (if any) experience with PHP to get through this box; as long as you know some programming basics and don’t mind researching functions on php.net, you’ll be able to put it all together.
Initial Scan
The only interesting port is Port 80, so I start there.
Web discovery
I fuzz for directories. Given that Apache and PHP appear in my nmap scan, I check for files that end in .php.
Testing uploads
At /upload.php, I find a basic file uploader.
If I upload a TXT file, it fails.
But if I try an image file—like a PNG—it succeeds.
My uploaded image can be found at /photos.php (on the left).
To exploit this, I’d want to upload a PHP reverse shell. Then I could trigger it by simply visiting /photos.php.
But uploading a PHP file yields the same error as when uploading a TXT file. Clearly there’s some filtering going on in the file upload function.
A note on trial and error
In the HackTheBox forums, I gathered that a lot of folks simply tried a few common upload bypass techniques and got initial access. The technique used for Networked is incredibly similar to the one used on another retired box. So if you already have that technique somewhere in your mental to-do list, you’ll get through this part by pure trial and error.
But guessing likely isn’t the intended method. If you analyze the source code, you can know what the technique is before you try it. This is the more valuable lesson to take away from this box.
Analyzing the PHP files
In the /backup directory, all the PHP code is readily available in a TAR file.
Just extract it, and you’ll see the code behind all the web pages.
What’s relevant to our exploit are upload.php—which shows the code behind the upload page—and lib.php—which defines the functions used in upload.php.
upload.php
Here’s an excerpt from upload.php:
This tells me that if the file ends in “.jpg”, “.png”, “.gif”, or “.jpeg”, $valid
will be set to true
. I need to make sure my reverse shell meets this criteria.
Another important snippet shows why I’m triggering that error message:
This indicates that if my file doesn’t return True for the check_file_type function (or is larger than 60,000 bytes), I’ll get the “Invalid image file” error.
lib.php
The check_file_type function is in lib.php:
If the file content type begins with ‘image/’ (as JPG, PNG, and GIF files do), my file will pass the test. Another important thing to add to my checklist.
And what does the file_mime_type function do?
If my file matches on the regex in the second line, it should pass as well.
Recap of criteria
So my file has to meet all of the following criteria:
- The filename must end in “.jpg”, “.png”, “.gif”, or “.jpeg”.
- The content type must begin with “image/”.
- The filename must match the regex “/^([a-z\-]+\/[a-z0-9\-\.\+]+)(;\s.+)?$/”.
I head over to https://regex101.com/ and paste in the regex. Here I can safely mess around with common upload bypasses, see if they match the regex, and avoid submitting dozens of garbage uploads to the victim. Per my checklist, I must keep an image file extension at the end (e.g. “.png”).
Here’s one that appears to work:
Although I’ll satisfy having “.png” at the end of the string, the extra dots in the middle will terminate the filename before the .png. So when I do a GET request on my image, it’ll behave as if it were a PHP file.
Performing the bypass
It’s also important to test if there are any checks on the file content itself. Will I be allowed to include PHP code in my decoy PNG file? I don’t see anything relevant in the PHP files, so all I can do is test.
I intercept my normal file upload with Burp. The filename, Content-Type, and content itself look like this.
So I create a PHP reverse shell (using the one in /usr/share/webshells/) and paste it right after my PNG content ends.
I forward the request, and my upload succeeds.
This means that the upload does no checks to see if there’s any PHP code in the image. (It may check for “magic bytes” in the beginning of the file, which is why I was careful to preserve the PNG and add the PHP after the PNG content.)
To execute the code, we need the file to have the .php extension, not .png, so I send the upload again with the PHP code—but I also modify the filename like this:
As with before, it succeeds.
I set up my netcat listener:
Then I visit /photos.php to trigger the payload. I see my filename on the page, but no image.
Back on my listener, I get a shell as user Apache.
I upgrade to a Python TTY to make life easier.
Analyzing the cronjob
In the user Guly’s home folder, I find:
- user.txt — with no permission to read
- check_attack.php — a script that checks if filenames have been modified (and if so, alerts the user Guly)
- crontab.guly — a cronjob that executes check_attack.php every 3 minutes. We can assume this is under the context of user Guly, given the filename and location.
These are the contents of crontab.guly:
Let’s see what check_attack.php does (comments are mine).
Ideally, I’d want to tweak the script to give me a shell, but I don’t have permission to modify it. If you take a closer look, you’ll notice there’s one variable you do have control over: the filenames in the uploads folder ($value).
Gaining command execution for Guly
If I add or rename a file in /var/www/html/uploads, I can insert my own input into $value in the script. But it’s tough to know exactly where in the script this would be effective.
Luckily, I can get an idea of what’s going on with the “echo” commands throughout. And I can test executing the PHP file as the Apache user—before I let the cronjob (i.e., user Guly) execute it.
First, I create an empty test file (test.txt) and drop it in the uploads folder.
Now that “test.txt” should be assigned to $value, I execute the PHP script and see what the output tells me.
This shows that the filename ($value) appends to the end of “rm -f /var/www/html/uploads”. So in the command, I have complete control over the bolded section:
rm -f /var/www/html/uploads/test.txt
In the script, the actual code I’m manipulating is:
If I create a filename that starts with a semicolon and continues with a command, I could inject a new command of my choosing for php exec() to run.
exec(“nohup /bin/rm -f $path ; command-to-inject > /dev/null 2>&1 &”);
The right reverse shell
I had trouble getting netcat
to work here, mostly due to the slashes.
I try socat
instead. I set up my listener on Kali:
Then I create my filename that (somehow) allows all these punctuation marks.
And (again as a test) I execute the attack as user Apache.
I get a shell as Apache on my listener. So the test worked.
Getting a shell as Guly
This time, instead of executing the file myself, I just wait for the cronjob to execute it for me under the context of Guly.
On my attacking machine, I kill the second Apache shell and create a new socat listener. On the victim machine, I create that socat payload filename. I wait for the cronjob. Within 3 minutes, I have a shell as Guly.
And I can grab user.txt.
Another exploitable script: changename.sh
I run sudo -l
to see everything Guly is allowed to run as root, and I find another exploitable script to play with.
This is changename.sh:
This script changes some values in a configuration file regarding the network interface guly0. I check the configuration file.
When I run changename.sh as Guly without sudo, it prompts me to change each field (where I enter “test”), but I don’t have permission to do so.
Testing command execution
What’s strange is that second to last line: “/tmp/foo: No such file or directory”. It implies that something at /tmp/foo is trying to be executed, but there’s no file there. /tmp is usually a world-writable directory, so I try to add my own “foo” file there and rerun the script. My “foo” file will just echo out the word “test”.
In the changename.sh output, I see that “test” was echoed.
So if I store any command as /tmp/foo, the user running changename.sh will execute it.
Reading root.txt
If I create a /tmp/foo file that contains “cat /root/root.txt”, I can sudo the changename.sh script so that root will execute my command (and show the contents of the root flag).
I’ll also have to be careful to preserve the “NAME” field (ps /tmp/foo). Unlike Guly, root can actually modify the fields, and this will likely mess up whatever is executing the foo file.
As expected, the flag appears in the output.
Bonus: root shell
The technique to read the flag doesn’t take much modifying to get root shell. First, I set up a netcat listener.
Then I simply replace /tmp/foo with a netcat command.
Back on my listener, I get a root shell.