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:

Screenshot from wireshark showing .NET remoting network traffic which has a .NET magic at the start.

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:

string objectUri = wkRequestHeaders.RequestUri;
           
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:

Screen shot of Wireshark following connection. The string Boo! is sent to the server which responds with .NET remoting protocol.

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:

ysoserial.exe -f BinaryFormatter -o raw -g TypeConfuseDelegate -c notepad > 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:

Hex dump of request.bin.

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.