โŒ

Normal view

There are new articles available, click to refresh the page.
Today โ€” 2 May 2024Securifera

Okta Verify for Windows Remote Code Execution โ€“ CVE-2024-0980

By: b0yd
2 May 2024 at 17:41

This article is in no way affiliated, sponsored, or endorsed with/by Okta, Inc. All graphics are being displayed under fair use for the purposes of this article.

Poppin shells with Okta Verify on Windows

These days I rarely have an opportunity to do bug hunting. Fortunately, over the holiday break, I found some free time. This started as it usually does with me looking at what software was running on my computer.

A while back I had installed Okta Verify on my Windows box as it was required for some โ€œenhancedโ€ 2FA that I was required to have to access a thing. Months later it sat there doing whatever it does. I googled to see if Okta had a bug bounty program because even though I had some time, itโ€™d be nice to get paid if I found a thing. I was thrilled to find that Okta had a bug bounty with Bugcrowd, Okta Verify is in it, and the payouts look good, almost too good.

I started with my usual bug hunting flow when approaching a random Windows service. This typically includes looking for the usual low hanging fruit. A good article for the types of things to look for can be found here.

Firing up Sysinternalโ€™s Procmon, I saw there is a service called Okta.Coordinator.Service that is running as SYSTEM. Without going into the details (namely because Okta hasnโ€™t fixed it or issued it a CVE),ย I found a thing. I submitted the report and was promptly paid.

Well thatโ€™s weird. The bug I submitted is an unequivocal 7.8 CVSS. Which without knowing the voodoo behind P ratings (P1-P4), seems like would be a P2 at least. Instead I get a P3 and paid out at the lower end.

Looking back on it, Iโ€™m betting this is probably an old bug bounty program trick to motivate researchers to dig deeperโ€ฆ because, it worked. I decided to take a closer look since I hadnโ€™t even opened up the binary to see what it was doing โ€“ and I wanted to get that big payout.

Letโ€™s Get Crackinโ€™

I havenโ€™t popped Okta.Coordinator.Service.exe into a disassembler yet, but Iโ€™m betting itโ€™s a .NET application. My guess comes from its name and the fact that thereโ€™s an Okta.Coordinator.Service.exe.config file right there with it, which you usually see with .NET applications.

When I open up the executable in JetBrains dotPeek, I can confirm it is indeed a .NET application. The binary appears to be a service wrapper. It handles the service related functionality: install, uninstall, start, stop, etc.ย  It references a Okta.AutoUpdate.Executor class that just so happens to have a matching DLL in the same directory.

Moving on to the DLL in dotPeek, I found the code used by the service. The first thing I noticed was it sets up a NamedPipe server, which listens for commands to update the Okta Verify software. This is a common design paradigm in Windows for enabling low-privileged applications to communicate with a high-privileged service to perform updates, as these often require elevated privileges. Itโ€™s a complex mechanism thatโ€™s tricky to do right, and often a good place for finding bugs. I was able to confirm the existence of the named-pipe server with a little Powershell.

Next, I investigated how to initiate an update and what aspects of this process could be manipulated by an attacker. The handler for the named pipe processes a straightforward JSON message that includes several fields, some checked against expected values. The primary field of interest is the update URL. If the input data passes validation, the software will attempt to fetch details from the specified URL about the most recent update package available. As shown below, the URL (sub)domain is verified against a whitelist before proceeding. For now, Iโ€™ll avoid trying to meet/bypass this requirement and simply add an entry in the hosts file on my test machine.

Typically at this stage, Iโ€™d code up a proof of concept (POC) to send a JSON message to the named pipe and check if the software connected to a web server I control. But since I havenโ€™t spotted any potential vulnerabilities yet, I skipped that step and moved on.

From here I took a look at the code responsible for processing the JSON message retrieved from the attacker controlled update server. The application is expecting a message that contains metadata about an update package including versioning and an array of file metadata objects. These objects contain several pertinent fields such the download URL, size, hash, type, and command line arguments. The provided download URL is validated with the same domain checking algorithm as before. If the check passes, the software downloads the file and writes it to disk. This is where things get interesting. The code parses the download URL from the received metadata and constructs the file path by calling the Path.Combine function.

Several factors are converging here to create a serious vulnerability. The most obvious is the use of the Path.Combine function with user supplied data. I went into depth about this issue in a previous blog post here. The TLDR is if a full path is provided as the second argument to this function, the first argument that typically specifies the parent folder, is ignored. The next issue is how the filename is parsed. The code splits the file location URL by forward slash and takes the last element as the filename. The problem (solution) is a full Windows path can be inserted here using back slashes and itโ€™s still a valid URL. Since the service is running as SYSTEM, we have permissions to right anywhere. If we put all this together our payload looks something like the script below.

