A Local File Inclusion (LFI) is a web vulnerability that allows reading local files. This vulnerability occurs when a web server uses the file path as input. Additionally, it can lead to remote command execution if certain requirements are met.

Basic LFI

As mentioned at the beginning, LFI occurs when a file path is being called through an input field. Typically, we’ll see this situation in PHP variables, but we shouldn’t limit the vulnerability to this as it would be incorrect.

To make the idea clearer, let’s see it with an example. We have the following PHP code:

Basic LFI vulnerable PHP code

It’s a simple code where a value is expected through a GET request in the file variable. Subsequently, it checks if the file variable has any content, and, if so, it includes the file that has as its name, the value of the variable.

To host the file, we set up a web server:

Local web server with PHP

This way, when accessing http://localhost it will load the file:

Server access without specifying file

Remember that when we access a web resource without specifying a file, by default it tries to load index.html or index.php, that’s why the file is not specified above, it’s not necessary.

In this case we don’t see anything, because we simply haven’t specified anything in the file variable that exists in the PHP code. Now, if we define the variable and pass as a value, for example, the /etc/hosts file:

Reading /etc/hosts file through LFI

We manage to view it, we’re achieving a Local File Inclusion (LFI). For a more readable format we can view it from the source code (Ctrl + U):

Source code showing file content

With this, we could already enumerate sensitive system files (e.g., configuration files) and obtain information from it.

For example, something typical to do especially in CTFs is to view the system users in /etc/passwd and check if they have any id_rsa in the /home/<user>/.ssh/id_rsa directory

This is the most basic type of LFI, since in the PHP code we’re not doing any type of input sanitization. There are certain protections to make it not so easy to achieve the Local File Inclusion, however, likewise, there are various techniques and bypasses to achieve the LFI.

Directory Path Traversal

Let’s change the PHP code to the following:

PHP code with directory restriction

Now, when trying to include a file, the PHP code itself will add the path /var/www/html with the purpose that only files within this path can be included.

Contrary to before, now we have a protection that seems to prevent us from loading files that are outside the directory specified by the code, however, this protection can be bypassed very easily using a Directory Path Traversal.

A Directory Path Traversal is a technique that allows us to escape from the path we’re being forced to remain in. This technique is carried out through the use of dot-dot-slash, in other words, ../.

For example, let’s try to load the /etc/hosts file in the same way we did before:

Failed read attempt without path traversal

This time it doesn’t load because it’s trying to include the /var/www/html/etc/hosts file, which doesn’t exist. However, if we try to use Directory Path Traversal, the following will happen:

Successful bypass using path traversal

We can read it, and this happens because it’s trying to load the file:

  • /var/www/html/../../../../../../../../../../../../etc/hosts

Which does exist. A detail about this is that we don’t need to know exactly how many directories we need to go back to reach the root. Because we can go back indefinitely, since when we reach the root, it will simply stay there no matter how much we keep trying to go back. This behavior also occurs in Linux:

pwd command showing current directory

Backward navigation in file system

With this last example, if in the PHP code instead of /var/www/html/, it was /var/www/html (without the trailing slash). The payload above:

  • ../../../../../../../../../../../../etc/hosts

Would not work because it would result in:

  • /var/www/html../../../../../../../../../../../../etc/hosts

So to the payload we would simply need to add a slash at the beginning so it would be:

  • /../../../../../../../../../../../../etc/hosts

And together:

  • /var/www/html/../../../../../../../../../../../../etc/hosts

It’s a mini change, but it can determine whether it works or not.

Apart from this, what happens if in the PHP code we add sanitization to remove the ../ string from the value that enters through the file variable. The PHP code would be the following:

PHP code with basic sanitization

If we try the same as before:

Failed attempt due to sanitization

It won’t work, since it’s changing our input from:

  • /var/www/html/../../../../../../../../../../../../etc/hosts

To:

  • /var/www/html/etc/hosts

And we know that file doesn’t exist. What can we do then?

Well, what we can do is try to make it access the file instead of:

  • /var/www/html/../../../../../../../../../../../../etc/hosts

Try to access:

  • /var/www/html/….//….//….//….//….//….//….//….//….//….//….//….//etc/hosts

