Posted by James Forshaw, Project Zero
One of the more interesting classes of security vulnerabilities are those affecting interoperability technology. This is because these vulnerabilities typically affect any application using the technology, regardless of what the application actually does. Also in many cases they’re difficult for a developer to mitigate outside of not using that technology, something which isn’t always possible.
I discovered one such vulnerability class in the Component Object Model (COM) interoperability layers of .NET which make the use of .NET for Distributed COM (DCOM) across privilege boundaries inherently insecure. This blog post will describe a couple of ways this could be abused, first to gain elevated privileges and then as a remote code execution vulnerability.
A Little Bit of Background Knowledge
If you look at the history of .NET many of its early underpinnings was trying to make a better version of COM (for a quick history lesson it’s worth watching this short video of Anders Hejlsberg discussing .NET). This led to Microsoft placing a large focus on ensuring that while .NET itself might not be COM it must be able to interoperate with COM. Therefore .NET can both be used to implement as well as consume COM objects. For example instead of calling QueryInterface on a COM object you can just cast an object to a COM compatible interface. Implementing an out-of-process COM server in C# is as simple as the following:
// Define COM interface.
[ComVisible(true)]
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
[Guid("3D2392CB-2273-4A76-9C5D-B2C8A3120257")]
public interface ICustomInterface {
void DoSomething();
}
// Define COM class implementing interface.
[ComVisible(true)]
[Guid("8BC3F05E-D86B-11D0-A075-00C04FB68820")]
public class COMObject : ICustomInterface {
public void DoSomething() {}
}
// Register COM class with COM services.
RegistrationServices reg = new RegistrationServices();
int cookie = reg.RegisterTypeForComClients(
typeof(COMObject),
RegistrationClassContext.LocalServer
| RegistrationClassContext.RemoteServer,
RegistrationConnectionType.MultipleUse);
A client can now connect to the COM server using it’s CLSID (defined by the Guid attribute on COMClass). This is in fact so simple to do that a large number of core classes in .NET are marked as COM visible and registered for use by any COM client even those not written in .NET.
To make this all work the .NET runtime hides a large amount of boilerplate from the developer. There are a couple of mechanisms to influence this boilerplate interoperability code, such as the InterfaceType attribute which defines whether the COM interface is derived from ITulisan saya or IDispatch but for the most part you get what you’re given.
One thing developers perhaps don’t realize is that it’s not just the interfaces you specify which get exported from the .NET COM object but the runtime adds a number of “management” interfaces as well. This interfaces are implemented by wrapping the .NET object inside a COM Callable Wrapper (CCW).
We can enumerate what interfaces are exposed by the CCW. Taking System.Object as an example the following table shows what interfaces are supported along with how each interface is implemented, either dynamically at runtime or statically implemented inside the runtime.
Interface Name | Implementation Type |
_Object | Dynamic |
IConnectionPointContainer | Static |
IDispatch | Dynamic |
IManagedObject | Static |
IMarshal | Static |
IProvideClassInfo | Static |
ISupportErrorInfo | Static |
ITulisan saya | Dynamic |
The _Object interface refers to the COM visible representation of the System.Object class which is the root of all .NET objects, it must be generated dynamically as it’s dependent on the .NET object being exposed. On the other hand IManagedObject is implemented by the runtime itself and the implementation is shared across all CCWs.
I started looking at the exposed COM attack surface for .NET back in 2013 when I was investigating Internet Explorer sandbox escapes. One of the COM objects you could access outside the sandbox was the .NET ClickOnce Deployment broker (DFSVC) which turned out to be implemented in .NET, which is probably not too surprising. I actually found two issues, not in DFSVC itself but instead in the _Object interface exposed by all .NET COM objects. The _Object interface looks like the following (in C++).
struct _Object : public IDispatch {
HRESULT ToString(BSTR * pRetVal);
HRESULT Equals(VARIANT obj, VARIANT_BOOL *pRetVal);
HRESULT GetHashCode(long *pRetVal);
HRESULT GetType(_Type** pRetVal);
};
The first bug (which resulted in CVE-2014-0257) was in the GetType method. This method returns a COM object which can be used to access the .NET reflection APIs. As the returned _Type COM object was running inside the server you could call a chain of methods which resulted in getting access to the Process.Start method which you could call to escape the sandbox. If you want more details about that you can look at the PoC I wrote and put up on Github. Microsoft fixed this by preventing the access to the reflection APIs over DCOM.
The second issue was more subtle and is a byproduct of a feature of .NET interop which presumably no-one realized would be a security liability. Loading the .NET runtime requires quite a lot of additional resources, therefore the default for a native COM client calling methods on a .NET COM server is to let COM and the CCW manage the communication, even if this is a performance hit. Microsoft could have chosen to use the COM marshaler to force .NET to be loaded in the client but this seems overzealous, not even counting the possibility that the client might not even have a compatible version of .NET installed.
When .NET interops with a COM object it creates the inverse of the CCW, the Runtime Callable Wrapper (RCW). This is a .NET object which implements a runtime version of the COM interface and marshals it to the COM object. Now it’s entirely possible that the COM object is actually written in .NET, it might even be in the same Application Domain. If .NET didn’t do something you could end up with a double performance hit, marshaling in the RCW to call a COM object which is actually a CCW to a managed object.
It would be nice to try and “unwrap” the managed object from the CCW and get back a real .NET object. This is where the villain in this piece comes into play, the IManagedObject interface, which looks like the following:
struct IManagedObject : public ITulisan saya {
HRESULT GetObjectIdentity(
BSTR* pBSTRGUID,
int* AppDomainID,
int* pCCW);
HRESULT GetSerializedBuffer(
BSTR *pBSTR
);
};
When the .NET runtime gets hold of a COM object it will go through a process to determine whether it can “unwrap” the object from its CCW and avoid creating an RCW. This process is documented but in summary the runtime will do the following:
Call QueryInterface on the COM object to determine if it implements the IManagedObject interface. If not then return an appropriate RCW.
Call GetObjectIdentity on the interface. If the GUID matches the per-runtime GUID (generated at runtime startup) and the AppDomain ID matches the current AppDomain ID then lookup the CCW value in a runtime table and extract a pointer to the real managed object and return it.
Call GetSerializedBuffer on the interface. The runtime will check if the .NET object is serializable, if so it will pass the object to BinaryFormatter::Serialize and package the result in a Binary String (BSTR). This will be returned to the client which will now attempt to deserialize the buffer to an object instance by calling BinaryFormatter::Deserialize.
Both steps 2 and 3 sound like a bad idea. For example while in 2 the per-runtime GUID can’t be guessed; if you have access to any other object in the same process (such as the COM object exposed by the server itself) you can call GetObjectIdentity on the object and replay the GUID and AppDomain ID back to the server. This doesn’t really gain you much though, the CCW value is just a number not a pointer so at best you’ll be able to extract objects which already have a CCW in place.
Instead it’s step 3 which is really nasty. Arbitrary deserialization is dangerous almost no matter what language (take your pick, Java, PHP, Ruby etc.) and .NET is no different. In fact my first ever Blackhat USA presentation (whitepaper) was on this very topic and there’s been follow up work since (such as this blog post). Clearly this is an issue we can exploit, first let’s look at it from the perspective of privilege escalation.
Elevating Privileges
How can we get a COM server written in .NET to do the arbitrary deserialization? We need the server to try and create an RCW for a serializable .NET object exposed over COM. It would be nice if this could also been done generically; it just so happens that on the standard _Object interface there exists a function we can pass an arbitrary object to, the Equals method. The purpose of Equals is to compare two objects for equality. If we pass a .NET COM object to the server’s Equals method the runtime must try and convert it to an RCW so that the managed implementation can use it. At this point the runtime wants to be helpful and checks if it’s really a CCW wrapped .NET object. The server runtime calls GetSerializedBuffer which results in arbitrary deserialization in the server process.
This is how I exploited the ClickOnce Deployment broker a second time resulting in CVE-2014-4073. The trick to exploiting this was to send a serialized Hashtable to the server which contains a COM implementation of the IHashCodeProvider interface. When the Hashtable runs its custom deserialization code it needs to rebuild its internal hash structures, it does that by calling IHashCodeProvider::GetHashCode on each key. By adding a Delegate object, which is serializable, as one of the keys we’ll get it passed back to the client. By writing the client in native code the automatic serialization through IManagedObject won’t occur when passing the delegate back to us. The delegate object gets stuck inside the server process but the CCW is exposed to us which we can call. Invoking the delegate results in the specified function being executed in the server context which allows us to start a new process with the server’s privileges. As this works generically I even wrote a tool to do it for any .NET COM server which you can find on github.
Microsoft could have fixed CVE-2014-4073 by changing the behavior of IManagedObject::GetSerializedBuffer but they didn’t. Instead Microsoft rewrote the broker in native code instead. Also a blog post was published warning developers of the dangers of .NET DCOM. However what they didn’t do is deprecate any of the APIs to register DCOM objects in .NET so unless a developer is particularly security savvy and happens to read a Microsoft security blog they probably don’t realize it’s a problem.
This bug class exists to this day, for example when I recently received a new work laptop I did what I always do, enumerate what OEM “value add” software has been installed and see if anything was exploitable. It turns out that as part of the audio driver package was installed a COM service written by Dolby. After a couple of minutes of inspection, basically enumerating accessible interface for the COM server, I discovered it was written in .NET (the presence of IManagedObject is always a big giveaway). I cracked out my exploitation tool and in less than 5 minutes I had code execution at local system. This has now been fixed as CVE-2017-7293, you can find the very terse writeup here. Once again as .NET DCOM is fundamentally unsafe the only thing Dolby could do was rewrite the service in native code.
Hacking the Caller
Finding a new instance of the IManagedObject bug class focussed my mind on its other implications. The first thing to stress is the server itself isn’t vulnerable, instead it’s only when we can force the server to act as a DCOM client calling back to the attacking application that the vulnerability can be exploited. Any .NET application which calls a DCOM object through managed COM interop should have a similar issue, not just servers. Is there likely to be any common use case for DCOM, especially in a modern Enterprise environment?
My immediate thought was Windows Management Instrumentation (WMI). Modern versions of Windows can connect to remote WMI instances using the WS-Management (WSMAN) protocol but for legacy reasons WMI still supports a DCOM transport. One use case for WMI is to scan enterprise machines for potentially malicious behavior. One of the reasons for this resurgence is Powershell (which is implemented in .NET) having easy to use support for WMI. Perhaps PS or .NET itself will be vulnerable to this attack if they try and access a compromised workstation in the network?
Looking at MSDN, .NET supports WMI through the System.Management namespace. This has existed since the beginning of .NET. It supports remote access to WMI and considering the age of the classes it predates WSMAN and so almost certainly uses DCOM under the hood. On the PS front there’s support for WMI through cmdlets such as Get-WmiObject. PS version 3 (introduced in Windows 8 and Server 2008) added a new set of cmdlets including Get-CimInstance. Reading the related link it’s clear why the CIM cmdlets were introduced, support for WSMAN, and the link explicitly points out that the “old” WMI cmdlets uses DCOM.
At this point we could jump straight into RE of the .NET and PS class libraries, but there’s an easier way. It’s likely we’d be able to see whether the .NET client queries for IManagedObject by observing the DCOM RPC traffic to a WMI server. Wireshark already has a DCOM dissector saving us a lot of trouble. For a test I set up two VMs, one with Windows Server 2016 acting as a domain controller and one with Windows 10 as a client on the domain. Then from a Domain Administrator on the client I issued a simple WMI PS command ‘Get-WmiObject Win32_Process -ComputerName dc.network.local’ while monitoring the network using Wireshark. The following image shows what I observed:
The screenshot shows the initial creation request for the WMI DCOM object on the DC server (192.168.56.50) from the PS client (192.168.56.102). We can see it’s querying for the IWbemLoginClientID interface which is the part of the initialization process (as documented in MS-WMI). The client then tries to request a few other interfaces; notably it asks for IManagedObject. This almost certainly indicates that a client using the PS WMI cmdlets would be vulnerable.
In order to test whether this is really a vulnerability we’ll need a fake WMI server. This would seem like it would be quite a challenge, but all we need to do is modify the registration for the winmgmt service to point to our fake implementation. As long as that service then registers a COM class with the CLSID {8BC3F05E-D86B-11D0-A075-00C04FB68820} the COM activator will start the service and serve any client an instance of our fake WMI object. If we look back at our network capture it turns out that the query for IManagedObject isn’t occurring on the main class, but instead on the IWbemServices object returned from IWbemLevel1Login::NTLMLogin. But that’s okay, it just adds a bit extra boilerplate code. To ensure it’s working we’ll implement the following code which will tell the deserialization code to look for an Tulisan saya Assembly called Badgers.
[Serializable, ComVisible(true)]
public class FakeWbemServices :
IWbemServices,
ISerializable {
public void GetObjectData(SerializationInfo info,
StreamingContext context) {
info.AssemblyName = "Badgers, Version=4.0.0.0";
info.FullTypeName = "System.Badgers.Test";
}
// Rest of fake implementation...
}
If we successfully injected a serialized stream then we’d expect the PS process to try and lookup a Badgers.dll file and using Process Monitor that’s exactly what we find.
Chaining Up the Deserializer
When exploiting the deserialization for local privilege escalation we can be sure that we can connect back to the server and run an arbitrary delegate. We don’t have any such guarantees in the RCE case. If the WMI client has default Windows Firewall rules enabled then we almost certainly wouldn’t be able to connect to the RPC endpoint made by the delegate object. We also need to be allowed to login over the network to the machine running the WMI client, our compromised machine might not have a login to the domain or the enterprise policy might block anyone but the owner from logging in to the client machine.
We therefore need a slightly different plan, instead of actively attacking the client through exposing a new delegate object we’ll instead pass it a byte stream which when deserialized executes a desired action. In an ideal world we’d find that one serializable class which just executes arbitrary code for us. Sadly (as far as I know of) no such class exists. So instead we’ll need to find a series of “Gadget” classes which when chained together perform the desired effect.
So in this situation I tend to write some quick analysis tools, .NET supports a pretty good Reflection API so finding basic information such as whether a class is serializable or which interfaces a class supports is pretty easy to do. We also need a list of Assemblies to check, the quickest way I know of is to use the gacutil utility installed as part of the .NET SDK (and so installed with Visual Studio). Run the command gacutil /l > assemblies.txt to create a list of assembly names you can load and process. For a first pass we’ll look for any classes which are serializable and have delegates in them, these might be classes which when an operation is performed will execute arbitrary code. With our list of assemblies we can write some simple code like the following to find those classes, just call FindSerializableTypes for each assembly name string:
static bool IsDelegateType(Type t) {
return typeof(Delegate).IsAssignableFrom(t);
}
static bool HasSerializedDelegate(Type t) {
// Custom serialized objects rarely serialize their delegates.
if (typeof(ISerializable).IsAssignableFrom(t)) {
return false;
}
foreach (FieldInfo field in FormatterServices.GetSerializableMembers(t)) {
if (IsDelegateType(field.FieldType)) {
return true;
}
}
}
static void FindSerializableTypes(string assembly_name) {
Assembly asm = Assembly.Load(assembly_name);
var types = asm.GetTypes().Where(t => t.IsSerializable
&& t.IsClass
&& !t.IsAbstract
&& !IsDelegateType(t)
&& HasSerializedDelegate(t));
foreach (Type type in types) {
Console.WriteLine(type.FullName);
}
}
Across my system this analysis only resulted in around 20 classes, and of those many were actually in the F# libraries which are not distributed in a default installation. However one class did catch my eye, System.Collections.Generic.ComparisonComparer<T>. You can find the implementation in the reference source, but as it’s so simple here it is in its entirety:
public delegate int Comparison<T>(T x, T y);
[Serializable]
internal class ComparisonComparer<T> : Comparer<T> {
private readonly Comparison<T> _comparison;
public ComparisonComparer(Comparison<T> comparison) {
_comparison = comparison;
}
public override int Compare(T x, T y) {
return this._comparison(x, y);
}
}
This class wraps a Comparison<T> delegate which takes two generic parameters (of the same type) and returns an integer, calling the delegate to implement the IComparer<T> interface. While the class is internal its creation is exposed through Comparer<T>::Create static method. This is the first part of the chain, with this class and a bit of massaging of serialized delegates we can chain IComparer<T>::Compare to Process::Start and get an arbitrary process created. Now we need the next part of the chain, calling this comparer object with arbitrary arguments.
Comparer objects are used a lot in the generic .NET collection classes and many of these collection classes also have custom deserialization code. In this case we can abuse the SortedSet<T> class, on deserialization it rebuilds its set using an internal comparer object to determine the sort order. The values passed to the comparer are the entries in the set, which is under our complete control. Let’s write some test code to check it works as we expect:
static void TypeConfuseDelegate(Comparison<string> comp) {
FieldInfo fi = typeof(MulticastDelegate).GetField("_invocationList",
BindingFlags.NonPublic | BindingFlags.Instance);
object[] invoke_list = comp.GetInvocationList();
// Modify the invocation list to add Process::Start(string, string)
invoke_list[1] = new Func<string, string, Process>(Process.Start);
fi.SetValue(comp, invoke_list);
}
// Create a simple multicast delegate.
Delegate d = new Comparison<string>(String.Compare);
Comparison<string> d = (Comparison<string>) MulticastDelegate.Combine(d, d);
// Create set with original comparer.
IComparer<string> comp = Comparer<string>.Create(d);
SortedSet<string> set = new SortedSet<string>(comp);
// Setup values to call calc.exe with a dummy argument.
set.Add("calc");
set.Add("adummy");
TypeConfuseDelegate(d);
// Test serialization.
BinaryFormatter fmt = new BinaryFormatter();
MemoryStream stm = new MemoryStream();
fmt.Serialize(stm, set);
stm.Position = 0;
fmt.Deserialize(stm);
// Calculator should execute during Deserialize.
The only weird thing about this code is TypeConfuseDelegate. It’s a long standing issue that .NET delegates don’t always enforce their type signature, especially the return value. In this case we create a two entry multicast delegate (a delegate which will run multiple single delegates sequentially), setting one delegate to String::Compare which returns an int, and another to Process::Start which returns an instance of the Process class. This works, even when deserialized and invokes the two separate methods. It will then return the created process object as an integer, which just means it will return the pointer to the instance of the process object. So we end up with chain which looks like the following:
While this is a pretty simple chain it has a couple of problems which makes it less than ideal for our use:
The Comparer<T>::Create method and the corresponding class were only introduced in .NET 4.5, which covers Windows 8 and above but not Windows 7.
The exploit relies in part on a type confusion of the return value of the delegate. While it’s only converting the Process object to an integer this is somewhat less than ideal and could have unexpected side effects.
Starting a process is a bit on the noisy side, it would be nicer to load our code from memory.
So we’ll need to find something better. We want something which works at a minimum on .NET 3.5, which would be the version on Windows 7 which Windows Update would automatically update you to. Also it shouldn’t rely on undefined behaviour or loading our code from outside of the DCOM channel such as over a HTTP connection. Sounds like a challenge to me.
Improving the Chain
While looking at some of the other classes which are serializable I noticed a few in the System.Workflow.ComponentModel.Serialization namespace. This namespace contains classes which are part of the Windows Workflow Foundation, which is a set of libraries to build execution pipelines to perform a series of tasks. This alone sounds interesting, and it turns out I have exploited the core functionality before as a bypass for Code Integrity in Windows Powershell.
This lead me to finding the ObjectSerializedRef class. This looks very much like a class which will deserialize any object type, not just serialized ones. If this was the case then that would be a very powerful primitive for building a more functional deserialization chain.
[Serializable]
private sealed class ObjectSerializedRef : IObjectReference,
IDeserializationCallback
{
private Type type;
private object[] memberDatas;
[NonSerialized]
private object returnedObject;
object IObjectReference.GetRealObject(StreamingContext context) {
returnedObject = FormatterServices.GetUninitializedObject(type);
return this.returnedObject;
}
void IDeserializationCallback.OnDeserialization(object sender) {
string[] array = null;
MemberInfo[] serializableMembers =
FormatterServicesNoSerializableCheck.GetSerializableMembers(
type, out array);
FormatterServices.PopulateObjectMembers(returnedObject,
serializableMembers, memberDatas);
}
}
Looking at the implementation the class was used as a serialization surrogate exposed through the ActivitiySurrogateSelector class. This is a feature of the .NET serialization API, you can specify a “Surrogate Selector” during the serialization process which will replace an object with surrogate class. When the stream is deserialized this surrogate class contains enough information to reconstruct the original object. One use case is to handle the serialization of non-serializable classes, but ObjectSerializedRef goes beyond a specific use case and allows you to deserialize anything. A test was in order:
// Definitely non-serializable class.
class NonSerializable {
private string _text;
public NonSerializable(string text) {
_text = text;
}
public override string ToString() {
return _text;
}
}
// Custom serialization surrogate
class MySurrogateSelector : SurrogateSelector {
public override ISerializationSurrogate GetSurrogate(Type type,
StreamingContext context, out ISurrogateSelector selector) {
selector = this;
if (!type.IsSerializable) {
Type t = Type.GetType("ActivitySurrogateSelector+ObjectSurrogate");
return (ISerializationSurrogate)Activator.CreateInstance(t);
}
return base.GetSurrogate(type, context, out selector);
}
}
static void TestObjectSerializedRef() {
BinaryFormatter fmt = new BinaryFormatter();
MemoryStream stm = new MemoryStream();
fmt.SurrogateSelector = new MySurrogateSelector();
fmt.Serialize(stm, new NonSerializable("Hello World!"));
stm.Position = 0;
// Should print Hello World!.
Console.WriteLine(fmt.Deserialize(stm));
}
The ObjectSurrogate class seems to work almost too well. This class totally destroys any hope of securing an untrusted BinaryFormatter stream and it’s available from .NET 3.0. Any class which didn’t mark itself as serializable is now a target. It’s going to be pretty easy to find a class which while invoke an arbitrary delegate during deserialization as the developer will not be doing anything to guard against such an attack vector.
Now just to choose a target to build out our deserialization chain. I could have chosen to poke further at the Workflow classes, but the API is horrible (in fact in .NET 4 Microsoft replaced the old APIs with a new, slightly nicer one). Instead I’ll pick a really easy to use target, Language Integrated Query (LINQ).
LINQ was introduced in .NET 3.5 as a core language feature. A new SQL-like syntax was introduced to the C# and VB compilers to perform queries across enumerable objects, such as Lists or Dictionaries. An example of the syntax which filters a list of names based on length and returns the list uppercased is as follows:
string[] names = { "Alice", "Bob", "Carl" };
IEnumerable<string> query = from name in names
where name.Length > 3
orderby name
select name.ToUpper();
foreach (string item in query) {
Console.WriteLine(item);
}
You can also view LINQ not as a query syntax but instead a way of doing list comprehension in .NET. If you think of ‘select’ as equivalent to ‘map’ and ‘where’ to ‘filter’ it might make more sense. Underneath the query syntax is a series of methods implemented in the System.Linq.Enumerable class. You can write it using normal C# syntax instead of the query language; if you do the previous example becomes the following:
IEnumerable<string> query = names.Where(name => name.Length > 3)
.OrderBy(name => name)
.Select(name => name.ToUpper());
The methods such as Where take two parameters, a list object (this is hidden in the above example) and a delegate to invoke for each entry in the enumerable list. The delegate is typically provided by the application, however there’s nothing to stop you replacing the delegates with system methods. The important thing to bear in mind is that the delegates are not invoked until the list is enumerated. This means we can build an enumerable list using LINQ methods, serialize it using the ObjectSurrogate (LINQ classes are not themselves serializable) then if we can force the deserialized list to be enumerated it will execute arbitrary code.
Using LINQ as a primitive we can create a list which when enumerated maps a byte array to an instance of a type in that byte array by the following sequence:
The only tricky part is step 2, we’d like to extract a specific type but our only real option is to use the Enumerable.Join method which requires some weird kludges to get it to work. A better option would have been to use Enumerable.Zip but that was only introduced in .NET 4. So instead we’ll just get all the types in the loaded assembly and create them all, if we just have one type then this isn’t going to make any difference. How does the implementation look in C#?
static IEnumerable CreateLinq(byte[] assembly) {
List<byte[]> base_list = new List<byte[]>();
base_list.Add(assembly);
var get_types_del = (Func<Assembly, IEnumerable<Type>>)
Delegate.CreateDelegate(
typeof(Func<Assembly, IEnumerable<Type>>),
typeof(Assembly).GetMethod("GetTypes"));
return base_list.Select(Assembly.Load)
.SelectMany(get_types_del)
.Select(Activator.CreateInstance);
}
The only non-obvious part of the C# implementation is the delegate for Assembly::GetTypes. What we need is a delegate which takes an Assembly object and returns a list of Type objects. However as GetTypes is an instance method the default would be to capture the Assembly class and store it inside the delegate object, which would result in a delegate which took no parameters and returned a list of Type. We can get around this by using the reflection APIs to create an open delegate to an instance member. An open delegate doesn’t store the object instance, instead it exposes it as an additional Assembly parameter, exactly what we want.
With our enumerable list we can get the assembly loaded and our own code executed, but how do we get the list enumerated to start the chain? For this decided I’d try and find a class which when calling ToString (a pretty common method) would enumerate the list. This is easy in Java, almost all the collection classes have this exact behavior. Sadly it seems .NET doesn't follow Java in this respect. So I modified my analysis tools to try and hunt for gadgets which would get us there. To cut a long story short I found a chain from ToString to IEnumerable through three separate classes. The chain looks something like the following:
Are we done yet? No, just one more step, we need to call ToString on an arbitrary object during deserialization. Of course I wouldn’t have chosen ToString if I didn’t already have a method to do this. In this final case I’ll go back to abusing poor, old, Hashtable. During deserialization of the Hashtable class it will rebuild its key set, which we already know about as this is how I exploited serialization for local EoP. If two keys are equal then the deserialization will fail with the Hashtable throwing an exception, resulting in running the following code:
throw new ArgumentException(
Environment.GetResourceString("Argument_AddingDuplicate__",
buckets[bucketNumber].key, key));
It’s not immediately obvious why this would be useful. But perhaps looking at the implementation of GetResourceString will make it clearer:
internal static String GetResourceString(String key, params Object[] values) {
String s = GetResourceString(key);
return String.Format(CultureInfo.CurrentCulture, s, values);
}
The key is passed to GetResourceString within the values array as well as a reference to a resource string. The resource string is looked up and along with the key passed to String.Format. The resulting resource string has formatting codes so when String.Format encounters the non-string value it calls ToString on the object to format it. This results in ToString being called during deserialization kicking off the chain of events which leads to us loading an arbitrary .NET assembly from memory and executing code in the context of the WMI client.
Conclusions
Microsoft fixed the RCE issue by ensuring that the System.Management classes never directly creates an RCW for a WMI object. However this fix doesn’t affect any other use of DCOM in .NET, so privileged .NET DCOM servers are still vulnerable and other remote DCOM applications could also be attacked.
Also this should be a lesson to never deserialize untrusted data using the .NET BinaryFormatter class. It’s a dangerous thing to do at the best of times, but it seems that the developers have abandoned any hope of making secure serializable classes. The presence of ObjectSurrogate effectively means that every class in the runtime is serializable, whether the original developer wanted them to be or not.
And as a final thought you should always be skeptical about the security implementation of middleware especially if you can’t inspect what it does. The fact that the issue with IManagedObject is designed in and hard to remove makes it very difficult to fix correctly.
Posting Komentar