July 19, 2018

Bug Hunting – PHP

(Last Updated On: 29th January 2017)

I’ve been doing a bit of bug hunting recently (with varying degrees of success) and really wanted to write this article just so I have a little cheat sheet to go back to!  I’ll keep updating it and adding bits as I find more.

When I scoured the internet I came across a few “examples” of PHP bugs and I will count my blessings if I ever find one real world as they really are so glaringly obvious that a developer would almost have to put them in on purpose.  Doing my OSCP showed me a few more examples but more from the black box side with fuzzing and plenty of SQL injection.  It was only when I set myself a little project that I began to find a lot more ways a bit of PHP web code could go wrong and rather than forget them all I figured I’d document them here!

One important thing to note before you start picking apart some PHP is how the script is accessing user provided input.  HTTP allows a user to provide “POST” data in the data segment of the HTTP request or provide “GET” data through the URL (like you are providing us with this article id with “post=1082” in the URL).  A third option is through a cookie stored on the client which gets passed automatically to the web domain that generated it.  Prior to looking at some embedded device firmware, I had only ever seen these available to the PHP code as arrays $_GET, $_POST and $_Cookie.  What surprised me about the embedded firmware is that the CGI binary it was using to execute PHP received all of the user input from GET, POST and Cookies and then provided them to the script as regular variables.  This is important because it changes what you are looking for when bug hunting as you won’t see the $_GET or $_POST prefix.  It also means you could potentially manipulate other script variables which we will come to later.

Throughout this article, I will utilise examples where the underlying operating system is Windows.  This just allows me to use file extensions and make bits a little clearer.

Part 1 — Exec, System and Passthrough

As you’ll see, a lot can go wrong when using the php Exec, Passthrough and System functions which allow you to pass a command to the underlying OS and return its contents.   If you don’t sanitise user input before it gets passed to this function then don’t be surprised if the user can run arbitrary commands against your server!  Let’s see the textbook example of this, the humble ping command!

 $return = exec("ping.exe ".$_GET['host']);
 echo $return

If this webpage was ping.php on example.com, we could use it to ping our webserver by browsing to the URL :
http://example.com/ping.php?host=blog.synack.co.uk
But what if I wanted to run another command?  In Windows you can chain commands with the & so to run the command whoami.exe I could run:

ping.exe blog.synack.co.uk & whoami

So to put this into a URL the ‘host’ parameter needs to become ‘blog.synack.co.uk & whoami.exe’ although for short we can drop the .exe bit as well.  Unfortunately we cannot simply put spaces and ampersands into a URL so we need to encode it using a site like this one here.  When we do that, we get “blog.synack.co.uk%20%26%20whoami” so this become our host parameter and the URL becomes:

http://example.com/ping.php?host=blog.synack.co.uk%20%26%20whoami

If the script fetches the ‘host’ variable from $_POST or $_Cookie then it wall take a little more effort to exploit.  The easiest way I have found is to install the OWASP ZAP proxy and intercept your request then modify it to contain whatever you want.

Fantastic we have injected a command to get RCE (Remote Code Execution), but the chances of finding this in a live application is pretty much zero because php has a built in function called escapeshellcmd which removes all the special characters someone could use to inject an additional command.  The code you are likely to see on a website is this:

 $cmd = "ping.exe " + $_GET['host'];
 exec(escapeshellcmd($cmd));

So now we are foiled!  Maybe…

We could potentially still get an exploit in some cases.  Let’s image that the embedded device we are testing uses a custom binary that checks for the existence of a session.  This binary requires two arguments, the session ID taken from a Cookie and the location of the session database and will return a ‘0’ for a match and a ‘1’ if there isn’t a session in the database.  The PHP code may look something like this:

 $cmd = "mybinary.exe " + $_Cookie['SessionID'] + " " + $SessionDB
 $result = exec(escapeshellcmd($cmd))
 if ($result == 0)
 {
 echo "Session exists"
 }

Now we can’t get an RCE here as escapeshellcmd being used to protect the exec function from malicious characters, however maybe we can bypass authentication here if we can push that session database argument into position three!  Consider the case that we can upload files to the server through some other means (LFI’s covered later).  If we  provide a Session ID of “1 /path/to/session/db/we/uploaded” then the command becomes:

