Analysing IIS Compilation artifacts

Published: 07/07/2024

If you’ve ever conducted incident response or a forensic investigation on a Microsoft IIS webserver, you’ve likely come across some pretty weird looking file paths similar to these:

IIS Process Hacker

Everything loaded under the SharedDomain looks fairly reasonable and appears to be coming from the Global Assembly Cache (GAC) but C:\Windows\Microsoft.NET\Framework64\v4.0.30319\Temporary ASP.NET Files\root\e22c2559\92c7e946\App_Web_x1elc5dt.dll looks incredibly suspicious.

In this post I’ll cover what these suspicious files are and what benefit they can provide during an investigation.

Breaking down the mysterious path

Let’s start with that incredibly suspicious looking file path as it turns out it can provide us with quite a bit of information.

C:\Windows\Microsoft.NET\Framework64\v4.0.30319

There’s nothing to surprising here, just the install location of the .NET Framework version we’re using. All this really tells us is we’re dealing with a 64 bit host and IIS is using version v4.0.30319 of the .NET Framework Common Language Runtime (CLR). It’s important to note that v4.0.30319 is the version of the CLR not the version of the .NET Framework IIS is using. The values you’re likely to encounter in the real world are:

  • v4.0.30319 when .NET Framework version 4.x is in use
  • v2.0.50727 when .NET Framework version 2.x or 3.x are in use

Temporary ASP.NET Files

This value is hardcoded into IIS ( as part of System.Web.HttpRuntime.SetUpCodegenDirectory() for those interested)

Path.Combine(HttpRuntime.s_installDirectory, "Temporary ASP.NET Files");

root

Referred to as the SimpleAppName internally, this name is derived from the application’s name by stripping the leading / character, converting the value to lowercase and replacing all remaining / characters with _. If the length of the application name is one (i.e. the application name is /) a default value of root is returned.

So in our situation, our application is running under the root application on out webserver. When dealing with real world applications like Microsoft Exchange, its common to see this value set to ecp for the Exchange Control Panel or owa for the main Exchange OWA logon application.

e22c2559 and 92c7e946

I’ll tackle the next two together as they are intertwined however we’ll have to start with the latter value first.

92c7e946 is the product of taking the application’s ID (in our case /LM/W3SVC/1/ROOT), converting it to lower case then appending the web root directory (e.g. C:\inetpub\wwwroot\) before running the resulting string through a custom hashing function located in System.Web.Util.StringUtil.GetStringHashCode().

e22c2559 is calculated through a second hashing function (System.string.GetLegacyNonRandomizedHashCode()) by passing in the previously calculated hash (92c7e946)

TLDR:

hash(tolower("/LM/W3SVC/1/ROOT") + "C:inetpubwwwroot") = 92c7e946
hash("92c7e946") = e22c2559

App_Web_x1elc5dt.dll

And lastly, the weirdly named dll file. Sadly there’s no mysterious meaning to this file name, App_Web_ is hardcoded in IIS’ source and the random looking value is just that, a random value generated by Path.GetRandomFileName(). I will say that in my day job as a threat hunter I have encountered files that have variants on this naming convention including, some of which retain partial file names (e.g. App_Web_Logon_aspx.dll) but I haven’t pinned down the code path that determines when a file keeps its original name vs getting a random name

Now that we understand the meaning of the location of these weird assemblies, lets move onto…

Decompiling the suspicious assembly

I’ll be using dnSpy to decompile the suspicious assembly however as this post isn’t a “how to” on assembly analysis (let me know if there’s interest in a post on that) I’ll be focusing on interesting artifacts we can extract rather than stepping through the entire process.

Metadata

The first value of interest is the assemblies metadata. Most of the information available to us was already determined through our earlier file path analysis however one crucial piece of information is available to us through assembly metadata, the timestamp of when this assembly was compiled. Assembly metadata The timestamp is displayed as a hex encoded unix timestamp with no time zone applied as well as a formatted local date/time value. Checking the IIS logs for this server, we can see that a request for /zeroed.aspx came in at 2024-07-07 00:40:04. IIS log files are recorded in UTC so if we add 10 hours to account for me living in Melbourne Australia we can see that this assembly was compiled at the same time as our web request.

