During a recent string of engagements, ProCheckUp discovered a series of remote code execution vulnerabilities in a number of customer applications. This blog post will address one in particular, discovered in a network monitoring application, and will aim to provide potential paths for remediation and prevention.
The vulnerable functionality allowed admins to search for resources in locally stored git repositories. The whole searching process was done through the help of a search bar which in turn ran a shell command utilising the svn command line utility.
When searching for the string ‘TEST_STRING’, the web application returned a verbose error message as below.
-------------------------------------------------------------------------------------------------------------------------
Error running this command: svn <redacted> <redacted> <redacted> list <redacted> <redacted> 'TEST_STRING' <redacted> 'file://<redacted>'
-------------------------------------------------------------------------------------------------------------------------
N.B. Parameters have been redacted to protect our client’s anonymity.
By examining the error message, it appears as though we have control over a parameter which is then fed into an ‘svn’ shell command’s arguments.
For the purpose of showcasing this vulnerability, I have built a virtual lab made up of two exploitable implementations and a hardened one which will help me demonstrate the execution process and potential remediations.
First scenario:
The following code checks if there is a POST request parameter called ‘query’ (line 22). It then assigns the value of the ‘query’ parameter to a variable initialised as ‘$str’ on line 23.
If no input is detected, the code will display the ‘No input present.’ message. If input is detected, the value of ‘$str’ is then used as parameter in the ‘find’ system call on line 29.
Expected use:
From the screenshot above, we can see that the background command executed properly and retrieved the ‘index_easy.php’ file for us. Now it’s the time to break it!
Exploit in action:
The response to the request containing the payload is now blank. However, I already set up a listener as demonstrated in the screenshot below.
Command Injection output:
Command execution achieved! Examining the screenshot above, if an attacker uses the $() syntax on its own, the command injected in between the brackets is going to be executed.
Great, but what is happening? Time to debug it!
On line 22 the code checks if there is a POST parameter called ‘query’. If there is, the $str variable takes its value:
Before issuing the request, a breakpoint on line 24 has been set in order to demonstrate that the $str variable really did inherit the ‘query’ parameter’s value.
With the execution paused at the breakpoint, we can check what value did the $str variable initialize with:
For clarity, I created a variable $command which will show which command will be executed on the underlying host:
Ultimately, the above command will execute on the underlying host, thus allowing for arbitrary command execution.
Let’s see how it looks like under the microscope when a malicious input is used, starting with the $str variable:
The malicious command displays the contents of the /etc/passwd file and pipes the output to an attacker-controlled server via the netcat command line utility.
The $str variable is then passed to the ‘find’ shell command and the resulting command is this:
Executing the whole command will also execute our arbitrarily chosen command too.
Note: the ‘||’ logical ‘or’ operator executes the latter command only if the first command did not execute properly. This is so that if the user does not know any of the files within the directory, they’d have a better chance of discovering the contents of the directory without trying too many string combinations. After all, the main point of this application is for the user to search for locally stored files.
Great! We have achieved remote code execution… But is it always that easy?
Second scenario:
The code above does an approximation of the same thing as the code in the first scenario. There are slight differences however. The command to be executed on the underlying host is slightly altered. The user input is set to be put between asterisks encapsulated by a set of single quotes. This measure will not allow attackers to execute code as easily as in the first scenario.
The user-input is not yet filtered, but the attacker needs to improvise an escape from the single quote encapsulation.
Expected usage:
Command Injection output:
If we try to use the same payload as in the first example, our injected command would not execute.
Let’s debug it to find out why!
When we send the above malicious string, the $str variable initializes with the following value:
Same as above, no problems so far.
What is the resulting command when the $str value gets passed to the ‘find’ shell command?
The resulting command is non-malicious and technically, the shell will understand to look for files or directories that contain the user-inputted string within their names. Obviously, there would be no output, as Linux based operating systems do not allow for slashes to be used in filenames.
Thus, the attacker needs to get creative and circumvent this escape.
The above screenshot shows how the second scenario can be circumvented.
But what is exactly happening? Bring out the debugger…
As usual, we send out the POST request containing the malicious ‘query’ parameter and then the $str variable takes its value:
This variable will then get passed to the ‘find’ shell command. How does it look in the end?
The resulting command is ultimately an amalgamation of commands.
For the attacker to succeed, they need to close the first quote in the ‘find’ command, use a semi-colon to designate that another command is going to execute after the first one finishes.
Lastly, the second quotation mark needs to be escaped, so the attacker needs to isolate their arbitrary command but also close [begin] the quotation mark syntax.
With both left- and right-hand sides escaped, the injected command successfully executes!
Third scenario:
In this scenario, we try to circumvent any instance of arbitrarily injected command. The code checks for a POST request parameter called ‘query’ and if it finds it, it assigns its value to the ‘$str’ variable.
Another variable called ‘$filtered_string’ is then initialised and uses PHP’s preg_replace() function to replace any characters it finds which don’t match the regex range(line 24).
The $filtered_string variable employs the use of preg_replace() whilst using the $str variable as input parameter.
As an additional layer of security, a second variable called ‘$second_filter’ is also initialised and uses PHP’s addslashes() function. This function adds slashes before any single quotes it finds in its input parameter (line 25). However, the $second_filter variable uses the $filtered_string variable in its use of the addslashes() function.
In this scenario, input filtering has been implemented in quite a strict way.
If I am to send the same payload like the one in the second scenario, this is the value that the $str variable initializes with:
Okay, nothing different so far… moving on we see that the $filtered_string takes in $str’s variable value and by using PHP’s preg_replace() function, it replaces any characters outside the specified regex range with blank characters resulting in the following string:
The only sets of characters made available by the regex range are:
- a-z
- A-Z
- 0-9
- ._- (dot, underscore, dash)
As explained above, any characters outside this range of characters will be replaced with an empty string.
But the filtering extravaganza is not over yet. There is still one more layer of filtering added.
After the $filtered_string is set, its value is then passed to the $second_filter variable which employs the help of the ‘addslashes()’ function. Arguably, this is an overkill of a filter, however it does not hurt to add defence in depth.
Ultimately, the value of $second_filter is passed to the ‘find’ shell command, resulting in the following:
For the sake of argument, let’s assume that quotation marks were allowed by the regex filter. We will use the same payload.
The $str variable initializes with the following value:
This value is then passed to the preg_replace() function:
Notice the presence of the single-quotation marks, we’ve successfully allowed them through our preg_replace() filter. Time to see the second filter in action!
As we can see, the second filter adds slashes before any quotation marks are found within our payload.
The resulting command is this:
Although the final command does escape the quotation marks of the ‘find’ command, due to the preg_replace() regex filter, no other special characters that can help the attacker inject new commands can be used and hence will be filtered.
Summary
Here is some general advice that you can take out of this blog post.
Never execute shell commands of any kind with user-controlled input. Hackers can be a smart bunch, if you give them one finger, they’ll eat you whole!
Instead of executing shell commands with user-controlled input, refer to using programmatic abstractions such as using PHP’s scandir(), Windows API or other programmatic API for listing things like file directories. Even use of these abstractions can lead to vulnerabilities if user data is trusted and not subject to sanitisation.
Shell commands should be a last resort and should always be heavily filtered and limited to only the smallest set of users possible.
However, you should not take the above remediation steps as a guarantee of security. The implementations referenced in this blog post are specific to this use case only and may not necessarily provide full protection if shell commands were to be introduced in different contexts.
Although we have had to reverse engineer the exact solution in order to reproduce the referenced test, this is an accurate reproduction of this specific test and representative of similar issues we encounter on a day-to-day basis and which can be effectively identified and remediated via penetration testing combined with, and in addition to, increased developer education.
Reason 19,656,791 not to trust user-controlled data.
Categories