Remote Code Execution as a Service: A Privilege Escalation Journey
“Mess with the best, die like the REST!"
– Dade ‘Zero Cool’ Murphy, ‘Hackers’
Information security is sick! Having said that, it’s also a rather intimidating affair for me personally. There are so many things to learn, so many subcategories to discover and so many people specializing in only a subset of even those.
I never quite knew when to take the plunge and finally start looking at real software. Watching endless hours of conference talks and reading endless pages of books and articles simply doesn’t result in more experience in the field. Just in more knowledge, I guess. In the end, I became lulled into thinking that I’m on the right track, while in reality I’ve just avoided the struggles of actually putting all this knowledge into practice.
There is a lot of motivational content geared towards beginners that can be summed up as: “Just do it!” No shit, huh? It sounds trite, but it’s also: true.
So just doing it we will!
This article is about what I can do with my current skills, the will to learn and some time on my hands. Oh and spoilers: The stars aligned in what can only be described as a really pleasant first exposure to
pentesting / red teaming. Because this time we’re not fiddling with binaries. No, no, no! We’re leaving our ivory tower in order to learn about high-level privilege escalation stuff on
While the issues described in this article are unaltered, I hesitate to call them vulnerabilities per se. More like questionable design decisions combined with configuration issues. Of course I’m going to report those to the vendor. However, I don’t think they’ll change their overall design. That’s why I’ve altered all the specific information (endpoints, names of executables and directories etc). Look, I’m new to this. I don’t want to throw shade on anyone. If I get noticed of fixes, I’ll update the article accordingly.
In the end, it doesn’t really matter too much, because the vendor specific stuff is only a segue into our exploration of the field.
The starting point of this little privilege escalation journey was actually an endpoint. Here’s a little backstory for context: I have to interface with a vendor’s
REST API. The
API accompanies their main product, which is a document management software in the broadest sense. Every instance is self-hosted inside a customer’s network on a dedicated machine (there are other components besides the
REST API). For local development I’ve got a virtual machine at hand.
Some endpoints of said
API follow the conventions you’ve come to expect. The following example uses a random public
REST API that returns some juicy
We send a (
HTTP GET) request to
/posts and we receive a list of all posts. So far, so good. What if we want a specific post? Well, we get more specific:
REST 101 out of the way. You can now argue whether one should use PATCH or PUT.
But what else does our infamous
API have to offer? As I’ve said, we have a few conventional endpoints similar to our examples from above. But we also have this catch-all monstrosity:
Look at it. Just look at it! 🤯
Let’s unpack the body of our
POST request: First we have an outer array that contains exactly one object, which itself contains a single key-value-pair.
The real insanity starts with the value:
LINQ query expression written in
method syntax1. Actually, it’s a complete expression lambda, written as a sweet and innocent string!
Too many programmy words? Well, it basically means we can supply an (almost) arbitrary function that gets run in the context of the
API Server. The existing documentation only shows legitimate search and filter use cases, but we’re not fooled! This
API provides Remote Code Execution as a Service, or RCEaaS (™️) for short!
We can only speculate what the decisions behind this design are, but I’ve never come across anything like it before. At the very least this endpoint requires a valid user account for said software product, but that’s not worth much in a moderately sized company.
This design screams misuse, but let’s quickly find some positives:
Because we have total control over the
lambda, we could shape the response before it’s send, like:
Select() in the chain applies some transformation to every item that was found (in our case only one, since we specified an unique id). In the above example, we create a new anonymous type with only a handful of fields.
This way of querying data bears remarkable resemblance to a more commonly known API-query-language-system-thingy: GraphQL. An
API of the
GraphQL variety also provides a single endpoint and lets the caller decide what data gets included in the response.
I don’t know when exactly our private vendor contrived their
API, but they could be called pioneers, I guess. If only there wasn’t this little thing called arbitrary code execution.
But while it’s always easy to be dismissive of something, let’s first see what we can actually achieve with this primitive.
Where Am I?
Before testing this, I had no idea in what context our code gets executed. Surely there must be some kind of sandbox that blocks most calls. A whitelist that does indeed only allow for searching and filtering. Anything.
It turns out: Nope. There are hurdles, but nothing intentional as far as I can tell.
Let’s start exploring by trying to acquire some information about the environment. We call the
curl and pipe the result into
jq for better readability:
Wow, that’s big data! Look at that super secret api key2 just hanging out in line 38! As a side note: We have to include something from the objects that get passed to
Select() into our
anonymous object, otherwise the whole response will be empty. In this case we simply used the
Id in line 10.
While certainly a treasure trove of information, we can do better: Let’s execute big code. Remember that we talked about
expression lambdas above? Following the link reveals the second form of
C# anonymous functions:
statement lambdas. Instead of an expression on the right side of the
lambda declaration operator =>, we now write an arbitrary number of statements enclosed in braces. The final statement of the block can be a
return statement, which lets us shape the response just as before.
But why go through the hassle? Well, we can now write any code we please inside the braces. That’s a lot more ergonomic than solely relying on anonymous types for custom code. Let’s see it in action:
It turns out that the
.AsEnumerable() in line 8 is key here. From this point on, the following
LINQ lines operate on an in-memory collection, as opposed to translating the whole query expression into a
SQL statement. Otherwise, we would be limited to
expression lambdas. I don’t fully understand the details, but it has something to do with how our queries get compiled down the line.
Microsoft docs have this to say:
"Query expressions can be compiled to expression trees or to delegates, depending on the type that the query is applied to. IEnumerable<T> queries are compiled to delegates. IQueryable and IQueryable<T> queries are compiled to expression trees. For more information, see Expression trees." (source: https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/linq/#query-expression-overview) "You can't use statement lambdas to create expression trees." (source: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/lambda-expressions#statement-lambdas)
In the end, all that matters is that thicc code we are now able to write. Let’s try reading some files, shall we? But before we do so, we have to answer an essential question:
Who Am I?
API we’re talking to is an
Internet Information Services (IIS) hosted
.NET Framework application written in
IIS part is the most interesting bit here, because it will run our code. Let’s do a little research:
IIS server allows for different applications to run in separate contexts, called
Application Pools. Each
Application Pool gets assigned its own
worker process (when the first requests is made to the application), which processes the incoming requests3.
Older versions of
IIS relied on the service account NT AUTHORITY\NetworkService, which is predefined on
Windows operating systems. This means every
worker process inherited the privileges of said account. And those are pretty low. Still, more separation is always better! So starting from
IIS 7.5 on
Windows Server 2008 R2, every
Application Pool gets assigned its own identity4. Which should still be pretty low privileged, but we shall see what we can do with it!
Reading documentation is always fun, but we can also easily ask the server who we are.
Environment.UserName hold the necessary information, which will result in something like:
IIS APPPOOL\<Application Pool Name>.
Reading Files the Easy Way
If you want to read files the hard way, you’ve come to the wrong place5. We’re all about comfort here! That’s why we simply use the
C# standard library to do the job:
It worked! And they get neatly
Base64 encoded, too! How nice of them.
But hold on! What does the
global:: in line 12 do? Basically it’s a combination of the
global namespace alias and the namespace alias qualifier. Look, accessing
DLLs is weird inside our little
statement lambda thingys. I don’t know what the surrounding code brought into scope. Simply accessing the
System DLLs (think
libc) didn’t work, so I experimented with it until I stumbled upon this solution. We still can’t reach everything under the
System namespace, but we’re going to worry about that later when trying to obtain information from the outside.
So can we read any file?
Of course not! We are bound to things low-privileged users can do. Maybe we get lucky and our
Application Pool User got assigned to an Access Control List of an interesting directory or file. While certainly possible (think of a web application that accesses shared resources on a
Network File System), it’s quite unlikely that someone went out of their way to give our
Application Pool access to
C:\Windows\system32 or something equally interesting.
Misconfiguration as an Exploitation Vector
I knew nothing about
Windows privilege escalation before playing around with this
API, so naturally I searched around the Internet. Apart from some great talks (see
Acknowledgements at the end of the article), I stumbled upon one thing: Lists. Oh boy, as soon as one enters the
Windows world there are lists upon lists. Misconfiguration checklists, to be precise.
We don’t need a fancy
0-day to run our own code. Those most certainly have their place, but it’s way easier for someone like me to find configuration issues as opposed to doing the stuff James Forshaw does.
We’re going to cheat a bit and do our reconnaissance inside the actual
VM, instead of treating it like a black box. There’s no need to waste time on ideas that lead nowhere, if we have full access to the environment. Later we’re looking into ways of obtaining the same information from the outside.
In the beginning I’ve stated that our private vendor provides more components than only the
REST API. Naturally, they live in different parts of the file system. Some libraries are scattered around the Global Assembly Cache, configuration files are located inside a
C:\Program Files subdirectory and so on.
But what I’ve also noticed quite a few times is a dedicated partition for many other things, like their services. Let’s look at the permissions of that partition, maybe something went wrong:
Alright, so a normal user (“Benutzer”, entschuldigung!) has some permissions. They cannot, however, write to the partition. But what are those
“Show advanced permissions” clicked:
So many goddamn clicks, no wonder the special permissions are a common misconfiguration vector. I haven’t investigated why they are set in our case. Maybe some installer sets them or maybe it happens by default when creating a partition under
Windows. If it’s the installer by the vendor, then we’re going to have a talk!
Anyway, those permissions allow us to write files into arbitrary locations on that partition, because the permissions are inherited by every subdirectory. And you know what? In one of those directories resides a service binary of the vendor. AND YOU KNOW WHAT? That binary runs as NT AUTHORITY\SYSTEM, which is basically
It’s almost to good to be true to have so many mysterious coincidences. But reading around I’ve gotten the picture that those types of issues are quite common in the
Enough permissive talk, how do we go about exploiting this machine?
The most basic idea is to overwrite to service binary. We could simply create a malicious one that does evil things and watch the world burn, as it’s executed as
SYSTEM. But, haha, not so fast! Because the service is currently running, the binary is write-protected. Maybe there’s a way to crash the service and quickly switch out the binary, but that’s too much work for my taste.
If we could trick the service binary into running our own code, we would be golden. But how do we tackle this? The service is conveniently written in
C++, so there might be some memory corruption going on. That’s way above my pay grade, though, so we do what we do best: Injecting code.
Almost every program uses some third-party library functions. Those are provided by a Dynamic Link Library (
DLL) in the
Windows world. Well, provided by numerous
DLLs. There’s a well-defined search order for those. If we were able to plant a malicious
DLL somewhere before the legitimate one in the search path, we would win! Most of the well-known locations are only writable by privileged accounts, but in our case we can plant a
DLL directly next to the service binary. This happens to be the very first location in the search order.
Simply overwriting a
DLL that’s used would fail for the same reason as above: It, too, is write-protected when loaded into the service process. Fortunately, we find a reference to
VERSION.DLL6, which is not present in the binaries own directory.
I said that we’d win as soon as we would be able to plant a malicious
DLL. But what actually needs to happen for our own code to run?
DLL can have an optional entry point called
DllMain. If we provide one, it will get called automatically upon loading of the
DLL. It’s a similar concept to
GCC's constructor attribute.
There seem to be “significant limits on what you can safely do in a DLL entry point”, according to the
Microsoft docs. In order to keep it simple, our payload will only execute a
batch file that we can plant via the
Here’s the code for the
Most of this is boilerplate found in the documentation. Our
Payload() function simply spawns a new process that executes our script via
cmd.exe. What does our script do?
We simply create a new user and add it to the admin group as a proof of concept.
After compiling the
DLL (we’ll get there in a minute), we would write it and the script into the directory that contains the service binary. And then we’d have to wait until someone or something decides to restart the service, because those
DLLs are not automatically loaded once present.
Hold on! Our
DLL barely contains any code. I specifically selected the one with the least used functions, but chances are our service would still crash while starting up, because it’s looking for functions that we simply don’t provide. Maybe our payload would run, maybe not. It would be quite suspicious and fairly easy to pin point in any case.
What we need is a mechanism similar to the dynamic redirection we did previously on
dlsym(). In other words, we need a way of proxying calls to the original
DLL Proxying. I won’t go over the details, as someone else did a fantastic job already. At this point there’s no benefit of deep-diving into the specifics, as it boils down to some arcane
linker directives. Following the steps laid out in the repository is just fine.
With that, we now have three files to drop:
- the original
- the malicious
DLL(which also acts as a proxy)
- the payload.bat script
Dropping itself is as simple as storing the files as
Base64 encoded strings inline in the request and calling something like:
After doing so, let’s manually simulate a service restart:
net users shows no sign of us, but after stopping and starting the service we suddenly own this machine! What a great feeling.
An Outsider’s Perspective
While having pwned the machine, there’s still an 🐘 in the room: Obtaining the necessary information about the environment (directory permissions, running services etc.) from the outside.
All of the above examples rely on libraries that are already in scope in some way. They may have been imported directly by the code that handles our query. Or they come as part of the Base Class Library. We had to specify the
global namespace to get a hold of some, but others are simply not present. Most notably we cannot spawn our own processes, for which we need
To work around this issue, we’re again going to compile our own library with the necessary imports and functionality. Only this time we won’t drop any files!
The idea is to provide a generic function that simply spawns a
PowerShell process with a command of our choosing:
As we can see, we create a
powershell.exe process with some options set, capture its redirected output and return the resulting string. Line 8 is the interesting one here, because we use the
-encoded switch. It allows us to pass a
Base64 encoded command, which saves us the headaches from dealing with quotation marks and escaped symbols. It’s also pretty sneaky, right?
While there should be a way of compiling the library on
Linux, I’ve opted for using the
Visual Studio “Class Library (.NET Framework)” project template. After having compiled the
Dll, we need a way of dynamically loading it into our context and call its method.
We can achieve this via Reflection, which provides the functionality to load an
assembly (e.g. our
Dll), obtain information about its classes and in the end invoke its methods. All at runtime. That sounds amazing and scary at the same time! Let’s see it in action:
There’s quite a lot to unpack here, so let’s get to it:
First, we inline our
Dll, so that we can load it in line 12 via a call to
Assembly.Load(). There are other overloads for said method that take a path, or a fully qualified name. I’m genuinely curious about the use cases of loading from an inline byte array ¯\_(ツ)_/¯.
Next, we define a command for retrieving the service location in line 16. Again in
Base64, so that we don’t have to deal with escaping quotation marks 7.
Afterwards, we loop over every defined type in our
Dll, which is only our static
CommandExecution class from above. I have, however, not looked into a way of getting rid of the loop. Anyway, we retrieve the
Execute() method and invoke it in line 21 with an
object array containing our command as the sole argument.
Finally, we receive our result in line 33. That’s amazing!
We’re not going over every command needed for information gathering here. Now that we have a way of executing arbitrary shell commands on the machine, it’s only a matter of time until we get a good grasp of the environment.
And just like that we took over a
Windows box over the network. To recap:
We discovered a fishy
REST API endpoint of a document management software. In order to make requests, we only needed an account, privileges didn’t matter. Said endpoint basically runs arbitrary code for us. A combination of a poorly placed and authorized service executable and way too permissive access rights opened the door for a
DLL Hijacking attack.
We created a malicious
DLL that executed a script we planted while also proxying calls to the legitimate
DLL. This way, everything continued running as expected.
Upon restarting the service, our malicious payload ran, which created a new local administrator account. It could have been anything, though. Creating reverse shells, tinkering with the registry, installing keyloggers or mimkatzing some domain passwords. You name it.
We took some shortcuts in our reconnaissance phase, in order to quickly iterate on ideas before settling on one. Finally we showed how the necessary information could be obtained from outside. We overcame the hurdle of not being able to create new processes by loading our own
DLL dynamically at runtime, giving us a little insight into the
Reflection system of the
Overall I’m really happy with this experiment. It was very much a “learning as we’re going” experience. Which is the best kind of learning experience for me.
I hope I could shed some light on the world of
Windows pentesting and red teaming without mentioning
Kali Linux one billion times. Thank you so much for reading!
Resources and Acknowledgments
- This talk by Jake Williams. A great introduction to
Windowsprivilege escalation techniques.
- DLL Hijacking Guide by tothi. Thanks for not making me work through all the linker details!
Microsoftdocumentation. While certainly hit or miss and notoriously hard to comb through, they helped immensely this time.
Here’s your friendly reminder that environment variables are not a safe place to store secrets. ↩︎
It’s not as straightforward to obtain the imports of a binary as it is on
Linux. In the end, I simply loaded the .exe into
Ghidraand looked at the “Symbol Tree”. ↩︎
At first, I’ve simply used the amazing CyberChef to encode the command. It turns out that this will trip up
PowerShell, as it’s expecting an
Unicodestring. To make it work, we have to encode our command to
UTF-16LEbefore applying the
Base64encoding. This can be done with
CyberChefas well. Of course it can. ↩︎