.Net Remoting 系列二】Solarwinds ARM 漏洞分析

首发于https://forum.butian.net/share/3998 本篇主要是以Solarwinds Arm产品介绍自定义ServerChanel的场景,漏洞分析利用是其次,事实上是去年挖的没有详细记录,后续写的,勿怪哈哈哈

最开始的漏洞是由@SinSinology挖掘的CVE-2023-35187后续又出现了很多CVE。

text

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.

根据漏洞描述可以定位到OpenClientUpdateFile方法;

csharp

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,该类实现如下

csharp

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类

csharp

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,下面分析它处理的大致逻辑。

按照之前的思路梳理自定义的ServerChannel,看看注册的Channel类型是否是GrpcServerChannel,查找ChannelServices#RegisterChannelInternal的调用,找到GrpcRemotingChannelCreator#createGlobalServerChannelInternal方法,实现如下

csharp

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大同小异,实现大致如下

csharp

//构造函数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是自己实现的,跟进下

csharp

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

csharp

// 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的服务,查看其实现

csharp

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,为了进一步验证,看下整个数据流

csharp

//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就能调用对应的的方法,整理了一些

URIServicenamespace
wuselWuselImplementationpn.server
gafferGafferImplementationpn.gaffer
tattleTattleImplementationpn.server
updateUpdateSourceImplementationpn.update
tracerServerTracerServerImplementationpn.tracer
farmServerFarmServerImplementationpn.server.farm
tracerCenterTracerCenterImplementationpn.tracer
jobCenterJobCenterImplementationpn.jobs
GenericGafferCredentialServiceGenericGafferCredentialServiceImplementationpn.connectors.generic.gaffer
logServiceLogServiceImplementationpn.server
ServerDebuggerServerDebuggerImplementationServerDebuggerImplementation

以上面的CVE为例,是TattleImplementation服务出现了任意文件读取的问题,只需要构造GrpcClientChannel注册grpc://IP:5555/tattle之后调用其方法,至于升级成RCE就需要利用.erlang.cookie( C:\ProgramData\SolarWinds\Orion\RabbitMQ\.erlang.cookie )的姿势了,这里不再详细说明。

服务端为了实现更强大的功能可能就会碰到自定义的ServerChanel,还有基于UDP、Websocket等协议的,代码审计的关注点。