Pwn2Own or Not2Pwn, Part 3: The lazy man’s escalation

Cim Stordal
Cognite
Published in
10 min readMar 1, 2021

Welcome back to our Pwn2Own or Not2Pwn series about our Schneider Electric EcoStruxure Operator Terminal Expert exploit attempt. To catch you up: In part 1, we gave an overview of the full chain. In part 2, we detailed the initial code execution exploit, and in part 2.5, we offered an additional set of vulnerabilities that can be used for code execution. In this final entry in the series, we’ll detail how to escalate privileges into system-level privileges.

Due to a misunderstanding of the Pwn2Own rules, we assumed a privilege escalation was required for a successful entry. This wasn’t the case. (Note to self: Always read the rules closely.) Luckily it didn’t take long before we found one anyway.

Finding the bug

When looking for a way to escalate privileges in Windows, a few obvious methods stand out. You can audit the kernel code or system services for memory corruption bugs or look for file-based logic bugs. File-based bugs can be further broken down into multiple categories, for example configuration bugs and file operation hijacking bugs. We didn’t want to deal with memory corruptions, so we decided to look into file-based bugs.

Configuration bugs

One approach is to look for wrongly setup services, with one example being unquoted service paths. We found an issue like that with the XBTZG935 USB Link Cable service, which resulted in CVE-2020–7497. However, since we didn’t have permission to restart the service, and since Pwn2Own didn’t allow reboots, this bug wasn’t quite sufficient for our purposes.

Incorrect configurations can also happen in relation to access permissions for certain folders. For example, a directory in the PATH can be set to be writable by all users. CVE-2021–22665, which both Cognite and Claroty discovered in Rockwell Automation, is one example. However, we didn’t find anything similar in this application.

File operation hijacking bugs

Probably the easiest way to find third-party Local Privilege Escalations (LPEs) is to look at file operation hijacking bugs. So, as one does, we threw up ProcMon and started to open some project files to see if anything interesting happened. We quickly saw that a system process would attempt to set Access rights(SetSecurityFile) for the C:\ProgramData\FLEXnet directory — and on the files inside.

FNPLicensingService64.exe from FlexNet is a 64-bit service which is running as NT AUTHORITY\SYSTEM. This service is responsible for validating the license. Developed by Revenera (formerly known as Flexera), the service is used by quite a few companies.

To see this for yourself, open ProcMon and set a filter for integrity is System and Path contains “C:\ProgramData\FLEXnet”. If you then double-click the EcoStruxure Operator Terminal Expert V3.1 icon on the desktop, your ProcMon logs will look like this:

Here we see a bunch of file operations performed toward the Flexnet directory: CreateFile operations, SetSecurityFile operations, and so on. There will be multiple SetSecurityFile calls toward the Flexnet directory, but marked in blue you will see the interesting one: the one setting the DACL. This is our bug.

Fun fact about this approach: Not only do we not have to worry about memory corruption, but we also don’t have to read any code. It’s the lazy man’s exploit!

At this point you’re probably hearing SetSecurityFile and DACL and not really understanding how it all fits together. No worries, we’ll cover this in the next section.

File operation abuse — what is it?

For a primer on file operation abuse, feel free to scale the mountain of bug reports from people like James Forshaw and read the accompanying blog posts. Alternatively, this blog post gives a good summary.

With the SetSecurityFile operation above, for example, the goal is to redirect that action from the FlexNet directory toward a file or directory of our choosing. This specific SetSecurityFile call will update the Discretionary Access Control List (DACL) information. This is basically just a list of which users have what kind of access for a specific file or directory. In this specific case it will add a new entry giving everyone read, write, execute, and modify permissions.

How does one hijack file actions, you may ask? Symlinks and hard links to the rescue. Thanks to people like James Forshaw, we didn’t have to invent any new techniques (the blog post mentioned above also gives readers a nice introduction to the various exploitation techniques). All we need to know is that by hard linking or symlinking a file to another file, we can redirect or hijack the file operation performed on it.

