Local File Inclusion (LFI)
Last updated
Last updated
example,
the page may have a ?language
GET parameter,
if a user changes the language from a drop-down menu,
then the same page would be returned but with a different language
parameter (e.g. ?language=es
).
In such cases, changing the language may change the directory the web application is loading the pages from (e.g. /en/
or /es/
).
Function
Read Content
Execute
Remote URL
PHP
include()
/include_once()
✅
✅
✅
require()
/require_once()
✅
✅
❌
file_get_contents()
✅
❌
✅
fopen()
/file()
✅
❌
❌
NodeJS
fs.readFile()
✅
❌
❌
fs.sendFile()
✅
❌
❌
res.render()
✅
✅
❌
Java
include
✅
❌
❌
import
✅
✅
✅
.NET
@Html.Partial()
✅
❌
❌
@Html.RemotePartial()
✅
❌
✅
Response.WriteFile()
✅
❌
❌
include
✅
✅
✅
- 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:
In this case, if we try to traverse the directory with ../../../etc/passwd
, the final string would be lang_../../../etc/passwd
, which is invalid:
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:
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:
a web application may allow us to download our avatar through a URL like (/profile/$username/avatar.png
).
If we craft a malicious LFI username (e.g. ../../../etc/passwd
), then it may be possible to change the file being pulled to another local file on the server and grab it instead of our avatar.
we would be poisoning a database entry with a malicious LFI payload in our username.
Then, another web application functionality would utilize this poisoned entry to perform our attack (i.e. download our avatar based on username value).
This is why this attack is called a Second-Order
attack.
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.
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. ....////
)
some of these filters may be bypassed by URL encoding our input, such that it would no longer include these bad characters
but would still be decoded back to our path traversal string once it reaches the vulnerable function.
If the target web application did not allow .
and /
in our input, we can URL encode ../
into %2e%2e%2f
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:
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:
some web applications append an extension to our input string (e.g. .php
), to ensure that the file we include is in the expected 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
.
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:
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:
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
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.
To use PHP wrapper streams, we can use the php://
scheme in our string, and we can access the PHP filter wrapper with php://filter/
The filter
wrapper has several parameters, but the main ones we require for our attack are resource
and read
.
The resource
parameter is required for filter wrappers, and with it we can specify the stream we would like to apply the filter on (e.g. a local file), while the read
parameter can apply different filters on the input resource, so we can use it to specify which filter we want to apply on our resource.
There are four different types of filters available for use, which are
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
This is especially useful for cases where we are dealing with LFI with appended PHP extensions, because we may be restricted to including PHP files only
Once we have a list of potential PHP files we want to read, we can start disclosing their sources with the base64
PHP filter.
to 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:
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 be config.php
.
then decode the string
linux wordlist
Windows wordlist
Others
can also use
Though they are not part of seclists
, so we need to download them first
are a type of PHP wrappers, where we can pass different types of input and have it filtered by the filter we specify.