每个系统管理员都熟悉 Veeam 面向企业的备份解决方案“Veeam Backup & Replication”。不幸的是,每个勒索软件运营商也都熟悉,因为它在大多数企业网络的存储领域中都处于某种“特权地位”。除非您还可以拒绝对备份的访问,否则在目标上部署 cryptolocker 恶意软件是没有意义的,因此,这类攻击者绝对喜欢破坏这种特定的软件。
既然有这么多的目光聚焦于它,那么它拥有丰富的 CVE 历史也就不足为奇了。今天,我们将看看最新的一集——CVE-2024-40711。
该漏洞由Code White Gmbh的Florian Hauser报告,他们在网站上声明该漏洞是未经认证的 RCE。Veeam 自己已经发布了修复程序,他们的公告告诉我们该漏洞会影响版本及以下版本。12.1.2.172
不过,Code White 并未透露详细信息,我们对如此热门目标中存在如此强大的漏洞感到好奇。我们亲自去寻找根本原因,发现了一组技术上有趣的漏洞 - 以及比乍一看更复杂的修补情况。
我们从头开始,在咨询处,为了您的方便而复制在这里:
我们的第一个好奇心从这里开始。仔细查看 CVSS 评分,您会注意到“所需权限”选项已设置为“低”,表示需要身份验证,即使文本声称该错误是“未经身份验证的”。也许这是一个错误。
关注 Code White 的工作,我们可以看到他们通过 Twitter(呃,我的意思是通过 X)发布了一个利用视频:
发布的 PoC 视频演示了对 版本的利用12.1.0.2131,这看起来有点奇怪——为什么不使用最新受影响的版本12.1.2.172?也许这就是他们手头上的全部内容。无论如何,以下是完整内容,如果你想看的话:
Veeam 还发布了一个表格,其中包含所有受影响的 Veeam Backup & Replication 版本及其各自的版本号。
Code White 没有包含有关漏洞类型的任何信息,因此我们的第一步是启动补丁差异过程以确定漏洞的根本原因。
通常,当人们决定开始对漏洞进行补丁差异化时,他们会获得最新版本和前一个版本,因此差异数量将被最小化。这使得研究人员可以专注于与安全相关的领域,而忽略与功能相关的更改。这正是我们所做的 - 但很快就被证明是一个错误。
补丁差异混乱
我们希望这个过程像往常一样快速,于是我们开始着手比较最新版本( )12.2.0.334与前一版本( )12.1.2.172的差异。但很快,我们意识到变化的规模超出了预期——准确地说,有 2,600 个文件被更改了!
幸运的是,其中 700 个文件只包含微小的更改,可以快速排除 - 诸如更新版本号之类的琐碎事情。这给我们留下了 1,900 个更改的文件,包括 .NET 类文件、配置文件和资源。这是一个巨大的数字 - 安全补丁真的会影响这么多文件吗!?当然不会!
已更改文件的摘录
这里值得一提的另一件有趣的事情是,最新版本不仅修复了与我们正在寻找的漏洞 CVE-2024-40711 相关的漏洞;相反,据 Veeam 称,它修复了多个安全问题,这使得将这些部分联系在一起变得更加困难。哪些漏洞与 Code White 漏洞有关,哪些不相关?
在怀疑的旁观者看来,Veeam 的员工可能故意让我们的生活变得困难 - 将安全更新与功能相关的更改混在一起。或者,大量的更改可能是巧合,漏洞修复只是与计划中的功能发布相吻合。
经过几个小时的审查,我们开始注意到针对不同漏洞的修复痕迹。让我们从似乎与 Code White 漏洞相关的更改开始。
黑名单上又有新成员
12.2.0.334在对和进行补丁比较时12.1.2.172,我们发现 中嵌入的资源文件发生了更改Veeam.Backup.Common.dll。这不是查找安全相关补丁的常用位置!
以下是对这一变化的详细分析:
System.Runtime.Remoting.ObjRef这是一个非常简单的变化 - 我们可以看到 Veeam 为.NET 类类型添加了一个新行。这是一个著名的 .NET 反序列化小工具,由Markus Wulftange创建,事实上,这是他的“标志性”攻击之一。鉴于该漏洞本身是由同样在 Code White 工作的Florian Hauser发现的,人们可以开始将这些点联系起来 - 这一定是与他们的漏洞相关的补丁之一!
此黑名单文件是一份禁止使用的 .NET 类类型列表,已知这些类类型可用于反序列化攻击。此补充表明 CVE-2024-40711 与反序列化攻击有关。
但真的就这么简单吗?只是黑名单中新增了一个小工具条目?合乎逻辑的下一步就是找到这个黑名单的使用地点,然后通过这个缺失的小工具找到它所保护的内容……然后我们就得到了 RCE,对吧?
别这么快——事实远比这复杂得多!
Veeam .NET Remoting 内部原理
Veeam Backup & Replication 严重依赖 .NET Remoting,许多 Veeam 服务(所有服务都以 运行NT Authority/System,您会感兴趣的)都在监听 .NET Remoting 通信。多年来,由于这些接口具有强大的“反序列化绑定”(稍后会详细介绍),没有人能够利用它们 - 也许Florian Hauser已经成功利用了它们。不过,在进一步讨论之前,让我们先看看 Veeam 过去是如何保护自己免受此类攻击的。
Veeam 在库中实现了其核心 .NET Remoting 架构Veeam.Common.Remoting.dll。此类可执行数百项操作,但我们只对了解 Veeam 的 .NET Remoting 实现如何处理序列化的远程处理请求感兴趣。
我们经常看到人们尝试实现他们自己的自定义 .NET Remoting 服务器,为了做到这一点,他们遵循有关此主题的古老文档(因为所有酷孩子都使用 WCF!)。为了拥有自己的自定义 .NET Remoting 实现,他们必须创建从IServerChannelSink或派生的类IClientChannelSink。
2014 年,James Forshaw 在“谈到 .NET Remoting 时,愚蠢就是愚蠢”一文中谈到了这个话题。从下面的图表(借用 James 的博客文章)中可以看出,这里有两个关键元素:
-
运输接收器
-
格式化接收器
“Transport Sink” 只是一个派生自IServerChannelSink或 的类IClientChannelSink(取决于您负责哪一方的通信)。它将通过实现某些方法(例如)来处理 .NET Remoting 数据包的接收和处理ProcessMessage。这些方法负责与执行大量反序列化工作的“Formatter Sink”类进行交互。
Veeam 正是这样做的,在Veeam.Common.Remoting.CBinaryServerFormatterSink类中实现了他们的自定义 .NET Remoting 服务器:
如您所见,此类实现了接口IServerChannelSink:
正如预期的那样,它有自己的ProcessMessage方法实现。这个方法非常庞大,因此我们不提供图片,而是在此处提供它的摘要版本并根据行号进行解释。我们需要检查此代码,以便真正了解 Veeam 的远程实现为反序列化远程请求采取了哪些步骤。
每当收到 .NET Remoting 消息时,都会调用此方法。乍一看,它可能看起来很复杂,但它主要是样板代码,处理开发人员在收到 .NET Remoting 消息时需要处理的无聊事情。让我们深入了解一下。
在第 45 行,__RequestUri提取标头以确保ObjectUri访问正确。这__RequestUri是 .NET Remoting 数据包的一部分,用于将 URI 映射到特定对象,需要满足此条件,这很容易做到
然后,在第 49 行,requestStream将仅包含序列化数据的对象传递给要DeserializeBinaryRequestMessage反序列化的方法,并将其返回值赋给变量requestMsg。这就是反序列化发生的地方。接下来,我们需要研究这个方法,并弄清楚它到底是如何完成的。
1: public ServerProcessing ProcessMessage(IServerChannelSinkStack sinkStack, IMessage requestMsg, ITransportHeaders requestHeaders, Stream requestStream, out IMessage responseMsg, out ITransportHeaders responseHeaders, out Stream responseStream)
2: {
[..SNIP..]
43: try
44: {
45: if (RemotingServices.GetServerTypeForUri((string)requestHeaders["__RequestUri"]) == null)
46: {
47: throw new RemotingException(string.Format("Remoting Channel Sink UriNotPublished. RequestUri is '{0}'", requestHeaders["__RequestUri"]));
48: }
49: requestMsg = CBinaryServerFormatterSink.DeserializeBinaryRequestMessage(requestStream, requestHeaders);
50: if (requestMsg == null)
51: {
52: throw new RemotingException("Remoting Deserialize Error");
53: }
54: IMethodMessage methodMessage = requestMsg as IMethodMessage;
55: if (methodMessage != null)
56: {
57: string text3 = requestHeaders["access_token"] as string;
58: Dictionary<string, object> dictionary;
59: EJwtValidationResult ejwtValidationResult = this._mfaProvider.ValidateToken(text3, out dictionary); // (*_*)
60: if (ejwtValidationResult == EJwtValidationResult.Empty || ejwtValidationResult == EJwtValidationResult.Invalid)
61: {
62: this.EnsureMfa(requestHeaders);
63: }
64: this.EnsureAccessIsAllowed(methodMessage);
65: }
此方法的实现如下:
Veeam.Common.Remoting.CBinaryServerFormatterSink.DeserializeBinaryRequestMessage(Stream, ITransportHeaders)
看看代码,它看起来很简单,而且显然它所做的正是我们期望它做的事情。它创建了一个FormatterSink我们之前提到的requestStream对象来反序列化。让我们看看他们是如何实现这个“Formatter Sink”的。
格式化程序接收器在以下位置实现
Veeam.Common.Remoting.CBinaryServerFormatterSink.CreateFormatter(bool)
首先,它将创建一个实例,这通常BinaryFormatter是.NET Remoting 格式化程序接收器的制作方式。然后,它会做一些非常有趣的事情 - 它将变量的属性分配给自定义绑定器类。这正是我们对 Veeam 的期望,以便防止反序列化攻击。他们利用绑定器的概念来控制允许反序列化哪些类型。他们还将二进制格式化程序的属性分配给。BinderbinaryFormatterFilterLevelTypeFilterLevel.Low
现在正常情况下,我们需要看一下,RestrictedSerializationBinder但是在进入它的实现之前,请注意传递给这个自定义绑定类的一个非常重要的参数 - 第二个参数提到RestrictedSerializationBinder.Modes.FilterByWhitelist。
白名单?啊?难道不是我们补丁中的黑名单更改吗?为什么这里要传递白名单?在我们进一步深入研究时,请记住这个问题!
这个自定义绑定器是在另一个完全独立的库中实现的veeam.backup.common.dll,名为Veeam.Backup.Common.RestrictedSerializationBinder。这是类构造函数的样子。它需要两个参数,第二个参数用于属性_mode。如果未指定,它将默认为FilterByWhiteList。
我们还看一下活页夹类中的重要方法RestrictedSerializationBinder。
我们首先从查看EnsuredBlackWhitelistsAreLoaded方法开始。看起来,该方法负责加载白名单和黑名单文件,并调用CWhiteList()和CBlackList()类来实现这一点。
继续深入研究,我们将研究这两个类。CWhitelist 类的实现方式很简单,手动添加一些允许的类型,然后调用FillFromEmbeddedResource从文件中加载类名的方法whitelist.txt,并使用它们来填充this._allowedTypeFullNames属性。
我们已尝试最小化此代码,以使其易于阅读。此类(和CBlacklist)中还有一些其他方法可以解析.txt特定格式的文件,但重点是我们根据文本文件的内容填充条目。
namespace Veeam.Backup.Common
{
// Token: 0x020003BC RID: 956
public class CWhitelist
{
// Token: 0x060017AD RID: 6061 RVA: 0x0003EA6C File Offset: 0x0003CC6C
public CWhitelist()
{
this._allowedTypeFullNames.Add(typeof(LogicalCallContext).AssemblyQualifiedName);
this._allowedTypeFullNames.Add("System.UnitySerializationHolder, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089");
this._allowedTypeFullNames.Add(typeof(EndPoint).AssemblyQualifiedName);
this._allowedTypeFullNames.Add(typeof(DnsEndPoint).AssemblyQualifiedName);
this._allowedTypeFullNames.Add(typeof(IPEndPoint).AssemblyQualifiedName);
this._allowedTypeFullNames.Add(typeof(AddressFamily).AssemblyQualifiedName);
this._allowedTypeFullNames.Add(typeof(IPAddress).AssemblyQualifiedName);
this._allowedTypeFullNames.Add(typeof(SocketAddress).AssemblyQualifiedName);
this.FillFromEmbeddedResource();
string text = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "whitelist.txt");
if (File.Exists(text))
{
this.FillFromFile(text);
}
}
// Token: 0x060017AE RID: 6062 RVA: 0x0003EB88 File Offset: 0x0003CD88
private void FillFromEmbeddedResource()
{
Assembly executingAssembly = Assembly.GetExecutingAssembly();
string text = "Veeam.Backup.Common.Sources.System.IO.BinaryFormatter.whitelist.txt";
using (Stream manifestResourceStream = executingAssembly.GetManifestResourceStream(text))
{
using (StreamReader streamReader = new StreamReader(manifestResourceStream))
{
this.FillInternal(streamReader);
}
}
}
[..SNIP..]
为了完整起见,我们还研究了 CBlacklist 的实现。它使用相同的方法,加载文件以使用明确不允许的类型blacklist.txt填充属性。_notAllowedTypeFullNames
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
namespace Veeam.Backup.Common
{
// Token: 0x0200038C RID: 908
public class CBlacklist
{
// Token: 0x06001695 RID: 5781 RVA: 0x0003C7BC File Offset: 0x0003A9BC
public CBlacklist()
{
this.FillFromEmbeddedResource();
string text = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "blacklist.txt");
if (File.Exists(text))
{
this.FillFromFile(text);
}
}
// Token: 0x06001696 RID: 5782 RVA: 0x0003C80C File Offset: 0x0003AA0C
private void FillFromEmbeddedResource()
{
Assembly executingAssembly = Assembly.GetExecutingAssembly();
string text = "Veeam.Backup.Common.Sources.System.IO.BinaryFormatter.blacklist.txt";
using (Stream manifestResourceStream = executingAssembly.GetManifestResourceStream(text))
{
using (StreamReader streamReader = new StreamReader(manifestResourceStream))
{
this.FillInternal(streamReader);
}
}
}
[..SNIP..]
呼!代码量很大,分析也很多。不过,我们最终发现的很简单。这两个类执行以下操作:
-
加载whitelist.txt文件并使用它来填充_allowedTypeFullNames属性,然后
-
加载blacklist.txt文件并使用它来填充_notAllowedTypeFullNames属性。
但是这些属性接下来如何使用呢?好吧,为了找到答案,我们需要再次回到类中RestrictedSerializationBinder。代码如下 - 我们省略了类实现,这里只包含了两个重要的方法。
反序列化过程中针对不同的类类型调用typeResolveType方法。每次调用此方法时,都会首先调用该EnsureTypeIsAllowed方法,如第 3 行所示。
该EnsureTypeIsAllowed方法的作用正如其名称所示 - 它将使用白名单或黑名单来检查是否允许反序列化给定类型。决定检查哪个列表(黑名单或白名单)的属性名为_mode,并由构造函数设置。
[..SNIP..]
1: protected override Type ResolveType([TupleElementNames(new string[] { "assemblyName", "typeName" })] ValueTuple<string, string> key)
2: {
3: this.EnsureTypeIsAllowed(key);
4: Type type = base.ResolveType(key);
5: RestrictedSerializationBinder.CheckIsRestrictedType(type);
6: return type;
7: }
8:
9:
10: private void EnsureTypeIsAllowed([TupleElementNames(new string[] { "assemblyName", "typeName" })] ValueTuple<string, string> key)
11: {
12: if (!this._serializingResponse && SOptions.Instance.ShouldWhitelistingRemoting)
13: {
14: this.EnsuredBlackWhitelistsAreLoaded();
15: string text = key.Item2 + ", " + key.Item1;
16: if (this._mode == RestrictedSerializationBinder.Modes.FilterByWhitelist)
17: {
18: RestrictedSerializationBinder._allowedTypeFullnames.EnsureIsAllowed(text);
19: return;
20: }
21: if (this._mode == RestrictedSerializationBinder.Modes.FilterByBlacklist)
22: {
23: RestrictedSerializationBinder._notAllowedTypeFullnames.EnsureIsAllowed(text);
24: }
25: }
26: }
[..SNIP..]
听起来,到现在为止,一切都已准备就绪。我们之前看到,该ObjRef类型已添加到黑名单中,因此我们需要做的就是点击“黑名单”分支(第 21 行),并为反序列化器提供一个序列化ObjRef- 然后我们就可以获得梦寐以求的 RCE,游戏结束了,对吧?!
嗯,是的 - 但这比看起来要复杂一些。
我们之前看到,在创建“Formatter Sink”时,二进制格式化程序实例会提供FilterByWhiteList给绑定器构造函数。这意味着 Veeam 实施的 .NET Remoting始终使用白名单,而不是我们的小工具所在的黑名单!
这是否意味着我们需要绕过白名单?但白名单在最新版本中没有改变,那么这里发生了什么?!这变得更加有趣!
不那么受限制的序列化绑定
当我们意识到 Veeam .NET Remoting 代码使用的是白名单而不是黑名单(我们的绕过方法就在这里)时,我们就开始想事情可能没那么简单,也许这里没有直接的利用途径,也许还有更多的障碍需要克服。
一个重要的细节是,该RestrictedSerializationBinder类型是在Veeam.Backup.common.dll程序集内部实现的,而不是Veeam.Common.Remoting.dll我们预期的那样。这意味着 Veeam 不仅将绑定器用于其 .NET Remoting 实现,还将其用于其他目的。
快速搜索参考资料后RestrictedSerializationBinder我们发现以下类使用了此活页夹:
\Veeam.Backup.Common\Common\RestrictedSerializationBinder.cs
\Veeam.Common.Remoting\Common\Remoting\CBinaryServerFormatterSink.cs
\Veeam.Common.Remoting\Common\Remoting\CCoreChannel.cs
\Veeam.Common.Remoting\Common\Remoting\CImpersonationServerSink.cs
\Veeam.Backup.Common\Core\CProxyBinaryFormatter.cs
在分析了所有这些类之后,我们注意到该类上有一些有趣的方法CProxyBinaryFormatter- 即它有一些有希望的方法名称表明它正在执行反序列化和序列化。
一些检查显示,此类具有序列化和反序列化数据的方法,并且它使用了RestrictedSerializationBinder我们之前讨论过的相同绑定器。由于暴露了许多静态方法,因此当开发人员想要处理反序列化/序列化时,它充当“辅助”类。在我们寻找 CVE 的过程中,它的方法对我们来说非常有趣。
例如,在第 18 行,存在一个名为的静态方法CreateWithRestrictedBinder,它将实例化该类,并指定FilterByWhiteList我们之前看到的模式。但是,如果我们继续搜索,还有另一个更有趣的静态方法 - 看看第 83 行,你会发现一个名为的方法Deserialize。
此方法需要一个类型为 的参数string,然后它将对其进行 base64 解码(在第 88 行)以创建一个字节数组。接下来,它将创建该类的一个实例BinaryFormatter,最后,它将在第 93 行反序列化字节数组并在第 100 行返回对象。
然而,仔细检查就会发现一个关键的细节,该细节BinaryFormatter是通过FilterByBlackList论点来实例化的!
这对我们来说太棒了!我们终于找到了一种使用绑定器和黑名单模式的方法——我们之前已经看到过,我们可以通过提供一个ObjRef要反序列化的类来绕过它!
1: using System;
2: using System.IO;
3: using System.Runtime.Serialization.Formatters.Binary;
4: using Veeam.Backup.Common;
5:
6: namespace Veeam.Backup.Core
7: {
8: public class CProxyBinaryFormatter
9: {
10: private CProxyBinaryFormatter(RestrictedSerializationBinder binder)
11: {
12: this._formatter = new BinaryFormatter
13: {
14: Binder = binder
15: };
16: }
17:
18: public static CProxyBinaryFormatter CreateWithRestrictedBinder()
19: {
20: return new CProxyBinaryFormatter(new RestrictedSerializationBinder(false, RestrictedSerializationBinder.Modes.FilterByWhitelist));
21: }
22:
23: public T[] DeserializeCustom<T>(string[] itemsArray)
24: {
25: T[] array = new T[itemsArray.Length];
26: for (int i = 0; i < itemsArray.Length; i++)
27: {
28: array[i] = CProxyBinaryFormatter.Deserialize<T>(itemsArray[i]);
29: }
30: return array;
31: }
32:
33: public T DeserializeCustom<T>(string input)
34: {
35: T t;
36: try
37: {
38: t = CProxyBinaryFormatter.BinaryDeserializeObject<T>(Convert.FromBase64String(input), this._formatter);
39: }
40: catch (Exception ex)
41: {
42: Log.Exception(ex, "Binary deserialization failed", Array.Empty<object>());
43: throw;
44: }
45: return t;
46: }
47:
48: public static string Serialize(object obj)
49: {
50: string text;
51: try
52: {
53: text = Convert.ToBase64String(CProxyBinaryFormatter.BinarySerializeObject(obj));
54: }
55: catch (Exception ex)
56: {
57: Log.Exception(ex, "Binary serialization failed", Array.Empty<object>());
58: throw;
59: }
60: return text;
61: }
62:
63: public static string[] Serialize<T>(T[] itemsArray)
64: {
65: string[] array = new string[itemsArray.Length];
66: for (int i = 0; i < itemsArray.Length; i++)
67: {
68: array[i] = CProxyBinaryFormatter.Serialize(itemsArray[i]);
69: }
70: return array;
71: }
72:
73: public static T[] Deserialize<T>(string[] itemsArray)
74: {
75: T[] array = new T[itemsArray.Length];
76: for (int i = 0; i < itemsArray.Length; i++)
77: {
78: array[i] = CProxyBinaryFormatter.Deserialize<T>(itemsArray[i]);
79: }
80: return array;
81: }
82:
83: public static T Deserialize<T>(string input)
84: {
85: T t;
86: try
87: {
88: byte[] array = Convert.FromBase64String(input);
89: BinaryFormatter binaryFormatter = new BinaryFormatter
90: {
91: Binder = new RestrictedSerializationBinder(false, RestrictedSerializationBinder.Modes.FilterByBlacklist)
92: };
93: t = CProxyBinaryFormatter.BinaryDeserializeObject<T>(array, binaryFormatter);
94: }
95: catch (Exception ex)
96: {
97: Log.Exception(ex, "Binary deserialization failed", Array.Empty<object>());
98: throw;
99: }
100: return t;
101: }
102:
103: private static T BinaryDeserializeObject<T>(byte[] serializedType, BinaryFormatter deserializer)
104: {
105: if (serializedType == null)
106: {
107: throw new ArgumentNullException("serializedType");
108: }
109: if (serializedType.Length.Equals(0))
110: {
111: throw new ArgumentException("serializedType");
112: }
113: T t;
114: using (MemoryStream memoryStream = new MemoryStream(serializedType))
115: {
116: object obj = deserializer.Deserialize(memoryStream);
117: t = ((obj == DBNull.Value) ? default(T) : ((T)((object)obj)));
118: }
119: return t;
120: }
121:
122: private static byte[] BinarySerializeObject(object objectToSerialize)
123: {
124: byte[] array;
125: using (MemoryStream memoryStream = new MemoryStream())
126: {
127: new BinaryFormatter().Serialize(memoryStream, objectToSerialize ?? DBNull.Value);
128: array = memoryStream.ToArray();
129: }
130: return array;
131: }
132:
133: private readonly BinaryFormatter _formatter;
134: }
135: }
136:
好的,我们快到了。下一个问题是,我们如何才能找到这个方法?要回答这个问题,我们需要查看Deserialize使用此特定方法的所有地方。搜索显示有 547 个类 - 数量非常多。
这个数字本身就很有趣。如果有那么多地方使用这种方法,如果其中一个地方是 Veeam白名单中存在的类,该怎么办?如果是这样的话,我们可以滥用白名单允许的类型,以便到达已配置为使用黑名单模式的受限序列化绑定器。这使我们能够执行桥接技术,从受限反序列化跳转到不受限制的反序列化,并最终反序列化我们的ObjRef小工具以进行 RCE!
我们交叉引用了所有使用该方法且存在于 Veeam 白名单中的类Deserialize。结果只有 3 个类 - 我们快找到了!
CEpContainerSaveInfo
CDbCryptoKeyInfo
CUserRequestSpecificationConfigurationBackup
我们先来看看这个CDbCryptoKeyInfo类。记住,这个类也是白名单,也就是说可以通过 .NET Remoting 访问。这是一个很大的类,所以我们只在下面包含了相关的部分。你可能注意到,在第 75 行,我们Deserialize调用的方法存在漏洞。由于这个类被标记为Serializable(第 16 行),当反序列化这个类的对象时,会自动调用该方法。太棒了!这是一个我们可以使用的自定义小工具!
1: using System;
2: using System.Collections.Generic;
3: using System.Linq;
4: using System.Runtime.Serialization;
5: using System.Security.Cryptography;
6: using System.Xml;
7: using Veeam.Backup.Common;
8: using Veeam.Backup.Configuration.DataProtection;
9: using Veeam.Backup.Core;
10: using Veeam.Backup.Logging;
11: using Veeam.Backup.Meta;
12: using Veeam.Framework.Basic.DateAndTime;
13:
14: namespace Veeam.Backup.Model
15: {
16: [Serializable]
17: public class CDbCryptoKeyInfo : ISerializable, IConcurentTracking, IEquatable<CDbCryptoKeyInfo>, ILoggable, IMetaRecoveryKeyInfo, IMetaCryptoKey, IMetaEntity, IMetaElement, IMetaVisitable
18: {
19: private CDbCryptoKeyInfo(Guid id, CKeySetId keySetId, EDbCryptoKeyType keyType, ECryptoAlg cryptoAlg, byte[] encryptedKeyValue, string hint, DateTimeUtc modificationDateUtc, long version, Guid backupId, bool isImported, string tag)
20: {
21: this.BackupId = backupId;
22: this.Id = id;
23: this.KeySetId = keySetId;
24: this.KeyType = keyType;
25: this.EncryptedKeyValue = encryptedKeyValue;
26: this.Hint = hint;
27: this.ModificationDateUtc = modificationDateUtc;
28: this.CryptoAlg = cryptoAlg;
29: this.Version = version;
30: this.IsImported = isImported;
31: this.Tag = tag;
32: }
33:
34: [..SNIP..]
35:
36: private CDbCryptoKeyInfo(Guid id, CKeySetId keySetId, EDbCryptoKeyType keyType, ECryptoAlg cryptoAlg, byte[] encryptedKeyValue, string hint, DateTimeUtc modificationDateUtc, CRepairRec[] repairRecs, long version, Guid backupId, bool isImported, string tag)
37: : this(id, keySetId, keyType, cryptoAlg, encryptedKeyValue, hint, modificationDateUtc, version, backupId, isImported, tag)
38: {
39: this._repairRecs.AddRange(repairRecs);
40: }
41:
42: public CDbCryptoKeyInfo(byte[] encryptedKeyValue, Guid backupId)
43: {
44: this.BackupId = backupId;
45: this.EncryptedKeyValue = encryptedKeyValue;
46: }
47:
48: [..SNIP..]
49:
50: public void GetObjectData(SerializationInfo info, StreamingContext context)
51: {
52: info.AddValue("Id", this.Id);
53: info.AddValue("KeySetId", this.KeySetId.Value);
54: info.AddValue("KeyType", (int)this.KeyType);
55: info.AddValue("DecryptedKeyValue", Convert.ToBase64String(this.EncryptedKeyValue));
56: info.AddValue("Hint", this.Hint);
57: info.AddValue("ModificationDateUtc", this.ModificationDateUtc.Value);
58: info.AddValue("CryptoAlg", (int)this.CryptoAlg);
59: info.AddValue("RepairRecs", CProxyBinaryFormatter.Serialize<CRepairRec>(this._repairRecs.ToArray()));
60: info.AddValue("Version", this.Version);
61: info.AddValue("BackupId", this.BackupId);
62: info.AddValue("IsImported", this.IsImported);
63: }
64:
65: protected CDbCryptoKeyInfo(SerializationInfo info, StreamingContext context)
66: {
67: this.Id = (Guid)info.GetValue("Id", typeof(Guid));
68: byte[] array = (byte[])info.GetValue("KeySetId", typeof(byte[]));
69: this.KeySetId = new CKeySetId(array);
70: this.KeyType = (EDbCryptoKeyType)((int)info.GetValue("KeyType", typeof(int)));
71: this.EncryptedKeyValue = Convert.FromBase64String(info.GetString("DecryptedKeyValue"));
72: this.Hint = info.GetString("Hint");
73: this.ModificationDateUtc = info.GetDateTime("ModificationDateUtc").SpecifyDateTimeUtc();
74: this.CryptoAlg = (ECryptoAlg)info.GetInt32("CryptoAlg");
75: this._repairRecs = CProxyBinaryFormatter.Deserialize<CRepairRec>((string[])info.GetValue("RepairRecs", typeof(string[]))).ToList<CRepairRec>();
76: this.Version = info.GetInt64("Version");
77: this.BackupId = (Guid)info.GetValue("BackupId", typeof(Guid));
78: this.IsImported = info.GetBoolean("IsImported");
79: }
80:
81: [..SNIP..]
把(几乎)所有东西放在一起
我们已经深入研究了一些相当复杂的代码,现在让我们回顾一下。到目前为止,我们取得了什么成果?
好吧,我们找到了一个列入白名单的可序列化类,因此可以通过 .NET Remoting 反序列化访问。
我们发现,对于这个特定的类,在反序列化过程中,该类将CProxyBinaryFormatter.Deserialize第二次调用该方法,但这次启用的是黑名单模式而不是白名单模式。
最后,我们发现该ObjRef小工具之前在黑名单中缺失 - 允许在反序列化时执行代码。
综上所述,我们创建了一个桥接小工具,即binaryformatter嵌套在另一个小工具中binaryformatter。外层将满足 .NET Remoting 约束,例如低类型过滤器和白名单,然后一旦外层被反序列化,第二个二进制格式的有效负载将被 base64 解码和反序列化。这是使用带有黑名单的绑定器完成的,小工具可以利用它ObjRef。
让我们快速构建一个小工具!我们只包含了与我们的分析相关的部分,并故意省略了小工具正确运行所需的其他部分。这是为了防止脚本小子大规模利用(有关我们推理的详细信息,请参阅文章末尾的解释)。
[Serializable]
public class CDbCryptoKeyInfoWrapper : ISerializable
{
private string[] _fakeList;
public CDbCryptoKeyInfoWrapper(string[] _fakeList)
{
this._fakeList = _fakeList;
}
public void GetObjectData(SerializationInfo info)
{
info.SetType(typeof(CDbCryptoKeyInfo));
info.AddValue("Id", Guid.NewGuid());
info.AddValue("KeySetId", null);
info.AddValue("KeyType", 1);
info.AddValue("Hint", "aaaaa");
info.AddValue("DecryptedKeyValue", "AAAA");
info.AddValue("ModificationDateUtc", new DateTime());
info.AddValue("CryptoAlg", 1);
info.AddValue("RepairRecs", _fakeList);
}
}
一旦你知道如何做,这很简单!但实际上还有一个障碍需要克服。
尝试将我们闪亮的新自定义小工具部署到目标安装中失败了 - 令我们非常懊恼。我们根本无法连接到远程接口!为什么不行?!怎么回事?!为了找出答案,我们必须检查另一段 .NET 代码。
.NET 远程身份验证
连接失败的原因是 Veeam 在注册通道时明智地为其 .NET Remoting 实现使用了“安全”标志。“安全”标志强制要求进行身份验证才能访问通道。
那么这是否意味着身份验证是安全的?让我们快速了解一下 Veeam 用于验证用户身份的实现。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Tcp;
using Veeam.Backup.Common;
namespace Veeam.Common.Remoting
{
public sealed class CCliTcpChannelRegistration : IDisposable
{
public string ChannelName { get; }
private CCliTcpChannelRegistration(string channelName, CCliTcpChannelOptions options, IClientChannelSinkProvider routerSinkProvider, bool whitelistResponse)
{
this.ChannelName = channelName;
if (ChannelServices.GetChannel(channelName) != null)
{
return;
}
Log.Message("Registering TCP client channel [" + channelName + "].", Array.Empty<object>());
CBinaryClientFormatterSinkProvider cbinaryClientFormatterSinkProvider = new CBinaryClientFormatterSinkProvider(whitelistResponse);
routerSinkProvider.Next = cbinaryClientFormatterSinkProvider;
Dictionary<string, string> dictionary = new Dictionary<string, string>();
dictionary["name"] = channelName;
dictionary["tokenImpersonationLevel"] = "Impersonation";
dictionary["secure"] = "true";
dictionary["timeout"] = "900000";
Dictionary<string, string> dictionary2 = dictionary;
Log.Message("tokenImpersonationLevel: [" + dictionary2["tokenImpersonationLevel"] + "].", Array.Empty<object>());
if (options != null)
{
options.ApplyChannelProperties(dictionary2);
}
根据 .NET Remoting 文档,当设置了“安全”标志时,攻击者还可以实现自己的身份检查类。这是通过创建一个实现接口的类来实现的IAuthorizeRemotingConnection。如果没有实现这个类,攻击者就可以简单地以Anonymous Logon身份进行连接。
using System;
using System.Linq;
using System.Net;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Messaging;
using System.Security.Principal;
using Veeam.Backup.Common;
using Veeam.Backup.Logging;
namespace Veeam.Common.Remoting
{
internal sealed class CConnectionInterceptor : IAuthorizeRemotingConnection
{
public CConnectionInterceptor(IAccessCheckProvider accessChecker, Permissions minimalAllowedPermission = Permissions.BasicView)
{
this._addreses = Dns.GetHostAddresses(Dns.GetHostName());
this._accessChecker = accessChecker;
this._minimalAllowedPermission = minimalAllowedPermission;
Log.Message("CConnectionInterceptor was initialized with " + accessChecker.GetType().Name, Array.Empty<object>());
}
public bool IsConnectingEndPointAuthorized(EndPoint endPoint)
{
CallContext.LogicalSetData("ClientEndPoint", endPoint);
if (!this._accessChecker.IsEntCheckProvider)
{
return true;
}
if (this.IsLocalhostEndPoint(endPoint))
{
return true;
}
Log.Error(string.Format("CConnectionInterceptor {0} is not localhost. Access denied.", endPoint), Array.Empty<object>());
return false;
}
public bool IsConnectingIdentityAuthorized(IIdentity identity)
{
if (this._accessChecker == null)
{
Log.Error("AccessCheckProvider was not set. Access denied.", Array.Empty<object>());
return false;
}
if (!identity.IsAuthenticated)
{
Log.Error("CConnectionInterceptor " + ((identity != null) ? identity.Name : null) + " is not authenticated. Access denied.", Array.Empty<object>());
return false;
}
WindowsIdentity windowsIdentity = identity as WindowsIdentity;
if (windowsIdentity != null && windowsIdentity.IsAnonymous)
{
Log.Error("CConnectionInterceptor " + windowsIdentity.Name + " is Anonymous. Access denied.", Array.Empty<object>());
return false;
}
if (windowsIdentity != null && windowsIdentity.IsSystem)
{
Log.Message(LogLevels.HighDetailed, "CConnectionInterceptor " + windowsIdentity.Name + " is System. Access granted.", Array.Empty<object>());
return true;
}
if (this._accessChecker.IsVbrCheckProvider && this._accessChecker.VbrAccessChecker.HasAccess(identity, this._minimalAllowedPermission))
{
Log.Message(LogLevels.HighDetailed, "CConnectionInterceptor " + identity.Name + " is VBR user. Access granted.", Array.Empty<object>());
return true;
}
if (this._accessChecker.IsEntCheckProvider && this._accessChecker.EnterpriseAccessChecker.HasAccess(EnterprisePermissions.PortalView))
{
Log.Message(LogLevels.HighDetailed, "CConnectionInterceptor " + identity.Name + " is VBR user. Access granted.", Array.Empty<object>());
return true;
}
Log.Error("CConnectionInterceptor " + identity.Name + " is not a VBR user. Access denied.", Array.Empty<object>());
return false;
}
这里的一切似乎都很可靠,但我们却摸不着头脑。如何绕过这个?我们尝试了所有能想到的方法,浪费了好几个小时试图想出绕过检查的方法。那些举止可疑的人可能会认为这正是 Veeam 希望我们做的事情。
Veeam 静默修补
在本文开头,我们提到 Veeam 警告称,所有版本(包括)都12.1.2.172容易受到未经身份验证的 RCE 攻击。以下是表格,以防您忘记:
您可能还记得,我们的补丁差异(以及漏洞利用尝试)重点关注的是版本12.1.2.172,即最新的易受攻击的版本。然而,一时兴起,我们决定针对 Code White 所利用的同一版本尝试我们的漏洞利用 - 12.1.0.2131。
我们的冷酷本性很快得到了验证,令我们惊讶的是,与 .NET Remoting 对象的连接成功了,我们的小工具被反序列化,并且弹出了一个 shell!
这是怎么回事?这个错误被悄悄地修补了吗?让我们修补12.1.0.2131和之间的差异12.1.2.172。该IsConnectingIdentityAuthorized方法是什么样的?
什么?!我们差点从椅子上摔下来。尽管 Veeam 确实为授权检查创建了一个自定义类,但当被问及身份是否被授权时,它只是返回“true”。这意味着任何连接都是允许的,包括匿名的、完全未经身份验证的连接!
目前看来,修补情况比最初看上去的更加微妙。
我们看到的实际上是两个独立错误的影响——一个是反序列化错误(ObjRef从黑名单中省略)和一个不当授权错误(允许匿名连接IsConnectingIdentityAuthorized)。
有趣的是,Veeam 似乎在两个不同的版本中修补了这两个不同的错误(尽管 Code White 同时通知了他们这两个错误)。
一开始,它只是一个简单的12.1.0.2131。它包含一个未经身份验证的远程代码执行漏洞 CVE-2024-40711,由两个独立的错误组成。
随后,Veeam 修补了不当授权组件,并发布了12.1.2.172。此举可防止匿名利用,将 CVE-2024-40711 降级为仅需身份验证的漏洞。
然后,三个月后,他们修补了反序列化错误,创建了12.2.0.334。这完全修复了 CVE-2024-40711,防止了漏洞利用(剧透:实际上并没有,但这是下一篇博客文章的主题,因为细节仍处于保密状态)。
至于 Veeam 为什么选择修补两次,谁也说不准——也许对两个不同组件的修改需要不同的 QA 流程,而当只有一个修复程序准备好投入生产时,Veeam 会尽早发布版本,以便尽快保护他们的客户。
不过,也许——准备好面对某种阴谋论吧——也许Veeam 发布了两个不同的修复漏洞,试图淡化这个漏洞。
也许Veeam 首先“悄无声息”地修复了不当授权组件,而没有发布任何安全公告。这会导致将“真正的”错误(反序列化)降级为需要身份验证的漏洞,这意味着当他们稍后修补它时,他们可以宣布一个只有 CVSS 9.8 漏洞而不是 10.0 漏洞的大漏洞 - 您会记得,他们的公告正是这样做的。
不过,这种理论应该谨慎对待,因为 Veeam 的公告还指出 CVE-2024-40711 不需要身份验证,因此属于未经身份验证的 RCE。也许 CVSS 评分为 9.8(而不是 10.0)只是由于急于修复和发布时间表一致而造成的意外(或者可能只是汉隆剃刀的一个很好的例子)。
这个由两部分组成的交错补丁最终产生了 CVE-2024-40711,该漏洞在一个版本中得到了一定程度的修复。运行该补丁的版本12.1.2.172会受到经过身份验证的 RCE 攻击,而运行该补丁的版本12.1.1.56及以下版本则会受到整个未经身份验证的漏洞链攻击。
以下是 Veeam 公告中发布漏洞状态表的注释版本:
结论和总结
好吧,这是一个复杂的漏洞,需要阅读大量代码!我们已成功展示如何将多个错误串联在一起以在各种版本的 Veeam Backup & Replication 中获得 RCE。
然而,Veeam 的建议似乎自相矛盾,这让我们有些困惑。您可能还记得,从这篇博文一开始,Veeam 的建议就是 10.10.1 及以下版本都12.1.2.172存在漏洞。虽然漏洞标题为“允许未经身份验证的远程代码执行 (RCE) 的漏洞”,暗示这是一个世界末日的 CVSS 10 漏洞,但他们随后将该漏洞标记为不太严重的 CVSS 9.8,需要用户身份验证才能利用。这令人困惑,因为低于 10.10.10.10 的所有版本12.1.2.172都不需要身份验证即可利用,只有在 10.10.1 中进行了更改才12.1.2.172需要身份验证(参见上述分析)。
也许 Veeam 只是在他们的公告中犯了一个错误,因为我们(和 Code White)清楚地表明不需要身份验证。希望没有做出预先更改来12.1.2.172降低此漏洞的最终严重性。
不管 CVSS 如何,实际情况,正如你上面看到的,比‘之前的 RCE’ 更加微妙12.1.2.172:
版本
12.2.0 .334已完全修复。不受本博文中漏洞的影响。
12.1.2 .172受影响,但利用该漏洞需要身份验证。低权限用户可执行任意代码。
12.1.1 .56 及更早版本容易受到未经身份验证的 RCE 攻击。
说到漏洞利用,我们打破了这一漏洞的传统,没有发布完整的漏洞利用链(抱歉,各位!)。我们有点担心这一漏洞对恶意软件操作员的价值,因此(仅在这种情况下)没有发布有效的漏洞利用。我们最多会发布这个诱人的漏洞利用视频,这得等到我们下一篇文章时才能看完:
原文始发于微信公众号(Ots安全):Veeam Backup - 需要身份验证的 RCE,但大多数情况下无需身份验证 (CVE-2024-40711)
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论