Decrypting ASP.NET Identity cookies

I decided recently I need to learn Python. It’s a great scripting language, often used in forensics, diagnostics and debugging tools. There is even a plugin for windbg that allows you to script this debugger in Python language, but it’s a subject for another post. Moving back to learning Python – as an exercise I wrote a simple tool to decrypt ASP.NET Identity cookies and ASP.NET Anti-Forgery tokens. You may find it useful in situations when you need to diagnose why one of your users can’t sign in into your applications or is not authorize to access one of its parts. It does not perform validation but only decrypts the content using 256-bit AES (let me know in comments if you need some other decryption algorithm to be implemented). Adding validation logic shouldn’t be a big deal and the nist library (which I used for cryptographic operations) provides all the necessary functions.

The script goes as follows:

# -*- coding: utf-8 -*-
import base64
import logging
import argparse
import struct
import gzip
from StringIO import StringIO
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives import hashes, padding, hmac
from cryptography.hazmat.backends import default_backend
def derivekey(key, label, context, keyLengthInBits):
    lblcnt = 0 if None == label else len(label)
    ctxcnt = 0 if None == context else len(context)
    buffer = ['\x00'] * (4 + lblcnt + 1 + ctxcnt + 4)
    if lblcnt != 0:
        buffer[4:(4 + lblcnt)] = label
    if ctxcnt != 0:
        buffer[(5 + lblcnt):(5 + lblcnt + ctxcnt)] = context
    _writeuint(keyLengthInBits, buffer, 5 + lblcnt + ctxcnt)
    dstoffset = 0
    v = int(keyLengthInBits / 8)
    res = ['\x00'] * v
    num = 1
    while v > 0:
        _writeuint(num, buffer, 0)
        h = hmac.HMAC(key, hashes.SHA512(), backend=default_backend())
        hash = h.finalize()
        cnt = min(v, len(hash))
        res[dstoffset:cnt] = hash[0:cnt]
        dstoffset += cnt
        v -= cnt
        num += 1
    return ''.join(res)
def _writeuint(v, buf, offset):
    buf[offset:(offset + 4)] = struct.pack('>I', v)
def _tokendecode(aspnetstr):
    if len(aspnetstr) < 1:
        raise ValueError('Invalid input')
    # add padding if necessary - last character of the string defines the padding length
    num = ord(aspnetstr[-1]) - 48
    if num < 0 or num > 10:
        return None
    return base64.urlsafe_b64decode(aspnetstr[:-1] + num * '=')
def _decode(aspnetstr):
    # add padding if necessary
    pad = 3 - ((len(args.aspnetstr) + 3) % 4)
    if pad != 0:
        aspnetstr += pad * '='
    return base64.urlsafe_b64decode(aspnetstr)
def decrypt(dkey, b):
    # extract initialization vector (256 bit)
    iv = b[0:16]
    decryptor = Cipher(algorithms.AES(dkey), modes.CBC(iv), backend=default_backend()).decryptor()
    unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder()
    ciphertext = b[16:-32]
    text_padded = decryptor.update(ciphertext) + decryptor.finalize()
    return unpadder.update(text_padded) + unpadder.finalize()
if __name__ == '__main__':
    # Turn on Logging
    logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(message)s')
    parser = argparse.ArgumentParser('ASP.NET encryptor/decryptor')
    parser.add_argument('aspnetstr', metavar='aspnet-text', help='ASP.NET encrypted text')
    parser.add_argument('-skey', required=True, help='Symmetric key for AES encryption/decryption')
    parser.add_argument('-enctype', required=False, help='Type of action that generated the given encryption text (owinauth or antiforgery)')
    args = parser.parse_args()
    skey = args.skey.decode('hex')
    label = None
    context = None
    compressed = False
    encrypted = None
    if args.enctype == 'owinauth':
        label = b'>Microsoft.Owin.Security.Cookies.CookieAuthenticationMiddleware\x11ApplicationCookie\x02v1'
        context = b'User.MachineKey.Protect'
        compressed = True
        encrypted = _decode(args.aspnetstr)
    elif args.enctype == 'antiforgery':
        label = b'/System.Web.Helpers.AntiXsrf.AntiForgeryToken.v1'
        context = b'User.MachineKey.Protect'
        encrypted = _tokendecode(args.aspnetstr)
    dkey = derivekey(skey, context, label, 256)
    decrypted = decrypt(dkey, encrypted)
    if compressed:
        decrypted = gzip.GzipFile(fileobj=StringIO(decrypted)).read()
    print "%s %s" % (decrypted.encode('hex'), decrypted)

It works only with keys explicitly defined in web.config in machineKey section, eg.

    <machineKey decryption="AES" decryptionKey="22d14047d53135334cb08d4b4d7da1dcfccd0eae9e66fea0b8dfdcdca085a683"
                validation="HMACSHA256" validationKey="c2aec26d010bb4224ab2189184cca3c1b43ae9688026ae4a2f851fbf5521c73f" />
    <httpRuntime targetFramework="4.5" />

Example calls:

PS python-crypto> python .\ -enctype owinauth -skey BE5CF08F3D2E21DB3601E280503BF78E4EBD02D49245DCB37057DD1369A5172B sIqR0-eLDTb5LBvpH54dU4LI-qPIF4a5EirVltpf7FEPWVnKsyh6-djZWag2_fs5a7OietPNO-_DmfQKJrYSeGbbjf5Dt5CqWscqgKQSCjvBDevOEKUW4TS0Zm8VJA58rlpE877pybvFy_EifvdK8Dk_zIZYRPhNNHHHffDtBqmD2ocIv6NkY4NkvEtmbRK04c7oLQMM-92LMAQWk-SfCoRTUeWljOPNrd6eQ5XZ97uwuVi6smGJ0uXoYAv_eFNJjkNg1EL82VKu2t4AMvxGgf1T6YAdC4pJvl9zlb8ew07vVa6tSzcPrpe-KW2FhHUOHHnFUh7KOkloG-WWv_ePvKv02jusSWHFnbi3f7zK7eksbVNcoT0J_Gce9wELMa3aBM3u56cIKViVhIQAzWg6nFRTsUh8LIxHXVOnFhk7-3jndshd-QDv_KJ1C6rEyjIdgu61-2n2_jI-s3dt4fr70IG_U5dq6gGms3uXtLInEbXezTBxMW4RFGrqGafjtMc-BOSmGcCKXjskgbxtkZPyu2GiKTnOSncPMPv9bPP6dOI
02000000114170706c69636174696f6e436f6f6b6965010001000700000044687474703a2f2f736368656d61732e786d6c736f61702e6f72672f77732f323030352f30352f6964656e746974792f636c61696d732f6e616d656964656e7469666965720a3137343834303230393001000100010001000873736f6c6e69636101000100010051687474703a2f2f736368656d61732e6d6963726f736f66742e636f6d2f616363657373636f6e74726f6c736572766963652f323031302f30372f636c61696d732f6964656e7469747970726f7669646572104153502e4e4554204964656e746974790100010001001d4173704e65742e4964656e746974792e53656375726974795374616d702464333830613932392d663538302d343038392d626263392d3535623238613439326538640100010001002e687474703a2f2f6170706c69636174696f6e2f636c61696d732f617574686f72697a6174696f6e2f616374696f6e0e52573a5a616c6f67756a4a616b6f0100010001002e687474703a2f2f6170706c69636174696f6e2f636c61696d732f617574686f72697a6174696f6e2f616374696f6e0e52503a5a616c6f67756a4a616b6f0100010001001575726e3a64703a6c6f6767656475736572747970650149010001000100000000000100000002000000072e6973737565641d5765642c203239204f637420323031342031323a31303a313720474d54082e657870697265731d5765642c203132204e6f7620323031342031323a31303a313720474d54 ☻   ◄ApplicationCookie☺ ☺    D
1748402090☺ ☺ ☺ ssolnica☺ ☺ ☺ Q►ASP.NET Identity☺ ☺ ☺ ↔AspNet.Identity.SecurityStamp$d380a929-f580-4089-bbc9-55b28a492e8d☺ ☺ ☺ .http://application/claims/authorization/action♫RW:ZalogujJako☺ ☺ ☺ .http://application/claims/authorization/action♫RP:ZalogujJako☺ ☺ ☺ §urn:dp:loggedusertype☺I☺ ☺ ☺     ☺   ☻   .issued↔Wed, 29 Oct 2014 12:10:17 GM.expires↔Wed, 12 Nov 2014 12:10:17 GMT

PS python-crypto> python .\ -enctype antiforgery -skey 22d14047d53135334cb08d4b4d7da1dcfccd0eae9e66fea0b8dfdcdca085a683 22d14047d53135334cb08d4b4d7da1dcfccd0eae9e66fea0b8dfdcdca085a683 IgoPD31Z0v-eCQyBZeu_wL-Vvr8IlmIpci-9iZD6F1S5Tf_HZb0GOMLEOjWU5aGjg_UtpYZ07vB3TRsrBEsJoTa5k4U1ygm68CmuBMaQ5G01
013382f4b0bc9f19dbce20a58893b4e32801 ☺3é˘░╝č↓█╬ ąłô┤Ń(☺

While writing this script I’ve learnt few interesting facts about encryption in ASP.NET. The keys you provide in the machineKey section are not directly used in ecnryption and validation logic, but derivative keys are created taking into account a context and a label (according to the NIST specification). A context in ASP.NET applications is a string: User.MachineKey.Protect. Label is different for each part of the framework. For ASP.NET Identity cookies it’s equal to >Microsoft.Owin.Security.Cookies.CookieAuthenticationMiddleware\x11ApplicationCookie\x02v1 when for Anti-Forgery token it’s /System.Web.Helpers.AntiXsrf.AntiForgeryToken.v1. Another value would be used for forms authentication cookies. You may find some of those values in the Purpose class source code. Another interesting fact is that base64 url encoding implementation differs between parts of the ASP.NET framework. Anti-Forgery tokens are encoded with one additional char specifying the number of padding characters (‘=’) when ASP.NET Identity cookies do not contain such information (padding characters number is calculated based on cookie value length modulo 4).

If you ever need to go deeper into ASP.NET cryptography, some interesting classes to look into are: MachineKey, AspNetCryptoServiceProvider, NetFXCryptoService, Purpose and SP800_108

4 thoughts on “Decrypting ASP.NET Identity cookies

  1. Nitin khosla February 17, 2020 / 00:35

    Token encrypted using 4.5. MachineKey on server has decryptionkey specified with algorithm = AES (decryption=”AES”).
    Used above script to decrypt the token which was encrypted by authenticationticket class in .net

    2 comments/questions:
    1) Getting incorrect padding error. What could be the reason?
    2) Script fails with python3.
    v = int(keyLengthInBits / 8) => returns float.
    res = [‘\x00’] * v => fails with non-int

    • Sebastian Solnica February 17, 2020 / 08:51

      It’s been a long time since I touched this script, so first, thanks for the error report – I updated the script.

      Regarding 1), this error usually signifies that you used an invalid key. Are you sure the key is correct? I wrote a more detailed article on the way how the old ASP.NET encryption works, so maybe it will help you, too:

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.