#Software: Microsoft Internet Information Services 10.0
#Version: 1.0
#Date: 2024-07-07 00:40:28
#Fields: date time s-sitename s-ip cs-method cs-uri-stem cs-uri-query s-port c-ip cs-version cs(User-Agent) cs(Referer) cs-host sc-status sc-substatus sc-win32-status sc-bytes cs-bytes time-taken ScriptName ApplicationPoolPhysicalPath
2024-07-07 00:40:04 W3SVC1 127.0.0.1 GET /zeroed.aspx - 80 127.0.0.1 HTTP/1.1 Mozilla/5.0+(Windows+NT+10.0;+Win64;+x64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/126.0.0.0+Safari/537.36 - 127.0.0.1 200 0 0 1072 681 235 /test.aspx C:inetpubwwwroot

Classes

Expanding the ASP node shows us this assembly contains four classes ending in _aspx including one that lines up with the above logged request for /zeroed_aspx. ASP tree

Opening this class we can see that zeroed_aspx extends the System.Web.UI.Page class and overrides several methods. Whilst most of these methods are boring, there’s several values that have some value.

__Render__control1 contains the contents of the .aspx file that was compiled to create this assembly. In this case its a simple test application which outputs a string and the current time. Render Control When dealing with real world compromises, this will be the main function you’re interested in as it can contain anything from webshell logic and adversary infrastructure through to entire adversary payloads.

The constructor (zeroed_aspx()) for our class contains what appears to be a file path prefixed with a ~. ~ is typically a short hand way of referencing the current users home directory, but web servers don’t have home directories do they? It turns out they do but that will have to be saved for another post, in this case the ~ actually references the webroot of the current site. In our case, this is C:\inetpub\wwwroot\. Constructor

We can confirm that ~/zeroed.aspx is referencing a file under C:\inetpub\wwwroot\ by navigating to this directory Web Root We can see there is indeed a file called zeroed.aspx within our webroot. Not only that, we can confirm its content closely lines up with what we saw in the __Render__Control1 function above!

Whilst this was a very trivial example, this technique can be great for tracking down webshells that have ben stashed away deep in a web applications directory structure.

What about the other classes?

We’re done with zeroed_aspx_ but you’ll recall when we expanded the ASP node in dnSpy that there were four classes there. You may have noticed that some of the remaining classes names line up with the other files present in our webroot:

  • login_aspx => login.aspx
  • test_aspx => test.aspx
  • zeroed_aspx => zeroed.aspx
  • __aspx => ?????

The first three make sense but what’s up with that last entry? It turns out, class names cannot handle emojis so they are stripped out. If we open __aspx and checkout its referenced file location we get ~/\ud83d\ude01.aspx where \ud83d\ude01 are the unicode escaped characters for 😁. emojiPath

Batch compilation

The last topic I wanted to cover is why are there multiple classes in this assembly? I’ll be diving more into how this works in a future blog post so for today, lets just leave it at “its an optimisation thing”. IIS’ compilation process is quite complex time consuming so if you can precompile all aspx files at the same time, it is a massive time save. One thing to keep in mind with this batch compilation is it can cause malicious code such as a webshell to be compiled and loaded into IIS’ memory with no adversary interaction occurring.

As an example of this, we checked our IIS logs earlier and saw our server only ever processed a single request for /zeroed.aspx however if we take a look inside the login_aspx class we can see that login.aspx is actually a Behinder based webshell! Behinder So by performing a request to our completely benign date printing page, we inadvertently caused a pre-existing webshell to be compiled and loaded. Its important to note that just because this compiled webshell was loaded, doesn’t mean it was executed or performed any form of action, its just sitting there waiting for a request like any other page.

Assemblies are forever

Investigating webshells sucks. The only thing worse than investigating webshells is seeing evidence of a webshell in IIS logs only to have the file deleted just before you got to the host. But did you know, the .NET Framework does not provide a mechanism for unloading .NET assemblies? Once a .NET assembly has been loaded, it’ll hang around in memory for the life of the process. In addition, the file on disk will be locked by IIS, preventing its deletion until the current instance of IIS terminates. This means, if you can get to a host in the brief window after and adversary has deleted their webshell but before the the current IIS process has terminated, you may find an old App_Web_*.dll file laying around which contains the deleted webshells logic. I say brief but for most servers that are seeing continuos traffic, this window could be anywhere from a day to several months!

Wrapping up

That all I’ve got for this post, hopefully it helps someone with an upcoming investigation.

This post is intended as prior reading for an upcoming post I’m working on in which I’ll deep dive the IIS build process, covering how we go from an ASPX file on disk to a compiled assembly in memory. Stay tuned.