Since when it performs the sanitization and removes the values that match ../, we’ll be left with the path:

  • /var/www/html/../../../../../../../../../../../../etc/hosts

And we can access again:

Sanitization bypass using double encoding

It would also be the same if instead of ....// it was ..././. Knowing this, one can even dare to mix both payloads

This way, we see that despite the different sanitizations, we manage to read the file. The symbols could also be placed using URL Encode, double URL Encode.

At the end of it all, it’s about asking ourselves: What if I put this, what if I do it this way? It’s a matter of being creative.

Null Byte

Null Byte is a technique that worked until PHP version 5.3.4 (they fixed it in this version). This technique allowed anything added in the PHP code after the PHP variable we set to not be taken into account.

In a PHP code a string can be added at the end with the purpose that when including files, only files ending in the specified string are included, for example:

PHP code adding .php extension

In this case, the .php string is added so that only files ending in this extension can be read. If we tried to access /etc/hosts, it would convert to /etc/hosts.php, therefore, we could no longer read it.

Well, using a Null Byte would bypass this impediment. The idea was basically to place a %00 at the end of our input.

This caused everything added afterward to be ignored. So we would pass to the file variable an input like:

  • /var/www/html/….//….//….//….//….//….//….//….//….//….//….//….//etc/hosts%00

So that in addition to bypassing all the previously seen protections, everything added after the variable would be completely ignored thanks to the Null Byte.

This technique was eventually fixed, so it’s unlikely we’ll encounter it.

Path Truncation

Path Truncation is another technique to achieve the same purpose as Null Byte, ignoring any string located after the variable.

This technique was also patched at the time. Specifically it was fixed in PHP version 5.3. Still, it’s not bad to know it. Path Truncation is based on taking advantage of the 4096 byte limit that PHP has for a string.

Knowing this limit, if in the PHP code an extension is added after the file, what happens if we send as a value in the variable a string greater than 4096 bytes?

PHP will have to cut the string and ignore what is after these bytes, so the idea would be:

  • /etc/hosts[+4096 bytes]

This way, when PHP “cuts” the string, it will ignore the excess bytes we’ve added in addition to the extension that the code itself adds afterward.

However, a series of requirements are needed for it to work:

  • The data we pass to the variable must start with a random string or letter
  • The file/path we indicate must have an odd number of characters. For this, we take advantage of the above condition.
  • Byte 4096 must be a dot, this is also achieved with the first condition

Taking these three conditions into account, the payload should be something like:

  • a/../etc/hosts/./././. until exceeding 4096 bytes.

We can test the vulnerability in the Root Me Path Truncation challenge.

This works because:

  • /etc/hosts is equivalent to /etc/hosts/. (etc)

Example:

Equivalence between paths with and without dot

In this case I’m not adding any extension in the code, I simply show how placing a /. is indifferent to not placing it in the sense that the file will still load. However, the initial “random” character is mandatory, since if not, it won’t work:

Error without initial character

Because it establishes whether the number of characters in the path is even or odd, and with it, whether byte 4096 is a slash (/) or a dot (.). In this case, it would be the same to place an a as to place 3, 5, etc., as long as it’s always an odd number.

PS: we can generate the 4096 bytes with the following command:

Command to generate 4096 bytes

LFI to RCE

There are many ways to convert an LFI into an RCE (Remote Command Execution):

Log Poisoning

The most typical is Log Poisoning. Since we can read files through LFI, what happens if we read a file with PHP code?

Basically, it will be interpreted. Now, if we can control the content of the PHP code we can execute whatever we want. However, how do we write the code we want in a system file? This is where logs come in as the protagonist.

When, for example, we try to log in to SSH, login attempts are stored in this case in the /var/log/auth.log file:

auth.log log content

So, what happens if I try to log in with a user named <?php system("whoami"); ?>?:

PHP code injection in the log

It also gets written in the file. If I now read this file through an LFI, the PHP code should be executed:

whoami command execution through LFI

Indeed, in the same place where the command is written, when viewing it from the browser, it has been interpreted and has executed the whoami command for us.

