CVE-2021-25274 – SolarWinds Orion Platform Unauthenticated RCE

superior_hosting_service

SolarWinds
SolarWinds

Description

The Collector Service in SolarWinds Orion Platform before 2020.2.4 uses MSMQ (Microsoft Message Queue) and doesn’t set permissions on its private queues. As a result, remote unauthenticated clients can send messages to TCP port 1801 that the Collector Service will process. Additionally, upon processing of such messages, the service deserializes them in insecure manner, allowing remote arbitrary code execution as LocalSystem.

Technical Analysis

Description

On February 3, 2021, Trustwave security researcher Martin Rakhmanov published a blog post detailing vulnerabilities they discovered in the SolarWinds Orion Platform and Serv-U FTP Server products. These vulnerabilities are tracked as CVE-2021-25274CVE-2021-25275, and CVE-2021-25276.

Of particular significance is CVE-2021-25274, an unauthenticated remote code execution (RCE) vulnerability in SolarWinds Orion Platform versions prior to 2020.2.4. The vulnerability stems from unauthenticated Microsoft Message Queuing (MSMQ) queues exposed on TCP port 1801. Data sent to these queues is deserialized by the SolarWinds Collector, leading to RCE as the LocalSystem account. Trustwave stated that they intend to release a proof-of-concept (PoC) on February 9, 2021.

SolarWinds has released Orion Platform 2020.2.4 to patch CVE-2021-25274. Rapid7 urges SolarWinds customers to update immediately, as CVE-2021-25274 is considered an impending threat due to ease of exploitation.

Affected products

  • SolarWinds Orion Platform versions prior to 2020.2.4

Rapid7 analysis

The following code snippets are from patched version 2020.2.4. According to Rakhmanov:

After the patch is applied, there is a digital signature validation step performed on arrived messages so that messages having no signature or not signed with a per-installation certificate are not further processed.

This behavior can be seen in the decompiled .NET code.

Exhibit A: SolarWinds.Collector.Queue.MsmqQueueController

The MsmqQueueController class controls MSMQ queues with prefix .\\Private$\\SolarWinds/Collector/ProcessingQueue/. Its OpenQueue() method sets CompressedMessageFormatter as the formatter for serialization and deserialization operations.

  private MessageQueue OpenQueue()
  {
    if (queue != null)
    {
      return queue;
    }
    lock (SyncRoot)
    {
      if (queue != null)
      {
        return queue;
      }
      if (!MessageQueue.Exists(QueueName))
      {
        log.DebugFormat("Creating MSMQ [{0}]", QueueName);
        MessageQueue val = MessageQueue.Create(QueueName);
        try
        {
          ConfigureUsers(val);
        }
        finally
        {
          ((IDisposable)val)?.Dispose();
        }
      }
      log.DebugFormat("Opening MSMQ [{0}]", QueueName);
      MessageQueue val2 = null;
      try
      {
        bool messageSigningEnabled = CollectorSettings.Instance.MessageSigningEnabled;
        val2 = new MessageQueue(QueueName);
        val2.set_Formatter((IMessageFormatter)(object)new CompressedMessageFormatter((IMessageFormatter)new BinaryMessageFormatter(), messageSigningEnabled ? new DataSignature() : null));
        val2.get_MessageReadPropertyFilter().set_ArrivedTime(true);
        if (messageSigningEnabled)
        {
          val2.get_MessageReadPropertyFilter().set_Extension(true);
        }
        if (val2.get_MaximumQueueSize() != maximumQueueSize)
        {
          val2.set_MaximumQueueSize(maximumQueueSize);
        }
        queue = val2;
      }
      catch (Exception exception)
      {
        log.Error("Failed to initialize queue " + QueueName, exception);
        if (val2 != null)
        {
          ((Component)val2).Dispose();
        }
        throw;
      }
      return val2;
    }
  }

CompressedMessageFormatter itself uses BinaryMessageFormatter as its formatter. Message signing is optionally enabled in the patched version.

Exhibit B: SolarWinds.Collector.Queue.CompressedMessageFormatter

CompressedMessageFormatter.Read() calls baseFormatter.Read(), where baseFormatter is an instance of BinaryMessageFormatter.

public object Read(Message message)
  {
    if (message == null)
    {
      throw new ArgumentNullException("message");
    }
    if (dataSignature != null)
    {
      message.get_BodyStream().Position = 0L;
      if (!dataSignature.VerifyData(message.get_BodyStream(), message.get_Extension()))
      {
        throw new VerificationException("Invalid data signature");
      }
    }
    byte[] array = new byte[compressHeader.Length];
    message.get_BodyStream().Position = 0L;
    message.get_BodyStream().Read(array, 0, array.Length);
    if (compressHeader.SequenceEqual(array))
    {
      message.get_BodyStream().Seek(array.LongLength, SeekOrigin.Begin);
      DeflateStream val = new DeflateStream(message.get_BodyStream(), (CompressionMode)0);
      try
      {
        Message val2 = new Message();
        val2.set_BodyType(message.get_BodyType());
        ((Stream)(object)val).CopyTo(val2.get_BodyStream());
        message.set_BodyStream(val2.get_BodyStream());
      }
      finally
      {
        ((IDisposable)val)?.Dispose();
      }
    }
    message.get_BodyStream().Position = 0L;
    return baseFormatter.Read(message);
  }

Data signature verification is performed in the patched version. Presumably, this behavior does not exist in unpatched versions.

Exhibit C: System.Messaging.BinaryMessageFormatter

The following code snippet is from Microsoft’s reference source.

/// <include file='doc\BinaryMessageFormatter.uex' path='docs/doc[@for="BinaryMessageFormatter.Read"]/*' />
        /// <devdoc>
        ///    This method is used to read the contents from the given message
        ///     and create an object.
        /// </devdoc>
        public object Read(Message message)
        {
            if (message == null)
                throw new ArgumentNullException("message");

            int variantType = message.BodyType;
            if (variantType == VT_BINARY_OBJECT)
            {
                Stream stream = message.BodyStream;
                return formatter.Deserialize(stream);
            }

            throw new InvalidOperationException(Res.GetString(Res.InvalidTypeDeserialization));
        }

Finally, BinaryMessageFormatter.Read() directly deserializes its input stream. In a vulnerable version, RCE should be as straightforward as sending a .NET gadget chain over the MSMQ protocol to TCP port 1801. An independent security researcher has verified this.

Guidance

SolarWinds Orion Platform customers should update to version 2020.2.4 immediately. Furthermore, it is advised to limit access to TCP port 1801 on the Orion Platform host, as the patch leaves the MSMQ queues unauthenticated.

References