Error0x01
- 关注
译文声明
本文是翻译文章,文章原作者 Trend Micro Research Team,文章来源:zerodayinitiative.com
原文地址:https://www.zerodayinitiative.com/blog/2023/2/27/cve-2022-38108-rce-in-solarwinds-network-performance-monitor
译文仅供参考,具体内容表达以及含义原文为准。
漏洞简述
SolarWinds 网络性能监控软件存在一个不安全的反序列化漏洞。该漏洞是由于 BytesToMessage 函数中未对用户提供的数据进行充分验证而导致的。经过远程身份验证的攻击者可以通过向受影响的服务器发送精心构造的请求来利用此漏洞。成功利用漏洞可能会在 SYSTEM 安全上下文中导致任意代码执行。该漏洞已由 SolarWinds 进行了修复,漏洞编号为:CVE-2022-38108。
SolarWinds Orion 平台是许多 SolarWinds 产品使用的基础平台。该平台旨在将所有基于 Orion 的产品无缝集成到单个界面中。其中,网络性能监控 (NPM) 是这些 SolarWinds 产品之一,用于监控多厂商的网络设备和服务器。
SolarWinds Orion 平台采用开源消息代理 RabbitMQ 来在各个软件组件和代理之间发送和接收消息。RabbitMQ 支持高级消息队列协议版本 0-9-1 (AMQP 0-9-1),该协议被 SolarWinds 平台使用。SolarWinds 信息服务 (SWIS) 是其中一个模块,使用 RabbitMQ 与 SolarWinds Orion 平台中的其他服务通信。
为向 SWIS 发送消息,发送者可以将消息内容放置在路由键设置为“SwisPubSub”的 AMQP 帧中,并通过 TCP 端口 5671 将这些帧发送到 RabbitMQ。 AMQP 0-9-1 帧格式的详细信息将在下面的检测部分中描述。
JavaScript 对象表示法(JSON)是一种用于创建可由机器解析的人类可读输出的数据交换格式。JSON 对象的语法如下:
对象用花括号 {} 括起来
对象包含零个或多个由逗号 (",") 分隔的项目
项目由键和值组成。键和值之间用冒号 (":") 分隔
键必须是字符串(用引号括起来)
值必须是有效类型,包括字符串、数字、JSON 对象、数组、布尔值或 null
数组是用方括号 [] 括起来的对象
数组由零个或多个字符串、数字、JSON 对象、数组、布尔值或 null 类型的对象组成,由逗号 (",") 分隔
一个JSON 对象的示例如下:
{"name":"bob", "age":30}
存在一个不安全的反序列化漏洞在SolarWinds NPM中。通过RabbitMQ发送给SWIS的消息内容包含Json.NET序列化的对象。当接收到消息内容时,将调用.NET类EasyNetQ.DefaultMessageSerializationStrategy的DeserializeMessage()方法来处理消息内容。该方法将调用.NET类SolarWinds.MessageBus.RabbitMQ.EasyNetQSerializer的BytesToMessage()方法来反序列化消息内容。BytesToMessage()将调用.NET类SolarWinds.Newtonsoft.Json.JsonConvert的DeserializeObject()方法,该方法最终将调用.NET类中的Deserialize()方法。
SolarWinds.Newtonsoft.Json.Serialization.JsonSerializerInternalReader用于反序列化Json.NET序列化对象。
然而,上述所有方法都没有对消息内容进行适当的验证,以确定给定的对象类型是否安全进行反序列化。尽管SolarWinds已经有一个.NET类BlackListBinder,用于检查给定的对象类型是否与可疑类型名称匹配,但SWIS应用程序在处理来自RabbitMQ的消息内容时不使用BlackListBinder。攻击者可以通过RabbitMQ向SWIS应用程序发送消息,其中的消息内容包含从YsoSerial.Net中派生的恶意Json.NET序列化对象,并触发服务器上的任意代码执行。
一个远程攻击者如果有权限访问RabbitMQ消息代理,就可以通过向目标应用程序发送精心构造的消息来利用这个漏洞。成功利用该漏洞可以导致在SYSTEM安全上下文中执行任意代码。
源代码概述
以下代码片段取自SolarWinds NPM 2020.2.6版本。
反编译 .NET 类 EasyNetQ.DefaultMessageSerializationStrategy:
public IMessage DeserializeMessage(MessageProperties properties, byte[] body)
{
Type messageType = this.typeNameSerializer.DeSerialize(properties.Type);
// call to deserialize the body (a byte[] array)
object body2 = this.serializer.BytesToMessage(messageType, body);
return MessageFactory.CreateInstance(messageType, body2, properties);
}
反编译 .NET 类 SolarWinds.MessageBus.RabbitMQ.EasyNetQSerializer:
public IMessage DeserializeMessage(MessageProperties properties, byte[] body)
{
Type messageType = this.typeNameSerializer.DeSerialize(properties.Type);
// call to deserialize the body (a byte[] array)
object body2 = this.serializer.BytesToMessage(messageType, body);
return MessageFactory.CreateInstance(messageType, body2, properties);
}
public object BytesToMessage(Type messageType, byte[] bytes)
{
string @string = Encoding.UTF8.GetString(bytes); // serialzed object carried in RabbitMQ message EasyNetQSerializer._log.TraceFormat("Decoding msg to type {0}: {1}", new object[]
{
"messageType",
@string }
);
// call DeserializeObject() without checking the messageType
return JsonConvert.DeserializeObject(@string, messageType, this.serializerSettings);
}
反编译 .NET 类 SolarWinds.Newtonsoft.Json.Serialization.JsonSerializerInternalReader:
public object Deserialize(JsonReader reader, Type objectType, bool checkAdditionalContent)
{
if (reader == null)
{
throw new ArgumentNullException("reader");
}
// call GetContractSafe() to check objectType but does not use BlackListBinder class
JsonContract contractSafe = this.GetContractSafe(objectType);
object result;
try
{
JsonConverter converter = this.GetConverter(contractSafe, null, null, null);
if (reader.TokenType == JsonToken.None &&
!this.ReadForType(reader, contractSafe, converter != null))
{
if (contractSafe != null && !contractSafe.IsNullable)
{
throw JsonSerializationException.Create(reader,
"No JSON content found and type '{0}' is not nullable.".FormatWith(CultureInfo.InvariantCulture,
contractSafe.UnderlyingType));
}
result = null;
}
else
object obj;
if (converter != null && converter.CanRead)
{
obj = this.DeserializeConvertable(converter, reader, objectType, null);
}
else
{ // call to begin parsing JSON data and converting to object
// but still without any type checking and trigger insecure deserialization
obj = this.CreateValueInternal(reader, objectType, contractSafe, null,
null, null, null);
}
if (checkAdditionalContent && reader.Read() && reader.TokenType != JsonToken.Comment)
{
throw new JsonSerializationException(
"Additional text found in JSON string after finishing deserializing object.");
}
result = obj;
}
}
catch (Exception ex)
{
[... truncated for readability ...]
漏洞利用方法
为了检测利用此漏洞的攻击,检测设备必须监视并解析 TCP 端口 5671 上的流量。请注意,流量经过 SSL/TLS 加密,应在执行以下步骤之前进行解密。
检测设备首先必须确定客户端是否正在尝试通过以下格式显示的协议头启动AMQP 0-9-1连接:
Offset Length Description
(bytes)
------ ------- ----------------------------------------
0x00 4 \x41\x4d\x51\x50 (String literal "AMQP")
0x04 1 protocol id (always \x00)
0x05 3 version (always \x00\x09\x01)
如果找到这样的 AMQP 协议头,检测设备必须继续监视流量并查找 AMQP 0-9-1 帧。
AMQP 0-9-1帧
AMQP 0-9-1帧携带协议方法、消息内容和其他信息。所有帧都具有相同的一般格式:帧头、有效负载和帧结尾。帧结尾由字节值\ CE表示。一般帧格式如下所示:
Offset Length Description
(bytes)
------ ------- ----------------------------------------
0x00 1 frame type (four frame types)
\x01: Method frame
\x02: Content header frame
\x03: Content body frame
\x04: Heartbeat frame
0x01 2 channel
0x03 4 frame payload size (n)
0x07 n frame payload (the format depends on its frame type)
0x07+n 1 frame end (\xCE)
AMQP 0-9-1共有四种帧类型:Method、Content header、Content body和Heartbeat。每种帧类型都有自己的帧载荷格式。Method、Content header和Content body帧的载荷格式与本报告有关,并将在以下段落中进行描述。请注意,在以下段落中,“偏移量”值是相对于帧载荷开头(在上面显示的一般帧格式的偏移量0x07)的。
Method帧的载荷格式如下:
Offset Length Description
(bytes)
------ ------- ----------------------------------------
0x00 2 Class id
0x02 2 Method id
0x04 n/a Method arguments list
Method Arguments List字段根据其Method id,可以包含各种数据类型的零个或多个数据值。每种数据类型都在自己的规范中表示。这些数据类型包括整数、比特、字符串、时间戳和字段表(键值对)。本报告不会描述每个数据表示的详细信息。
Content header帧的载荷格式如下:
Offset Length Description
(bytes)
------ ------- ----------------------------------------
0x00 2 Class id
0x02 2 Weight (not used, must be \x00)
0x04 8 Body size (Total size of the binary content carried in following "Content body" frames)
0x0C 2 Property Flags
0x0E n/a Property List
发送方想要发送给接收方的用户数据可以在一个或多个Content body帧中传输,也就是说,用户数据可以被分成多个Content body帧。Content header帧的Body size字段用于记录发送方发送的原始用户数据(在分割之前)的总大小。
Content body帧的载荷格式如下:
Offset Length Description
(bytes)
------ ------- ----------------------------------------
0x00 n/a Binary content
Binary content字段携带发送方想要发送到接收方的整个(如果未分割)或部分用户数据。
检测设备必须首先查找包含路由键“SwisPubSub”的Method帧。为此,检测设备必须检查每个帧,以查看其帧类型是否为\x01,类id是否为\x3c(Basic类),方法id是否为\x28(Publish),以及其方法参数列表字段是否包含字符串“SwisPubSub”。如果找到这样的帧,则检测设备必须继续查找Content body帧,并将其Binary content字段作为JSON格式数据进行检查,以查看其是否包含恶意序列化对象。请注意,JSON格式数据可能会被拆分为多个Content body帧,并且检测设备必须能够从这些Content body帧中将所有Binary content字段组装在一起,然后再进行检查。
JSON 格式的数据必须使用以下任何合适的解析方法进行检查:
方法1 - 检测设备可以解析JSON
检测设备必须搜索“$type”键并检查其值是否以以下任何字符串之一开头:
System.Windows.Data.ObjectDataProvider
System.Security.Principal.WindowsPrincipal
System.Security.Principal.WindowsIdentity
Microsoft.IdentityModel.Claims.WindowsClaimsIdentity
System.Web.UI.MobileControls.SessionViewState+SessionViewStateHistoryItem
System.IdentityModel.Tokens.SessionSecurityToken
System.Web.Security.RolePrincipal
如果发现该值,则应将该流量视为恶意,并且很可能正在进行利用此漏洞的攻击。
将类名(如“System.Windows.Data.ObjectDataProvider”)用作判断此漏洞是否被利用的指标,是基于以下观察:这个不安全的反序列化漏洞依赖于使用YsoSerial.Net gadget,而这个工具与Json.NET配合使用;另外,此漏洞的修复方案是使用了拒绝列表(deny-list)来验证序列化对象的类型。
方法二 - 基于字符串的检测
检测设备必须检查JSON格式数据是否包含可以与以下正则表达式匹配的字符串:
\x22\s*\$type\s*\x22\s*:\s*\x22\s*(System\.Windows\.Data\.ObjectDataProvider
|System\.Security\.Principal\.WindowsPrincipal
|System\.Security\.Principal\.WindowsIdentity
|Microsoft\.IdentityModel\.Claims\.WindowsClaimsIdentity
|System\.Web\.UI\.MobileControls\.SessionViewState+SessionViewStateHistoryItem
|System\.IdentityModel\.Tokens\.SessionSecurityToken
|System\.Web\.Security\.RolePrincipal)
如果找到匹配的内容,应该将流量视为恶意,并且可能正在利用此漏洞进行攻击。以下是JSON格式中恶意序列化对象的示例:
{
"$type":"System.Windows.Data.ObjectDataProvider, PresentationFramework,
Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35",
"MethodName":"Start",
"MethodParameters":{
"$type":"System.Collections.ArrayList, mscorlib, Version=4.0.0.0,
Culture=neutral, PublicKeyToken=b77a5c561934e089",
"$values":["cmd", "/c whoami > C:\\poc.txt"]
},
"ObjectInstance":{"$type":"System.Diagnostics.Process, System, Version=4.0.0.0,
Culture=neutral, PublicKeyToken=b77a5c561934e089"}
}
注意,所有的字符串匹配都应以不区分大小写的方式进行。
结尾
SolarWinds公司发布了SolarWinds平台2022.4 RC1及更高版本来解决此漏洞。除了安装更新版本外,企业还可以通过阻止不需要的外部网络访问受影响的端口来保护自己。该厂商还建议在部署SolarWinds平台和SQL Server实例的网络上应用适当的分段控制。这也应该被视为最佳实践和强大防御策略的一部分。他们还建议客户遵循SolarWinds安全配置指南中提供的指导。最后,请确保只有经过授权的用户才能访问SolarWinds平台。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)