The idea of log poisoning is basically this. There are certain files that we, being outside the machine, can control their content. And if we have an LFI and can control the content of a readable file, then we have RCE.

The SSH log (/var/log/auth.log) is not the only one, other typical files that can serve us are:

  • Apache log —> /var/log/apache2/access.log
  • vsftpd log —> /var/log/vsftpd.log
  • Any other file or log where we can control the content from outside.

Note: sometimes, if we see it very difficult to insert a command due to the amount of symbols or quotes it may have, let’s not forget that we can encode it in base64 and execute: base64 | base64 -d | bash

Perhaps this way we can have fewer problems when inserting a command in a log.

Another important thing to highlight is that we have to be very careful when inserting PHP code in the log. Since if we make a mistake, the log won’t load and then we’ll have to delete the erroneous PHP code so we can read the file again (this is when it’s said that “we’ve ruined the log”). And of course, if this has happened to us on a remote machine, well, F.

Mail PHP Execution

Another possible way to achieve RCE is through an email. Emails received by a user are stored in the path:

  • /var/mail/<user>

So, if the machine has port 25 open (SMTP), we can send, for example via telnet, an email containing PHP code to the user we want and then read the email through LFI to interpret the code sent in the email.

We can send an email with telnet as follows:

telnet X.X.X.X 25

HELO localhost

MAIL FROM:<root> #Without the < or > symbols

RCPT TO:<www-data> #Without the < or > symbols

DATA

<?php

echo shell_exec($_REQUEST['cmd']); # Webshell

?>

To signal that we've finished writing the email, press enter twice, write a . and press enter again

With the email sent, if for example, we’ve sent it to the www-data user, we should find the email in the file:

  • /var/mail/www-data

Assuming it has arrived, since we’ve sent a webshell, through LFI we could execute commands as follows (example):

  • /index.php?file=/var/mail/www-data&cmd=<command>

Remember that if we’re concatenating variables in PHP, the first one is always with a question mark (?), however, all the following ones are concatenated with an ampersand (&)

Other files to check if the above doesn’t exist are:

  • /var/log/mail.log
  • /var/log/maillog
  • /var/adm/maillog
  • /var/adm/syslog/mail.log

/proc/self/environ

The /proc/self/environ file contains multiple environment variables, among them, one that may interest us is HTTP_USER_AGENT (if it exists). The value of this environment variable will depend on the User-Agent through which we’re accessing the web server. So if this file is readable, we can achieve RCE simply by changing our User-Agent to the PHP code we want.

On exploit-db there’s a PoC about shell via LFI - proc/self/environ method that explains this quite well.

/proc/self/fd or /dev/fd

Inside the directories either /proc/self/fd/ or /dev/fd/ we can find certain files with the following structure:

  • /proc/self/fd/x
  • /dev/fd/x

Where x is a number.

These files are directly related to some processes and system logs. So perhaps, one of these files may show us information about the web server we’re accessing, and with it, some field that is editable by us.

This post about BugBounty: from LFI to RCE explains this quite well, moreover, in a real Bug Bounty case.

PHP Sessions

This other method is quite curious and we can see it with an example in the article From LFI to RCE via PHP Sessions. When the server provides us with a PHPSESSID session cookie, it’s stored in the system, normally in a path like:

  • /var/lib/php/sessions/

Or similar. And with the name: sess_<PHPSESSID>

The storage path for session cookies is determined by the session.save_path environment variable, which is empty by default

So the complete path would be:

  • /var/lib/php/sessions/sess_<PHPSESSID>

If we’re able to access and read the session file through LFI, we may find fields that we can perhaps manipulate and change their value to PHP code.

LFI to RCE Conclusion

We’ve seen many possible techniques above, but really, in the end, the goal with each of them is to read PHP code through the LFI we have. So, although knowing the mentioned techniques can be very helpful, it’s our mission to analyze the specific case we’re in and see in which file we can control its content to, through LFI, read it and obtain RCE.

WARNING: let’s not make the mistake mentioned at the beginning of limiting LFI to PHP, the important thing is to understand the concept of the vulnerability. Since for example, in an IIS, we can do the same thing, but instead of with PHP files, with ASPX or ASP.

References