Copy to Clipboard

Now that I have a potential bug to test out, I craft the POC for the named pipe client to trigger the update. Luckily, this code already exists in the .NET DLL for me to repurpose.ย  With my web server code also in place I send the request to test out the file write. As I had hoped, the file write succeeds!

Cool, but what about impact!

I have the ability to writeย arbitraryย files as SYSTEM on Windows. How can I leverage this to achieve on-demand remote code execution? The first thing that comes to mind is some form of DLL hijacking. Iโ€™ve used phantom DLL hijacking in the past but this is more appropriate for red team operations where time constraints arenโ€™t really an issue. What I really need is the ability to force execution shortly after the file write.

Since the whole purpose behind this service is to install an update, can I just use it to execute my code? I reviewed the code after the file write to see what it takes to execute the downloaded update package. It appears the file type field in the file object metadata is used to indicate which file to execute. If the EXE or MSI file type is set, the application will attempt to validate the file signature before executing it, along with any supplied arguments. The process launcher executes the binary with UseShellExecute set to false so no possibility of command injection.

My original thought was to deliver a legitimate Okta Verify package since this would pass the signature check. I could then use ProcMon to identify a DLL hijack in the install package. Privileged DLL hijacks occur in almost all services because the assumption is you already require the permissions to write to a privileged location. Ironically though, I found the service binary actually contained a DLL hijack just prior to the signature verification to load the necessary cryptographic libraries. If I write a DLL to C:\Program Files (x86)\Okta\UpdateService\wintrust.dll, it will get loaded just prior to signature verification.

Great, so now I have a way to execute arbitrary code from an unprivileged user to SYSTEM. โ€œGuessingโ€ย that this probably wonโ€™t meet the bar of P1 or P2, I start thinking of how to upgrade this to remote code execution. If RCE doesnโ€™t get a P1 then what does? The interesting thing about named pipes is that they are often remotely accessible. It all depends on what permissions are set. Looking at the code below, it sets full control to the โ€œBUILTIN\Usersโ€ group.

Testing from a remote system in my network confirms that I get permissioned denied when I try to connect to the named pipe. After a couple of days I had an idea. If a Windows system is part of a Active Directory domain, does the BUILTIN/Users group permissions automatically get converted to the โ€œDomain Usersโ€ group in a domain? This would mean any user in an AD domain could remotely execute code on any system that has Okta Verify installed. Moreover, considering that this software is aimed at large corporate enterprises, it would likely be included in the standard build and deployed broadly. So not explicitly โ€œDomain Adminโ€ but a good chance of it. I had to find out, so I stood up a test AD network in AWS and the following video shows what happened.

Almost done

Well that seems like a big deal right? Maybe get a P1 (and 70kโ€ฆ)? Iโ€™m guessing the small detail about not having an Okta subdomain to download from may keep it from landing a P1. Having worked at big tech companies, I know that subdomain takeover reports are common. However, without having a subdomain takeover, itโ€™s likely the bugโ€™s significance will be minimized. I decided to dedicate some time to searching for one to complete the exploit chain. After going through the standard bug bounty subdomain takeover tools, I came up with only one viable candidate: oktahelpspot.okta.com.ย  It pointed to an IP with no open ports, managed by a small VPS provider named Arcustech.

After signing up for an account and some very innocent social engineering, I got the following response. And then a second email from the first personโ€™s manager. Oh well, so much for that.

The next thing that came to mind was leveraging a custom Okta client subdomain. Whenever a new client registers with Okta, they receive a personalized Okta subdomain to manage their identity providers, e.g. trial-XXXXXXX.customdomains.okta.com. I found a way to set custom routes in the web management application that would redirect traffic from your custom domain to a user defined URL.ย Unfortunately, this redirect was implemented in Javascript, rather than through a conventional 302 or 301 HTTP redirect. Consequently, the .NET HTTP client that the Okta Verify update service uses did not execute the Javascript and therefore did not follow the redirect as a browser would.

Reporting

At this point, I decided it was time to report my findings to Okta. Namely, because they were offering a bonus at the time for what appeared to be Okta Verify, which I think they might have either forgotten or included without mentioning. Secondly, I didnโ€™t want to risk someone else beating me to it. I am happy to report they accepted the report and awarded me a generous bounty of $13,337 as a P2. It wasnโ€™t quite $70k, or a P1, but itโ€™s nothing to sneeze at. I want to thank Okta for the bounty and quick resolution. They also kindly gave me permission to write this blog post and followed through with issuing CVE-2024-0980 along with an advisory.

One last note, if anyone reading this identifies a way to bypass the subdomain validation check I would be very interested. I attempted most of the common URL parsing confusion techniques as well as various encoding tricks all for naught. Drop me a line at @rwincey on X

โŒ
โŒ