Thanks to this SetSecurityFile operation and the ability to redirect that action, we now have a way to get write permission to a file. Now we need a good way to trigger it.

Pulling the trigger

To exploit this bug, we need to find a reproducible and reliable way to trigger it. Sure, opening the EcoStruxure program works, but that’s a pretty noisy exploit. We need to find something a bit smarter. We discussed it and came up with two approaches:

  1. The bug was caused by sending some messages over named pipes. It became clear that we could replay the messages on the same machine — at least for a period of time. However, something changed when attempting it on a different installation. We were unable to replay the same messages on two different installations. One way would be to reverse-engineer the communication protocol, so that we would be able to craft and send our own messages.
  2. We also knew that the bug would trigger when we started the EcoStruxure program. How about just finding the function that sends the messages and calling that one?

Continuing on the path of least resistance (are you seeing a trend here?), the second option seemed the easiest, so we went with that. We then tracked down the function that sends the messages: brickWrapper.LMActivateTrialLicense(job);.

Arbitrary SetSecurityFile to code execution

By giving ourselves permission to modify toward that one file, we now have a way to modify any file we want on the system. But we’re not done yet. What do we have to modify in order to get code execution as the system user?

If there’s a system service that a normal user can stop and start, we can use our logic bug trick without any further bugs: Stop the service, overwrite the service to be our fake service, restart it, and our fake service will then be running with system-level privileges. But looking at the installed services, we didn’t find any that gave us such permissions.

We had a couple of other ideas for how we could exploit it:

  1. Crash a system service that we are allowed to start.
  2. Find a service that attempts to load a missing DLL.
  3. Find scripts or other things that will be executed as system but are not cached, for example .sh files.

Verify service permissions:

We want to attack one of the system services added when installing this application. The Flexnet licensing manager seems as good as any, as that’s where our logic bug is. A nice side effect of this is that our exploit chain will work for all systems with the licensing manager installed, as it does not depend on any other software.

Looking at the access rights for an interactive user who is signed in, we see:

IU is short for interactively logged-on user.(A;;CCLCSWRPLOCRRC;;;IU)CC — SERVICE_QUERY_CONFIGLC — SERVICE_QUERY_STATUSSW — SERVICE_ENUMERATE_DEPENDENTSRP — SERVICE_STARTLO — SERVICE_INTERROGATECR — SERVICE_USER_DEFINED_CONTROLRC — READ_CONTROL

As we see, we don’t have WP, the access right for stopping a service — an important primitive if we want to overwrite the service. However, we do have the right to start the service if we want. Luckily we have a primitive for crashing the service, allowing us to stop it.

Crashing the service

As the old saying goes, if you can’t kill them, crash them (or something along those lines). When listing available named pipes, there are two that stand out. These two are also where the communication triggering our logic bug is sent.

FlexNet Licensing Service 64ABF27A87-DC96–4b05-A06B-83EB2749B800

FlexNet Licensing ServiceABF27A87-DC96–4b05-A06B-83EB2749B800

One refers to the 32-bit service; the other, the 64-bit service.

We made a low-effort attempt to understand the communication protocol (looking at the first package from the communication triggering our logic bug) and determined a region we assumed to be payload size. When setting it to a huge value and sending it to the 32-bit process, the process crashes. That’s all we really need to know in order to exploit this. We didn’t even check why it crashed — again, super lazy stuff.

In step two of our logic bug chain, we need to crash the service we want to overwrite, because we don’t have access rights to stop or restart the service. We do however have access to start it. To crash it we simply send the message shown in the picture. And the process crashes.

Exploiting the service

Exploiting the service is quite simple. Here are the various steps our final exploit took:

  1. Move Flexnet directory somewhere else.
  2. Crash the service we will hard link to later on.
  3. Use the Native-HardLink powershell script to hard link from C:\ProgramData\FLEXnet to the system service file we want to overwrite. We chose C:\Program Files (x86)\Common Files\Macrovision Shared\FlexNet Publisher\FNPLicensingService.exe.
  4. Call brickWrapper.LMActivateTrialLicense(job); to trigger the logic bug, giving us write permissions to FNPLicensingService.exe. brickWrapper can be found in C:\Program Files\Schneider Electric\EcoStruxure Operator Terminal Expert V3.1\BuildTime\SELMBrick.dll.
  5. Overwrite the FNPLicensingService.exe service with our own service.
  6. Call start on the service.

