.Net Remoting 系列二】Solarwinds ARM 漏洞分析
首发于https://forum.butian.net/share/3998 本篇主要是以Solarwinds Arm产品介绍自定义ServerChanel的场景,漏洞分析利用是其次,事实上是去年挖的没有详细记录,后续写的,勿怪哈哈哈
起因
最开始的漏洞是由@SinSinology挖掘的CVE-2023-35187后续又出现了很多CVE。
This vulnerability allows remote attackers to execute arbitrary code on affected installations of SolarWinds Access Rights Manager. Authentication is not required to exploit this vulnerability.The specific flaw exists within the OpenClientUpdateFile method. The issue results from the lack of proper validation of a user-supplied path prior to using it in file operations. An attacker can leverage this vulnerability to execute code in the context of SYSTEM.
自定义的ServerChannel
根据漏洞描述可以定位到OpenClientUpdateFile方法;
public class TattleImplementation : RemotingObject, ITattle, IPing
{
......
public Guid OpenClientUpdateFile(string fileName)
{
Guid guid2;
using (MethodCall methodCall = new MethodCall(this))
{
Guid guid = Guid.NewGuid();
using (this.fileStreams.Lock())
{
this.fileStreams.Add(guid, new FileStream(Path.Combine(ApplicationPaths.Instance.InstallationPath, fileName), FileMode.Open, FileAccess.Read));
}
guid2 = methodCall.SetResult<Guid>(guid);
}
return guid2;
}
public byte[] ReadClientUpdateFile(Guid updateId, int maxBytes)
{
byte[] array = new byte[maxBytes];
int num;
using (this.fileStreams.Lock())
{
num = this.fileStreams[updateId].Read(array, 0, maxBytes);
}
Array.Resize<byte>(ref array, num);
return array;
}
......
很明显可以通过调用OpenClientUpdateFile返回的guid再调用ReadClientUpdateFile可以读取任意文件,并且该类继承RemotingObject
,该类实现如下
using System;
using System.Runtime.Remoting;
namespace pn.remoting
{
public abstract class RemotingObject : MarshalByRefObject, IDisposable, IPing
{
public override object InitializeLifetimeService()
{
return null;
}
public string Ping(string packet)
{
return packet;
}
public void InstantiateInterface()
{
}
public virtual void Dispose()
{
RemotingServices.Disconnect(this);
}
}
}
可以看到该类继承自MarshalByRefObject,判断是漏洞是.Net Remoting导致的,全局搜索发现主要处理在SolarWinds.ARM.Remoting.dll。看到GrpcServerChannel类
namespace SolarWinds.ARM.Remoting
{
internal sealed class GrpcServerChannel : IChannelReceiver, IChannel, ISecurableChannel
{
public GrpcServerChannel(IGrpcRemotingChannelManager grpcChannelManager, string name, int port, IServerChannelSinkProvider sinkProvider = null)
{
this.port = port;
this.machineName = Dns.GetHostName();
this.baseAddresses = new string[0];
this.grpcChannelManager = grpcChannelManager;
this.name = name;
this.sinkProvider = sinkProvider ?? new BinaryServerFormatterSinkProvider(new Hashtable
{
{ "includeVersions", false },
{
"typeFilterLevel",
TypeFilterLevel.Full.ToString()
}
}, null);
this.setupChannel();
this.StartListening(null);
}
......
和TcpServerChannel类似的实现,看样子是服务端是基于GRPC的ServerChannel,下面分析它处理的大致逻辑。
Remoting功能代码逻辑分析
按照之前的思路梳理自定义的ServerChannel,看看注册的Channel类型是否是GrpcServerChannel,查找ChannelServices#RegisterChannelInternal的调用,找到GrpcRemotingChannelCreator#createGlobalServerChannelInternal方法,实现如下
private int createGlobalServerChannelInternal(int port, bool selectServerPort)
{
string text = string.Format("port_{0}", port);
object obj = this.channelAccess;
lock (obj)
{
......
IDictionary dictionary = new Hashtable();
dictionary["includeVersions"] = false;
dictionary["typeFilterLevel"] = TypeFilterLevel.Full.ToString();
IServerChannelSinkProvider serverChannelSinkProvider2;
if (!this.applicationConfiguration.GetValue<bool>("network.dump.enabled", false))
{
IServerChannelSinkProvider serverChannelSinkProvider = new BinaryServerFormatterSinkProvider(dictionary, null);
serverChannelSinkProvider2 = serverChannelSinkProvider;
}
......
IServerChannelSinkProvider serverChannelSinkProvider3 = serverChannelSinkProvider2;
IChannelReceiver channelReceiver;
if (selectServerPort || port > 0)
{
channelReceiver = new GrpcServerChannel(this.grpcManager, text, port, serverChannelSinkProvider3);
this.hostname = new Uri(channelReceiver.GetUrlsForUri("")[0]).Host;
port = this.grpcManager.LocalPort;
this.SetProperties(new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase)
{
{ "LocalPort", port },
{ "remoteHost", this.DnsHostName },
{ "remotePort", port }
});
StringBuilder sb = new StringBuilder(" gRPC .netRemoting listen on port ").AppendLine(port.ToString()).AppendLine(string.Format("ARM is using following channel URIs[{0}]: ", ((GrpcServerChannel)channelReceiver).ChannelUris.Length));
((GrpcServerChannel)channelReceiver).ChannelUris.ForEach(delegate(string u)
{
sb.AppendLine(u);
});
Log.info(this, sb.ToString());
}
......
ChannelServices.RegisterChannel(channelReceiver, false);
}
return this.grpcManager.LocalPort;
}
传入GrpcServerChannel的sinkProvider为BinaryServerFormatterSinkProvider,并开启了Full Type Filter。
下面梳理sinkProviderChains:通读SolarWinds.ARM.Remoting.GrpcServerChannel
的代码,和TcpServerChannel大同小异,实现大致如下
//构造函数TypeFilterLevel为Full
//调用setupChannel
//调用StartListening
private void setupChannel()
{
this.channelData = new ChannelDataStore(null);
// 创建默认的SinkProviderChain provider,这里之前创建的就是BinaryServerFormatterSinkProvider
if (this.sinkProvider == null)
{
this.sinkProvider = ChannelHelper.CreateDefaultServerProviderChain();
}
this.port = this.grpcChannelManager.BindPort(this.port);
Log.debug(this, string.Format("gRPC channel bound to port {0}", this.port));
this.baseAddresses = (from hostname in this.grpcChannelManager.HostNames
select string.Format("{0}{1}:{2}", "grpc://", hostname, this.port)).ToArray<string>();
Log.debug(this, string.Format("gRPC determines channel addresses by network interfaces [{0}] {1} {2}", this.baseAddresses.Length, Environment.NewLine, string.Join(Environment.NewLine, this.baseAddresses)));
if (this.port > 0)
{
this.channelData.ChannelUris = this.baseAddresses;
}
//这里和TcpServerChannel一样
CoreChannel.CollectChannelDataFromServerSinkProviders(this.channelData, this.sinkProvider);
this.transportSink = new GrpcServerChannel.GrpcServerTransportSink(ChannelServices.CreateServerChannelSinkChain(this.sinkProvider, this));
int num = this.port;
}
.......
public void StartListening(object data)
{
//GrpcServerTransportSink绑定到port_55555
this.grpcChannelManager.BindChannel(string.Format("port_{0}", this.grpcChannelManager.LocalPort), this.transportSink);
}
这里的sinkProvider和TcpServerChannel一样,但是transportsink是自己实现的,跟进下
internal sealed class GrpcServerTransportSink : IServerChannelSink, IChannelSinkBase
{
internal GrpcServerTransportSink(IServerChannelSink nextSink)
{
this.nextSink = nextSink;
}
public ServerProcessing ProcessMessage(IServerChannelSinkStack sinkStack, IMessage requestMsg, ITransportHeaders requestHeaders, Stream requestStream, out IMessage responseMsg, out ITransportHeaders responseHeaders, out Stream responseStream)
{
throw new NotSupportedException();
}
这里实现非常简单,初步看起来整个Channel Sinks Chains像是GrpcServerTransportSink-->BinaryServerFormatterSink-->Dispatch
。
继续看完GrpcServerChannel
// GrpcServerChannel
public void StartListening(object data)
{
this.grpcChannelManager.BindChannel(string.Format("port_{0}", this.grpcChannelManager.LocalPort), this.transportSink);
}
// GrpcRemotingChannelManager
public void BindChannel(string channelName, IServerChannelSink serverChannelSink)
{
this.serverChannels[channelName] = serverChannelSink;
this.startGrpc();
}
private void startGrpc()
{
lock (this)
{
if (this.isRunning)
{
return;
}
this.isRunning = true;
}
this.grpcManager.Register(new ServerServiceDefinition[]
{
Remoting.BindService(new GrpcRemotingService(this.serviceCollection, this.serverChannels, this))
});
this.grpcManager.StartServer();
}
在开启监听的同时通过Grpc的方式注册了GrpcRemotingService的服务,查看其实现
namespace SolarWinds.ARM.Remoting
{
internal sealed class GrpcRemotingService : Remoting.RemotingBase
{
{
this.serviceCollection = serviceCollection;
this.serverChannelSinks = serverChannelSinks;
this.subscriberProvider = subscriberProvider;
}
public override async Task Send(IAsyncStreamReader<RemotingMessageRequest> requestStream, IServerStreamWriter<RemotingMessageReply> responseStream, ServerCallContext context)
{
......
GrpcRemotingService.HandelRequest(request, requestMemStream, responseStream, serverChannelSink);
}
catch (OperationCanceledException ex)
{
throw new RemotingException("Timeout", ex);
}
finally
{
Stream stream = requestMemStream;
if (stream != null)
{
stream.Dispose();
}
TempFileCollection tempFileCollection2 = tempFileCollection;
if (tempFileCollection2 != null)
{
((IDisposable)tempFileCollection2).Dispose();
}
}
}
internal static void HandelRequest(RemotingMessageRequest request, Stream requestStream, IServerStreamWriter<RemotingMessageReply> responseMessageStream, IServerChannelSink serverChannelSink)
{
ITransportHeaders transportHeaders = request.Headers.ToTransportHeaders();
transportHeaders["__CustomErrorsEnabled"] = false;
string uri = request.Uri;
string text;
if (ChannelHelper.ParseGrpcURL(uri, out text) == null)
{
text = uri;
}
transportHeaders["__RequestUri"] = text;
ServerChannelSinkStack serverChannelSinkStack = new ServerChannelSinkStack();
serverChannelSinkStack.Push(serverChannelSink, null);
ServerProcessing serverProcessing;
try
{
global::System.Runtime.Remoting.Messaging.IMessage message;
ITransportHeaders transportHeaders2;
Stream stream;
// GrpcServer初始化时已设置默认的链为BinaryServerFormatterSink
serverProcessing = serverChannelSink.NextChannelSink.ProcessMessage(serverChannelSinkStack, null, transportHeaders, requestStream, out message, out transportHeaders2, out stream);
byte[] array = new byte[1048576];
int num = stream.Read(array, 0, array.Length);
......
如果熟悉C#中的Grpc就知道,该服务中的send方法可由客户端调用,进而调用HandelRequest方法,其实现就很熟悉了类似TcpServerTransportSink的ServiceRequest方法,根据代码逻辑,最后传递到BinaryServerFormatterSink#ProcessMessage方法处理。
整个sinkProviderChains需要参杂Grpc的调用,整个链就为GrpcRemotingService(GrpcRemotingService#send-->GrpcRemotingService#HandelRequest)-->BinaryServerFormatterSink-->Dispatch
。
刚好服务端的代码也存在GrpcClientChannel,为了进一步验证,看下整个数据流
//GrpcClientChannel
private IClientChannelSinkProvider createDefaultClientProviderChain()
{
IClientChannelSinkProvider clientChannelSinkProvider = new BinaryClientFormatterSinkProvider();
IClientChannelSinkProvider clientChannelSinkProvider2 = clientChannelSinkProvider;
clientChannelSinkProvider2.Next = new GrpcClientChannel.GrpcClientTransportSinkProvider(this.prop);
return clientChannelSinkProvider;
}
//GrpcClientTransportSinkProvider
public IClientChannelSink CreateSink(IChannelSender channel, string url, object remoteChannelData)
{
GrpcClientChannel.GrpcClientTransportSink grpcClientTransportSink = new GrpcClientChannel.GrpcClientTransportSink(url, (GrpcClientChannel)channel);
if (this.prop != null)
{
foreach (object key in this.prop.Keys)
{
grpcClientTransportSink[key] = this.prop[key];
}
}
return grpcClientTransportSink;
}
//GrpcClientTransportSink#ProcessMessage
public void ProcessMessage(System.Runtime.Remoting.Messaging.IMessage msg, ITransportHeaders requestHeaders, Stream requestStream, out ITransportHeaders responseHeaders, out Stream responseStream)
{
//调用sendRequestWithRetry
Debugger.NotifyOfCrossThreadDependency();
GrpcClientChannel.RemotingResponse remotingResponse = this.sendRequestWithRetry(msg, requestHeaders, requestStream);
responseHeaders = remotingResponse.Headers;
responseStream = remotingResponse.Content;
}
......
// sendRequestWithRetry
private GrpcClientChannel.RemotingResponse sendRequestWithRetry(System.Runtime.Remoting.Messaging.IMessage msg, ITransportHeaders requestHeaders, Stream requestStream)
{
IMethodCallMessage methodCallMessage = msg as IMethodCallMessage;
try
{
......
IEnumerable<RemotingMessageRequest> enumerable = GrpcClientChannel.GrpcClientTransportSink.TransformToMessages<RemotingMessageRequest>(requestHeaders, requestStream, (methodCallMessage != null) ? methodCallMessage.Uri : null);
// Grpc Channel AsyncDuplexStreamingCall调用
Uri uri = new Uri(this.url);
Channel clientChannel = this.channel.grpcChannelManager.GetClientChannel(uri.Host, uri.IsDefaultPort ? 55555 : uri.Port);
Remoting.RemotingClient remotingClient = new Remoting.RemotingClient(clientChannel);
AsyncDuplexStreamingCall<RemotingMessageRequest, RemotingMessageReply> asyncDuplexStreamingCall = remotingClient.Send(null, null, default(CancellationToken));
foreach (RemotingMessageRequest message in enumerable)
{
asyncDuplexStreamingCall.RequestStream.WriteAsync(message).Wait();
}
asyncDuplexStreamingCall.RequestStream.CompleteAsync().Wait();
if (!asyncDuplexStreamingCall.ResponseStream.MoveNext<RemotingMessageReply>().Result)
{
throw new EndOfStreamException();
}
RemotingMessageReply remotingMessageReply = asyncDuplexStreamingCall.ResponseStream.Current;
GrpcClientChannel.RemotingResponse remotingResponse = new GrpcClientChannel.RemotingResponse
{
Headers = remotingMessageReply.Headers.ToTransportHeaders(),
Content = new MemoryStream()
};
remotingMessageReply.Content.WriteTo(remotingResponse.Content);
if (bool.Parse(remotingMessageReply.Headers.GetValue("Chunked-Content", bool.FalseString)))
{
while (asyncDuplexStreamingCall.ResponseStream.MoveNext<RemotingMessageReply>().Result)
{
remotingMessageReply = asyncDuplexStreamingCall.ResponseStream.Current;
remotingMessageReply.Content.WriteTo(remotingResponse.Content);
}
}
remotingResponse.Content.Seek(0L, SeekOrigin.Begin);
return remotingResponse;
}
大致是GrpcClientTransportSink#ProcessMessage-->sendRequestWithRetry-->remotingClient.Send
,说明和上面分析的出入不大。
理清楚这些之后,看看正常功能,使用ARM客户端登录就会得到如下调用栈
漏洞分析
之前提到过当.net Remoting当中设置了黑白名单时,可以关注MBR类的方法是否存在一些直接可调用的危险方法。剩下的就是遍历恶意的方法如文件读写、命令执行等,然后构造Channel客户端并找到对应的objecturi
就能调用对应的的方法,整理了一些
URI | Service | namespace |
---|---|---|
wusel | WuselImplementation | pn.server |
gaffer | GafferImplementation | pn.gaffer |
tattle | TattleImplementation | pn.server |
update | UpdateSourceImplementation | pn.update |
tracerServer | TracerServerImplementation | pn.tracer |
farmServer | FarmServerImplementation | pn.server.farm |
tracerCenter | TracerCenterImplementation | pn.tracer |
jobCenter | JobCenterImplementation | pn.jobs |
GenericGafferCredentialService | GenericGafferCredentialServiceImplementation | pn.connectors.generic.gaffer |
logService | LogServiceImplementation | pn.server |
ServerDebugger | ServerDebuggerImplementation | ServerDebuggerImplementation |
以上面的CVE为例,是TattleImplementation服务出现了任意文件读取的问题,只需要构造GrpcClientChannel注册grpc://IP:5555/tattle之后调用其方法,至于升级成RCE就需要利用.erlang.cookie( C:\ProgramData\SolarWinds\Orion\RabbitMQ\.erlang.cookie )的姿势了,这里不再详细说明。
总结
服务端为了实现更强大的功能可能就会碰到自定义的ServerChanel,还有基于UDP、Websocket等协议的,代码审计的关注点。