On a penetration test last year, we discovered a local privilege escalation (LPE) in a piece of software running on our client's systems called Liquidware ProfileUnity. This post is a quick run down of the techinical details of the vulnerability, how to determine if you are affected, and potential mitigations.
I'm not an expert on administering large Windows networks, so I'll describe ProfileUnity in the vendor's own words. LiquidWare specializes in "adaptive workspace management," and ProfileUnity is an enterprise solution that offers "sophisticated user environment management and migration."
In the pen test scenario, we had access to a compromised Windows 10 machine configured as a standard employee build, with a low-privilege employee account. As we explored the attack surface visible from this vantage point, one of ProfileUnity's core features jumped out at us: the elevation service.
The elevation service allows system administrators to configure certain processes to run on endpoints with elevated privileges, without requiring the process owner to be a local administrator or type in an administrator password. The elevation service also imposes some safety constraints, such as not allowing elevated processes to spawn subprocesses. The name of the service immediately flagged it as an interesting attack surface, as well as the fact that it runs as
The first step in our analysis was to understand what the elevation service was really doing. With a little bit of digging, we found an XML file that is used to configure the elevation service.
By default, the configuration has two white list rules: elevate any process that has a security certificate signed by
Liquidware Labs, Inc. or
Liquidware Labs (lines 9-10).
This file cannot be written by a normal user, so in order to play with it we will need to edit it as a local administrator. We added lines 11-12 as seen in the screenshot above. Our new rules will automatically elevate every process named
MyTest2.exe. (The significance of the latter will be revealed later.)
Now if we open a command shell as a normal user, we will see this result:
Even though we are logged in as a low-privilege user, the window is labeled "Administrator". In the background, the elevation service:
- Saw a new process being created.
- Recognized that the process matched one of its elevation rules.
- Terminated the new process.
- Created its own subprocess with the same executable and command line arguments.
For the last step to work, the executable either needs to be in the search path or the command needs to include the full path to the executable. Process Explorer shows that
cmd.exe has moved underneath the elevation service.
So great, we are "elevated"—whatever that means—we own the box, right? Well… not so fast. Did you notice that the
whoami command didn't work up above?
Working Around Safety Measures
As stated in the overview, the elevation service has some safety constraints on elevated processes. One of these constraints is not allowing elevated processes to create child processes.
In the screenshot above, I ran
dir and then
ping. The first command works, and the second does nothing. Why? Well
dir is a builtin:
cmd.exe can execute that command without creating a subprocess. But
ping not built in, so
cmd.exe has to find it in the path and run it. Somehow, the elevation service is preventing me from doing that.
Tracing through the command shell in a debugger, I came across an internal Windows API called
CreateProcessInternalW. What is the first instruction in this function?
It is a
jmp into some function in the
lwl_userapp_monitor segment; this corresponds to a DLL that the elevation service loaded into this process. This is an example of userspace hooking, i.e. redirecting Windows API calls so that they can be monitored or modified. Antivirus often uses a similar technique, as well as some malware.
I tried single stepping past the jump for a little while, but it's really complicated code relative to my beginner capabilities as a reverse engineer. The upshot is that
CreateProcessInternalW never finishes running: the hook code returns directly to the caller.
CreateProcessInternalW is located in
kernelbase.dll, so if we load that up in IDA we can see what the first few bytes of this function are supposed to be:
The first instruction,
push 0xa00, is 5 bytes long, which is the same length as the
jmp hook. Back in the debugger, we can remove the hook by overwriting the jump instruction with the machine code for the push instruction:
68 00 0a 00 00.
After applying this patch we can continue running the target, and now we find that our shell can run subprocesses!
However, the shell still has some restrictions. For example, I can write a file to
\Windows\System32, which an ordinary user cannot do, but I cannot overwrite or delete files that already exist.
There are probably some attacks possible with this write primitive, or some other ways to take advantage of this elevated shell. I'm not a Windows security expert, so maybe some good folks on the interwebz will enlighten me. Instead, I set about trying to escalate to a less restricted shell.
Although the current shell is restricted in ways that I don't totally understand, it does have a lot of powerful privileges:
One surprising fact that I learned while researching this vulnerability is that the process has all of the privileges listed under
whoami /priv, even if a privilege is disabled! A process can enable one of its privileges with the
AdjustTokenPrivileges API. Two privileges in particular are very powerful:
SeImpersonatePrivilege, both of which I have in my elevated shell.
My next step was to write a stand-alone program that enables these two privileges, then uses those privileges to copy an access token from a system process into a new
cmd.exe. I won't post the full code here, but Andre Marques' detailed blog post about Windows security tokens and impersonation was an invaluable resource for me to figure this out. The basic process is:
- Enable the debug and impersonate privileges.
- Open a handle to your parent process, which in this case is
lwl_elevation_service.exeand running as
- Copy the access token from the parent process.
- Create a new
cmd.exewith the copied token (using the
CreateProcessWithTokenWAPI—see note below).
I compiled this into
MyTest2.exe, which as you saw at the outset, is on my whitelist for elevated applications.
In this screenshot, the window all the way in the back is my low-privilege shell. The window in the middle is the process that the elevation service creates to run my process at an elevated level. And the front window is my system shell.
Great! So what does this prove? Well, a process that is elevated by ProfileUnity can escalate to
SYSTEM, bypassing any of the restrictions that are supposed to be in place. But so far, this demonstration relies on the fact that I used an administrator account to whitelist
MyTest2.exe. A savvy administrator should be careful about which applications they whitelist, and how they whitelist them.
In the configuration that I showed at the beginning, any process named MyTest2.exe is elevated. This makes it trivial for an attacker with access to the machine to run malicious code with elevated privileges. A possible mitigation here is to put elevated executables in a secure location (not world writable), and specify the whitelist rule as a full path, i.e.
Exploiting the Default Configuration
What if the system administrator hasn't whitelisted any elevated processes? Is the elevation service secure in its default configuration? Unfortunately, the answer is no, because the default configuration still whitelists all executables signed by Liquidware itself. This default has two serious consequences:
- Almost any code execution vulnerability in any signed LiquidWare executable will allow an attacker to escalate to
- If LiquidWare's signing keys are compromised, then an attacker can sign malware and run with it with elevated privileges, without needing to compromise the software distribution channel itself.
The second bullet is mostly hypothetical: it illustrates the inherent trust that ProfileUnity customers must place on the vendor to adequately protect signing keys.
The first bullet, however, is not hypothetical. ProfileUnity ships with dozens of signed executables, and each one of them increases the attack surface for LPE. As I browsed through them, one stood out.
LouZip.exe is a compression tool similar to WinZip except that it supports ProfileUnity's proprietary compression format in addition to several mainstream formats.
This signed executable is installed as part of the "client tools" package that is automatically installed on every client machine managed by ProfileUnity. Alternatively, an old version can be downloaded from LiquidWare's website without needing a ProfileUnity license.
The help text for LouZip indicates that it supports
*.lou files (a proprietary format) as well as
*.7z (the open source 7z format). (Notice in the example above, LouZip did not run as an elevated process because it is not in the path.)
In Process Monitor, we can see that LouZip implements 7z compression by loading a file from
.\7z\x64\7z.dll. Of course, our low privilege user owns this file and can overwrite it, leading to a classic DLL highjacking vulnerability.
I reused the payload from
MyTest2.exe above and turned it into a DLL, with the malicious code running inside
DllMain. I overwrote
7z.dll with this malicious payload. Finally, to get a system shell, I just need to unzip any
*.7z file using the full path to
So there you have it, a local privilege escalation from unprivileged user to SYSTEM using the default ProfileUnity configuration.
For customers still running a vulnerable version of ProfileUnity, the mitigation scenarios are a bit complicated. LouZip is already signed and available for public download, and revoking the certificate seems unlikely because LiquidWare signs dozens of other binaries with it. My personal recommendations for now are to do one of the following:
- Modify the whitelist in
lwl_elevation_service.xmlto white list individual applications either by file hash or full path.
- If you aren't using the elevation service, it seems like it might be possible to disable it completely.
- 2020-01-07: Hyperion Gray disclosed the issue to our penetration test customer.
- 2020-02-14: Hyperion Gray disclosed the existence of the vulnerability to Liquidware in Profile Unity 6.8.2.
- 2020-02-17: LiquidWare indicated that they had fixed some vulnerabilities in 6.8.3. LiquidWare declined to agree to a coordinate disclosure agreement.
- 2020-04-02: Hyperion Gray notified LiquidWare of our intent to publish on of after April 13.
- 2020-04-16: Initial publication of this article.