The hard link mitigation

At one point we received an email from Schneider Electric saying “Revenera categorizes this bug as a Microsoft problem solved with Windows 10 2004.”

In Windows 10 version 2004 (the version released in the first half of 2020), the hard link technique we used got patched. Fair enough — even back in mid-2019 it was known that they eventually would patch that technique. Does this mean that the vulnerability is nonexploitable?

Sadly no. We can test this on 19042.572, the Windows 10 H2 update from Oct. 8, 2020. If we replace the FlexNet directory with a MountPoint for example pointing to the System32 folder, we will see that a new file will be created in that directory and that we have write permission. Interesting. If we are also able to control the file name, then this should be fairly exploitable.

Controlling the file name is quite easy. Using the CreateSymlink.exe tool that James Forshaw made, move the current FlexNet directory and replace it with an empty directory. Then, create a symlink:

PS C:\Users\Cim Stordal\Downloads\symboliclink-testing-tools-master\symboliclink-testing-tools-master\Debug> .\CreateSymlink.exe C:\ProgramData\FlexNet\schneide_0098df00_tsf.data C:\Windows\System32\mytestOpened Link \RPC Control\schneide_0098df00_tsf.data -> \??\C:\Windows\System32\mytest: 00000144Press ENTER to exit and delete the symlink

Finally, trigger the bug. Either call the function directly, as we did in our exploit, or — if you are just testing — just open a Schneider Electric EcoStruxure Operator Terminal Expert project file, and it will be called.

You will now have a file in System32 that you control the name of and have write permission to. Why does this work? Think back to the output we saw in ProcMon. Not only were there SetSecurityFile calls, but also createFile operations. This time, instead of using the SetSecurityFile operation, we are hijacking one of the createFile calls.

We here demonstrate the ability to create arbitrary files in restricted folders. That alone is not a full privilege escalation, and we’ll leave it as an exercise to the readers to chain this into one. The crash primitive discussed earlier in this post is patched, but let’s just say you may get lucky if you look for missing DLLs that are attempted to be loaded.

It has been over a year since our original report, and Revenera has since multiple times concluded that it is patched or not a vulnerability, without patching anything, nor issuing a CVE. As you can see the vulnerability can still be used to do privilege escalation. The team at Revenera are now finally working toward mitigating this vulnerability.

Timeline

  • Feb. 5, 2020: Vulnerability report sent to Schneider Electric.
  • May 13, 2020: Update: Schneider Electric is still waiting for Flexera to complete its analysis.
  • Sept. 18, 2020: Schneider Electric considers the vulnerability patched. “License Manager LM Brick v2.1.0.0 includes FNP V 11.14.1, which fixes the ‘crash bug.’ We see no other possibility to restart the FNP service. Since the ‘Logic Bug’ cannot be exploited without service restart, we consider the vulnerability as fixed.”
  • Oct. 7, 2020: Call with Schneider Electric about the privilege escalation.
  • Dec. 18, 2020: Revenera categorizes this bug as a Microsoft problem solved with Windows 10 2004.
  • Jan. 6, 2021: Cognite responds with a draft of this blog post, demonstrating the exploitability post-Windows 10 2004.
  • Jan. 20, 2021: Video call during which Cognite demonstrates the bug on the latest Windows version.
  • Feb. 10, 2021: Revenera replies that they can reproduce the bug, but asks if it should be treated as a Windows OS bug rather than a product-specific bug.
  • Feb. 10, 2021: Cognite sets March 1 as the publication deadline for this blog post.
  • Feb. 15, 2021: New video call meeting: Revenera now considers this a bug and asks for 90 days before Cognite publishes. Cognite denies that request.
  • March 1, 2021: Blog post published.

By Fredrik Østrem, Emil Sandstø and Cim Stordal

--

--