mybinary.exe 1 /path/to/session/db/we/uploaded /path/to/the/real/session/db

Let’s hope that mybinary.exe checks for the existence of that third argument otherwise we have pushed the real database location into an argument which won’t be utilised and can now control the session database the binary is checking for our session id!

At the start of the article I said that some implementations call the php script with all the $_GET, $_POST and $_Cookie variables converted to standalone variables.  An example would be $_GET[‘host’] from earlier.  Rather than being referenced as $_GET[‘host’], it would just be $host.  This raises some interesting repercussions.  Let’s imagine another example, slightly more complicated but possible on embedded devices.  In this case mybinary.exe takes a session id and a database file but the two parameters are reverse so our earlier trick doesn’t work!  The session id is still derived from the cookie but as this is handled by the CGI application the PHP script can only reference it as $SessionID, not $_Cookie[‘SessionID’].

 $auth = "Failed";
 $user_regexp = "Username: (\w+)"
 $cmd = "mybinary.exe " + $SessionDB + " " + $SessionID
 exec(escapeshellcmd($cmd,$lines))
 $i = 0; $size = count($lines);
 while ($i < $size) {
 if (ereg($user_regexp, $lines[$i], $res)) {
 $username = $res[1];
 $auth = "Success";
 $i = $size;
 }
 $i++;
 }

This code will put the output of the mybinary.exe into the array $lines.  The while loop will iterate through these lines looking for a username in the output and if it finds one then it will use it for the $username variable and authenticate the session.  Importantly, the $lines array is populated because it is the second parameter of the exec command and according to php.net if the array is already populated then the lines will be added to the end of the array.  This means that if we pass in a POST parameter of lines[0]=”Username: Simon” then we will create the array before it gets to this script block and therefore when the exec call returns its output (without a username) it will get appended to our existing array and we will authenticate just fine anyway!

LFI / RFI

Okay so you have persisted through the operating system calls and landed at the two acronyms LFI and RFI which are largely the same thing.  PHP scripts often share a lot of common functions and variable definitions so anything common is ripped out and stored in a separate script which is then included in each script that needs it.  This is achieved with the PHP include function as shown below:

include /includes/SomeOtherScript.php

The text book LFI (Local File Include) is illustrated in the code below and generally revolves around the user specifying a language.

$langfile = "languages/" + $_GET['lang'];
include  $langfile;

By passing the lang parameter in the URL we could request any file is included in the PHP script.  If we are able to include remotely hosted content then we have an RFI (Remote File Inclusion) otherwise it’s an LFI.  If we have an LFI then we need to think about how we going to get malicious code onto the server, although there are plenty of blog posts about contaminating logs etc and the OSCP will teach you this if you take it!

Real world examples are likely to look much different so I’ll show an example below that is a modification of some real world php but was the real world had a mitigation for the exploit I’m going to suggest.

Function get_dictionary_file $language, $language_name (
 if (substr($language,0,2) == "u/")
 {
 $result = "/etc/persistent/lang/"
 + substr($language,2,strlen($language) - 2);
 }
 else
 {
 $result = "lib/lang/" + $language + "/" + $language_name;
 }
 return $result;
);
$active_language = $_GET['lang'];
include get_dictionary_file(active_language,$languages[$active_language]);

As you can see this is much more complicated than before, but if we look at the get_dictionary_file function we see that although it takes two arguments, the second argument isn’t actually required if the $active_language variable begins with “u/”.  This is good, as it means we haven’t got to worry about getting the value into the $languages array which is generated by polling directories.  If this were the actual php script on this embedded device (which sadly it wasn’t) then we would only need to specify the lang URL parameter as u/../../../../../../../../etc/passwd to grab the passwd file on linux or put a path to u/../../../../var/logs/httpd.log to include our malicious php we managed to put in the logs.

Unfortunately rather than just taking the GET parameter, the real world device runs the code block below to assign the $active_language and there is no getting past that!

if (isset($languages[$ui_language]))
{
 $active_language = $ui_language;
}

File uploads

Coming soon…

More to come…
Previous «
Next »

Simon is a sysadmin for a global financial organisation and specialises in Windows, security and automation.

Leave a Reply

Subscribe to SYNACK via Email

%d bloggers like this: