作者:HuanGMz@知道创宇404实验室
.NET 相关漏洞中,ViewState也算是一个常客了。Exchange CVE-2020-0688,SharePoint CVE-2020-16952 中都出现过ViewState的身影。其实ViewState 并不算漏洞,只是ASP.NET 在生成和解析ViewState时使用ObjectStateFormatter 进行序列化和反序列化,虽然在序列化后又进行了加密和签名,但是一旦泄露了加密和签名所使用的算法和密钥,我们就可以将ObjectStateFormatter 的反序列化payload 伪装成正常的ViewState,并触发ObjectStateFormatter 的反序列化漏洞。
加密和签名序列化数据所用的算法和密钥存放在web.confg 中,Exchange 0688 是由于所有安装采用相同的默认密钥,而Sharepoitn 16952 则是因为泄露web.confg 。
.NET 反序列化神器 ysoserial.net 中有关于ViewState 的插件,其主要作用就是利用泄露的算法和密钥伪造ViewState的加密和签名,触发ObjectStateFormatter 反序列化漏洞。但是我们不应该仅仅满足于工具的使用,所以特意分析了ViewState 的加密和签名过程作成此文,把工具用的明明白白的。
初接触.NET,文中谬误纰漏之处在所难免,如蒙指教不胜感激。
1. 调试.Net FrameWork
1.1 .Net 源码
对于刚接触.Net反序列化,甚至刚接触C#的朋友来说,有一个舒适方便的调试环境实在是太重要了。这里就简单介绍一下如何进行.net framework 的底层调试。
.Net Framework 已经被微软开源了,你可以在官方网站上下载源码或者直接在线浏览。目前开源的版本包括 .Net 4.5.1 到 4.8。但是要注意,虽然微软开源了.Net 的源码,以及相应的VS项目文件,但是只能用于代码浏览,而无法进行编译。因为缺少重要组件(包括xaml文件和资源文件)。
1.2 调试
微软官文档有说明如何使用VS进行.Net源码的调试。其原理大概是通过pdb+源码的方式来进行单步调试。但经过实际尝试,发现并不是所有.net 程序集文件都有完整的pdb文件,其中一部分程序集的pdb是没有源码信息的。也就是说,只有一部分的程序集可以通过vs进行单步调试。
细节参考以下连接:https://referencesource.microsoft.com/setup.html
支持源码调试的程序集列表为:https://referencesource.microsoft.com/indexedpdbs.txt
在放弃使用vs进行调试后,我发现还可以使用dnspy 进行.net底层调试。dnspy 是一个开源的.Net反编译工具,与经典工具Reflector相比,它不仅可以用于反编译,还可以借助反编译直接进行调试。dnspy 的github链接在这里。可以下载源码进行编译,也可以直接下载编译好的版本,不过要注意满足它要求的.net framework 版本。
设置环境变量 COMPLUS_ZapDisable=1
为什么要设置这个环境变量,为了禁用所有NGEN映像(* .ni.dll)的使用。
假如你的windows服务器上安装有IIS服务,并且上面运行一个网站。使用浏览器打开该网站,这会使IIS在后台创建一个工作进程,用于运行该网站。这时我们用 process explore去查看 w3wp.exe 进程加载的dll,你会发现为什么程序集后面都有一个.ni的后缀。System.Web.dll 变为了 System.Web.ni.dll ,并且该dll的描述中还特意写了 “System.Web.dll”。其实这就是在使用.Net的优化版代码。
设置环境变量 COMPLUS_ZapDisable=1 ,重启windows(一定要重启,因为重启IIS服务才能应用到我们设置的新环境变量)。仍然用ie打开网站,然后使用Process explore去查看w3wp.exe,这时你就会发现:网站工作进程加载的程序集变回了我们所熟知的System.Web.dll。
注意1:设置环境变量后要重启
注意2:如果找不到w3wp.exe,使用管理员运行process explore。
使用dnspy 进行调试
首先我们用process explore检查w3wp.exe
加载的程序集所在的位置。因为你的系统上可能安装有多个版本的.Net或者是不同位数的.Net。如果你在dnsPy 中打开了错误的程序集,你在上面下断点的时候会提示你:无法中断到该断点,因为没有加载该模块。
选择32位或者64位的 dnspy(与被调试进程匹配),以管理员权限启动。随便找一个程序集,比如System.Web.dll,点开后我们看他第一行中所写的路径是否与目标进程加载的程序集相同:
如果不相同,左上方 文件->全部关闭,然后 文件->打开列表,从中选择一个版本合适的 .Net 。
然后上方 调试->附加到进程,选择w3wp.exe
,如果有多个进程,我们可以通过进程号来确定。那么如何判断哪一个进程是我们需要的呢?方法有很多种,你可以通过 process explore 查看w3wp.exe
的启动命令,看哪个是运行目标网站的工作进程。又或者,以管理员权限启动cmd,进入 C:\Windows\System32\inetsrv,然后运行appcmd list wp。
我们可以看到进程号和对应的网站集名称。
然后就是给目标函数下断点,刷新页面,会中断到断点。
2. ViewState基础知识
在我们尝试利用ViewState反序列化之前,我们需要一些了解相关的知识。
ASP.NET是由微软在.NET Framework框架中所提供,开发Web应用程序的类别库,封装在System.Web.dll文件中,显露出System.Web名字空间,并提供ASP.NET网页处理、扩展以及HTTP通道的应用程序与通信处理等工作,以及Web Service的基础架构。
也就是说,ASP.NET 是.NET Framework 框架提供的一个Web库,而ViewState则是ASP.NET所提供的一个极具特点的功能。
出现ViewState的原因:
HTTP模型是无状态的,这意味着,每当客户端向服务端发起一个获取页面的请求时,都会导致服务端创建一个新的page类的实例,并且一个往返之后,这个page实例会被立刻销毁。假如服务端在处理第n+1次请求时,想使用第n次传给服务器的值进行计算,而这时第n次请求所对应的page实例早已被销毁,要去哪里找上一次传给服务器的值呢?为了满足这种需求,就出现了多种状态管理技术,而VewState正是ASP.NET 所采用的状态管理技术之一。
ViewState是什么样的?
要了解ViewState,我们要先知道什么叫做服务器控件。
ASP.NET 网页在微软的官方名称中,称为 Web Form,除了是要和Windows Forms作分别以外,同时也明白的刻划出了它的主要功能:“让开发人员能够像开发 Windows Forms 一样的方法来发展 Web 网页”。因此 ASP.NET Page 所要提供的功能就需要类似 Windows Forms 的窗体,每个 Web Form 都要有一个< form runat=”server” >< /form >区块,所有的 ASP.NET 服务器控件都要放在这个区域中,这样才可以让 ViewState 等服务器控制能够顺畅的运作。
无论是HTML服务器控件、Web服务器控件 还是 Validation服务器控件,只要是ASP.NET 的服务器控件,都要放在< form runat=”server” >< /form >的区块中,其中的属性 runat=”server” 表明了该表单应该在服务端进行处理。
ViewState原始状态是一个 字典类型。在响应一个页面时,ASP.NET 会把所有控件的状态序列化为一个字符串,然后作为 hidden input 的值 插入到页面中返还给客户端。当客户端再次请求时,该hidden input 就会将ViewState传给服务端,服务端对ViewState进行反序列化,获得属性,并赋给控件对应的值。
ViewState的安全性:
在2010年的时候,微软曾在《MSDN杂志》上发过一篇文章,讨论ViewState的安全性以及一些防御措施。文章中认为ViewState主要面临两个威胁:信息泄露和篡改。
信息泄露威胁:
原始的ViewState仅仅是用base64编码了序列化后的binary数据,未使用任何类型的密码学算法进行加密,可以使用LosFormatter(现在已经被ObjectStateFormatter替代)轻松解码和反序列化。
LosFormatter formatter = new LosFormatter();
object viewstateObj = formatter.Deserialize("/wEPDwULLTE2MTY2ODcyMjkPFgIeCHBhc3N3b3JkBQlzd29yZGZpc2hkZA==");
反序列化的结果实际上是一组System.Web.UI.Pair对象。
为了保证ViewState不会发生信息泄露,ASP.NEt 2.0 使用 ViewStateEncryptionMode属性 来启用ViewState的加密,该属性可以通过页面指令或在应用程序的web.config 文件中启用。
<%@ Page ViewStateEncryptionMode="Always" %>
ViewStateEncryptionMode 可选值有三个:Always、Never、Auto
篡改威胁:
加密不能防止篡改 ,即使使用加密数据,攻击者仍然有可能翻转加密书中的位。所以要使用数据完整性技术来减轻篡改威胁,即使用哈希算法来为消息创建身份验证代码(MAC)。可以在web.config 中通过EvableViewStateMac来启用数据校验功能。
<%@ Page EnableViewStateMac="true" %>
注意:从.NET 4.5.2 开始,强制启用ViewStateMac 功能,也就是说即使你将 EnableViewStateMac设置为false,也不能禁止ViewState的校验。安全公告KB2905247(于2014年9月星期二通过补丁程序发送到所有Windows计算机)将ASP.NET 设置为忽略EbableViewStateMac设置。
启用ViewStateMac后的大致步骤:
(1)页面和所有参与控件的状态被收集到状态图对象中。
(2)状态图被序列化为二进制格式
a. 密钥值将附加到序列化的字节数组中。
b. 为新的序列化字节数组计算一个密码哈希。
c. 哈希将附加到序列化字节数组的末尾。(3) 序列化的字节数组被编码为base-64字符串。
(4)base-64字符串将写入页面中的__VIEWSTATE表单值。
利用ViewState 进行反序列化利用
其实ViewState 真正的问题在与其潜在的反序列化漏洞风险。ViewState 使用ObjectStateFormatter 进行反序列化,虽然ViewState 采取了加密和签名的安全措施。但是一旦泄露web.config,获取其加密和签名所用的密钥和算法,我们就可以将ObjectStateFormatte 的反序列化payload 进行同样的加密与签名,然后再发给服务器。这样ASP.NET在进行反序列化时,正常解密和校验,然后把payload交给ObjectStateFormatter 进行反序列化,触发其反序列化漏洞,实现RCE。
3. web.config 中关于ViewState 的配置
ASP.NET 通过web.config 来完成对网站的配置。
在web.config 可以使用以下的参数来开启或关闭ViewState的一些功能:
<pages enableViewState="false" enableViewStateMac="false" viewStateEncryptionMode="Always" />
enableViewState: 用于设置是否开启viewState,但是请注意,根据 安全通告KB2905247 中所说,即使在web.config中将enableViewState 设置为false,ASP.NET服务器也始终被动解析 ViewState。也就是说,该选项可以影响ViewState的生成,但是不影响ViewState的被动解析。实际上,viewStateEncryptionMode也有类似的特点。
enableViewStateMac: 用于设置是否开启ViewState Mac (校验)功能。在 安全通告KB2905247 之前,也就是4.5.2之前,该选项为false,可以禁止Mac校验功能。但是在4.5.2之后,强制开启ViewState Mac 校验功能,因为禁用该选项会带来严重的安全问题。不过我们仍然可以通过配置注册表或者在web.config 里添加危险设置的方式来禁用Mac校验,详情见后面分析。
viewStateEncryptionMode: 用于设置是否开启ViewState Encrypt (加密)功能。该选项的值有三种选择:Always、Auto、Never。
- Always表示ViewState始终加密;
- Auto表示 如果控件通过调用 RegisterRequiresViewStateEncryption() 方法请求加密,则视图状态信息将被加密,这是默认值;
- Never表示 即使控件请求了视图状态信息,也永远不会对其进行加密。
在实际调试中发现,viewStateEncryptionMode 影响的是ViewState的生成,但是在解析从客户端提交的ViewState时,并不是依据此配置来判断是否要解密。详情见后面分析。
在web.config 中通过machineKey节 来对校验功能和加密功能进行进一步配置:
<machineKey validationKey="[String]" decryptionKey="[String]" validation="[SHA1 | MD5 | 3DES | AES | HMACSHA256 | HMACSHA384 | HMACSHA512 | alg:algorithm_name]" decryption="[Auto | DES | 3DES | AES | alg:algorithm_name]" />
例子:
<machineKey validationKey="BF579EF0E9F0C85277E75726BFC9D0260FADE8DE2864A583484AA132944F602D" decryptionKey="51FE611365277B07911521B7CAFE3766751D16C33D96242F0E63E93FB102BCE2" validation="HMACSHA256" />
其中的validationKey 和 decryptionKey 分别是校验和加密所用的密钥,validation和decryption则是校验和加密所使用的算法(可以省略,采用默认算法)。校验算法包括: SHA1、 MD5、 3DES、 AE、 HMACSHA256、 HMACSHA384、 HMACSHA512。加密算法包括:DES、3DES、AES。 由于web.config 保存在服务端上,在不泄露machineKey的情况下,保证了ViewState的安全性。
了解了一些关于ViewState的配置后,我们再来看一下.NET Framework 到底是如何处理ViewState的生成与解析的。
4. ViewState的生成和解析流程
根据一些先验知识,我们知道ViewState 是通过ObjectStateFormatter的Serialize和Deserialize 来完成ViewState的序列化和反序列化工作。(LosFormatter 也用于ViewState的序列化,但是目前其已被ObjectStateFormatter替代。LosFormatter的Serialize 是直接调用的ObjectStateFormatter 的Serialize)
ObjectStateFormatter 位于System.Web.UI 空间,我们给他的 Serialize函数下个断点(重载有多个Serialize函数,注意区分)。使用dnspy 调试,中断后查看栈回溯信息:
通过栈回溯,我们可以清晰的看到Page类通过调用 SaveAllState 进入到ObjectStateFormatter的 Seralize 函数。
4.1 Serialize 流程
查看Serialize 函数的代码(这里我使用.Net 4.8 的源码,有注释,更清晰):
private string Serialize(object stateGraph, Purpose purpose) {
string result = null;
MemoryStream ms = GetMemoryStream();
try {
Serialize(ms, stateGraph);
ms.SetLength(ms.Position);
byte[] buffer = ms.GetBuffer();
int length = (int)ms.Length;
#if !FEATURE_PAL // FEATURE_PAL does not enable cryptography
// We only support serialization of encrypted or encoded data through our internal Page constructors
if (AspNetCryptoServiceProvider.Instance.IsDefaultProvider && !_forceLegacyCryptography) {
// If we're configured to use the new crypto providers, call into them if encryption or signing (or both) is requested.
...
}
else {
// Otherwise go through legacy crypto mechanisms
#pragma warning disable 618 // calling obsolete methods
if (_page != null && _page.RequiresViewStateEncryptionInternal) {
buffer = MachineKeySection.EncryptOrDecryptData(true, buffer, GetMacKeyModifier(), 0, length);
length = buffer.Length;
}
// We need to encode if the page has EnableViewStateMac or we got passed in some mac key string
else if ((_page != null && _page.EnableViewStateMac) || _macKeyBytes != null) {
buffer = MachineKeySection.GetEncodedData(buffer, GetMacKeyModifier(), 0, ref length);
}
#pragma warning restore 618 // calling obsolete methods
}
#endif // !FEATURE_PAL
result = Convert.ToBase64String(buffer, 0, length);
}
finally {
ReleaseMemoryStream(ms);
}
return result;
}
在函数开头处,调用了另一个重载的Serialzie函数,作用是将stateGraph 序列化为binary数据:
MemoryStream ms = GetMemoryStream();
try {
Serialize(ms, stateGraph);
ms.SetLength(ms.Position);
...
之后进入else分支:
if (_page != null && _page.RequiresViewStateEncryptionInternal) {
buffer = MachineKeySection.EncryptOrDecryptData(true, buffer, GetMacKeyModifier(), 0, length);
length = buffer.Length;
}
// We need to encode if the page has EnableViewStateMac or we got passed in some mac key string
else if ((_page != null && _page.EnableViewStateMac) || _macKeyBytes != null) {
buffer = MachineKeySection.GetEncodedData(buffer, GetMacKeyModifier(), 0, ref length);
}
这里有两个重要标志位, _page.RequiresViewStateEncryptionInternal 和 _page.EnableViewStateMac。这两个标志位决定了序列化的Binary数据 是进入 MachineKeySection.EncryptOrDecryptData()函数还是 MachineKeySection.GetEncodedData()函数。
其中EncryptOrDecryptData() 函数用于加密以及可选择的进行签名(校验),而GetEncodedData() 则只用于签名(校验)。稍后我们再具体分析这两个函数,我们先来研究一下这两个标志位。
这两个标志位决定了服务端产生的ViewState采取了什么安全措施。这与之前所描述的web.config 中的EnableViewStateMac 和 viewStateEncryptionMode的作用一致。
_page.RequiresViewStateEncryptionInternal 来自这里:
internal bool RequiresViewStateEncryptionInternal {
get {
return ViewStateEncryptionMode == ViewStateEncryptionMode.Always ||
_viewStateEncryptionRequested && ViewStateEncryptionMode == ViewStateEncryptionMode.Auto;
}
}
其中的ViewStateEncryptionMode 应当是直接来自web.config。所以是否进入 MachineKeySection.EncryptOrDecryptData 取决于web.config 里的配置。(注意,进入该函数不仅会进行加密,也会进行签名)。
_page.EnableViewStateMac 来自这里:
public bool EnableViewStateMac {
get { return _enableViewStateMac; }
set {
// DevDiv #461378: EnableViewStateMac=false can lead to remote code execution, so we
// have an mechanism that forces this to keep its default value of 'true'. We only
// allow actually setting the value if this enforcement mechanism is inactive.
if (!EnableViewStateMacRegistryHelper.EnforceViewStateMac) {
_enableViewStateMac = value;
}
}
}
对应字段 _enableViewStateMac 在Page类的初始化函数中被设置为默认值 true:
public Page() {
_page = this; // Set the page to ourselves
_enableViewStateMac = EnableViewStateMacDefault;
...
}
于是 _enableViewStateMac 是否被修改就取决于 EnableViewStateMacRegistryHelper.EnforceViewStateMac。
查看 EnableViewStateMacRegistryHelper 类,其为EnforceViewStateMac 做了如下注释:
// Returns 'true' if the EnableViewStateMac patch (DevDiv #461378) is enabled,
// meaning that we always enforce EnableViewStateMac=true. Returns 'false' if
// the patch hasn't been activated on this machine.
public static readonly bool EnforceViewStateMac;
也就是说:在启用EnableViewStateMac补丁的情况下,EnforceViewStateMac 返回true,这表示 前面的EnableViewStateMac 标志位会始终保持其默认值true。
在EnableViewStateMacRegistryHelper 类的初始化函数中,进一步表明了是依据什么修改 EnforceViewStateMac的:
static EnableViewStateMacRegistryHelper() {
// If the reg key is applied, change the default values.
bool regKeyIsActive = IsMacEnforcementEnabledViaRegistry();
if (regKeyIsActive) {
EnforceViewStateMac = true;
SuppressMacValidationErrorsFromCrossPagePostbacks = true;
}
// Override the defaults with what the developer specified.
if (AppSettings.AllowInsecureDeserialization.HasValue) {
EnforceViewStateMac = !AppSettings.AllowInsecureDeserialization.Value;
// Exception: MAC errors from cross-page postbacks should be suppressed
// if either the <appSettings> switch is set or the reg key is set.
SuppressMacValidationErrorsFromCrossPagePostbacks |= !AppSettings.AllowInsecureDeserialization.Value;
}
...
可以看到EnforceViewStateMac 在两种情况下被修改:
- 依据 IsMacEnforcementEnabledViaRegistry() 函数该函数是从注册表里取值,如果该表项为0,则表示禁用EnableViewStateMac 补丁。
private static bool IsMacEnforcementEnabledViaRegistry() {
try {
string keyName = String.Format(CultureInfo.InvariantCulture, @"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\.NETFramework\v{0}", Environment.Version.ToString(3));
int rawValue = (int)Registry.GetValue(keyName, "AspNetEnforceViewStateMac", defaultValue: 0 /* disabled by default */);
return (rawValue != 0);
}
catch {
// If we cannot read the registry for any reason, fail safe and assume enforcement is enabled.
return true;
}
}
- 依据 AppSettings.AllowInsecureDeserialization.HasValue该值应当是来自于web.config 中的危险设置:
<configuration>
…
<appSettings>
<add key="aspnet:AllowInsecureDeserialization" value="true" />
</appSettings>
</configuration>
总结来说,ViewStateMac 默认强制开启,要想关闭该功能,必须通过注册表或者在web.config 里进行危险设置的方式禁用 EnableViewStateMac 补丁才能实现。
4.2 Deserialize 流程
查看 Deserialize 函数的代码:
private object Deserialize(string inputString, Purpose purpose) {
if (String.IsNullOrEmpty(inputString)) {
throw new ArgumentNullException("inputString");
}
byte[] inputBytes = Convert.FromBase64String(inputString);
int length = inputBytes.Length;
#if !FEATURE_PAL // FEATURE_PAL does not enable cryptography
try {
if (AspNetCryptoServiceProvider.Instance.IsDefaultProvider && !_forceLegacyCryptography) {
// If we're configured to use the new crypto providers, call into them if encryption or signing (or both) is requested.
...
}
else {
// Otherwise go through legacy crypto mechanisms
#pragma warning disable 618 // calling obsolete methods
if (_page != null && _page.ContainsEncryptedViewState) {
inputBytes = MachineKeySection.EncryptOrDecryptData(false, inputBytes, GetMacKeyModifier(), 0, length);
length = inputBytes.Length;
}
// We need to decode if the page has EnableViewStateMac or we got passed in some mac key string
else if ((_page != null && _page.EnableViewStateMac) || _macKeyBytes != null) {
inputBytes = MachineKeySection.GetDecodedData(inputBytes, GetMacKeyModifier(), 0, length, ref length);
}
#pragma warning restore 618 // calling obsolete methods
}
}
catch {
// MSRC 10405: Don't propagate inner exceptions, as they may contain sensitive cryptographic information.
PerfCounters.IncrementCounter(AppPerfCounter.VIEWSTATE_MAC_FAIL);
ViewStateException.ThrowMacValidationError(null, inputString);
}
#endif // !FEATURE_PAL
object result = null;
MemoryStream objectStream = GetMemoryStream();
try {
objectStream.Write(inputBytes, 0, length);
objectStream.Position = 0;
result = Deserialize(objectStream);
}
finally {
ReleaseMemoryStream(objectStream);
}
return result;
}
重点仍然是里面的else分支:
else {
// Otherwise go through legacy crypto mechanisms
if (_page != null && _page.ContainsEncryptedViewState) {
inputBytes = MachineKeySection.EncryptOrDecryptData(false, inputBytes, GetMacKeyModifier(), 0, length);
length = inputBytes.Length;
}
// We need to decode if the page has EnableViewStateMac or we got passed in some mac key string
else if ((_page != null && _page.EnableViewStateMac) || _macKeyBytes != null) {
inputBytes = MachineKeySection.GetDecodedData(inputBytes, GetMacKeyModifier(), 0, length, ref length);
}
}
这里出现了一个新的标志位 _page.ContainsEncryptedViewState 用于决定是否进入MachineKeySection.EncryptOrDecryptData() 函数进行解密,查看ContainsEncryptedViewState 的来历:
if (_requestValueCollection != null) {
// Determine if viewstate was encrypted.
if (_requestValueCollection[ViewStateEncryptionID] != null) {
ContainsEncryptedViewState = true;
}
...
注释表明,该标志确实用于判断接收到的viewstate 是否被加密。查看dnspy逆向的结果,你会更清晰:
这 “__VIEWSTATEENCRYPTED” 很像是request 里提交的字段啊,查找一下,确实如此。
查看开启加密后的 request 请求,确实有这样一个无值的字段:
所以,ASP.NET在解析ViewState时,并不是根据web.config来判断 ViewState 是否加密,而是通过request里是否有__VIEWSTATEENCRYPTED 字段进行判断。换句话说,即使我们在web.config 里设置 Always 解密,服务端仍然会被动解析只有签名的ViewState。( 我在 YsoSerial.NET 工具 ViewState插件作者的博客里看到,.net 4.5 之后需要加密算法和密钥。但是我不明白为什么,在实际测试中似乎也不需要。)
5. GetEncodedData 签名函数
GetEncodedData() 函数用于对序列化后的Binary数据进行签名,用于完整性校验。查看其代码(.NET 4.8):
// NOTE: When encoding the data, this method *may* return the same reference to the input "buf" parameter
// with the hash appended in the end if there's enough space. The "length" parameter would also be
// appropriately adjusted in those cases. This is an optimization to prevent unnecessary copying of
// buffers.
[Obsolete(OBSOLETE_CRYPTO_API_MESSAGE)]
internal static byte[] GetEncodedData(byte[] buf, byte[] modifier, int start, ref int length)
{
EnsureConfig();
byte[] bHash = HashData(buf, modifier, start, length);
byte[] returnBuffer;
if (buf.Length - start - length >= bHash.Length)
{
// Append hash to end of buffer if there's space
Buffer.BlockCopy(bHash, 0, buf, start + length, bHash.Length);
returnBuffer = buf;
}
else
{
returnBuffer = new byte[length + bHash.Length];
Buffer.BlockCopy(buf, start, returnBuffer, 0, length);
Buffer.BlockCopy(bHash, 0, returnBuffer, length, bHash.Length);
start = 0;
}
length += bHash.Length;
if (s_config.Validation == MachineKeyValidation.TripleDES || s_config.Validation == MachineKeyValidation.AES) {
returnBuffer = EncryptOrDecryptData(true, returnBuffer, modifier, start, length, true);
length = returnBuffer.Length;
}
return returnBuffer;
}
大致流程:
- HashData()函数计算出hash值。
- 判断原buffer长度是否够,如果够,则直接在原buffer中data后添加hash值;否则申请新的buf,并将data和hash值拷贝过去。
- 判断hash算法是否是3DES 或者 AES,如果是,则调用EncryptOrDecryptData() 函数。
我们首先来看一下HashData函数:
internal static byte[] HashData(byte[] buf, byte[] modifier, int start, int length)
{
EnsureConfig();
if (s_config.Validation == MachineKeyValidation.MD5)
return HashDataUsingNonKeyedAlgorithm(null, buf, modifier, start, length, s_validationKey);
if (_UseHMACSHA) {
byte [] hash = GetHMACSHA1Hash(buf, modifier, start, length);
if (hash != null)
return hash;
}
if (_CustomValidationTypeIsKeyed) {
return HashDataUsingKeyedAlgorithm(KeyedHashAlgorithm.Create(_CustomValidationName),
buf, modifier, start, length, s_validationKey);
} else {
return HashDataUsingNonKeyedAlgorithm(HashAlgorithm.Create(_CustomValidationName),
buf, modifier, start, length, s_validationKey);
}
}
这里有几个特殊的标志位:s_config.Validation、_UseHMACSHA、_CustomValidationTypeIsKeyed,用来决定进入哪个函数生成hash。
s_config.Validation 应当是web.config 中设置的签名算法。
而另外两个标志则源自于 InitValidationAndEncyptionSizes() 函数里根据签名算法进行的初始化设置:
private void InitValidationAndEncyptionSizes()
{
_CustomValidationName = ValidationAlgorithm;
_CustomValidationTypeIsKeyed = true;
switch(ValidationAlgorithm)
{
case "AES":
case "3DES":
_UseHMACSHA = true;
_HashSize = SHA1_HASH_SIZE;
_AutoGenValidationKeySize = SHA1_KEY_SIZE;
break;
case "SHA1":
_UseHMACSHA = true;
_HashSize = SHA1_HASH_SIZE;
_AutoGenValidationKeySize = SHA1_KEY_SIZE;
break;
case "MD5":
_CustomValidationTypeIsKeyed = false;
_UseHMACSHA = false;
_HashSize = MD5_HASH_SIZE;
_AutoGenValidationKeySize = MD5_KEY_SIZE;
break;
case "HMACSHA256":
_UseHMACSHA = true;
_HashSize = HMACSHA256_HASH_SIZE;
_AutoGenValidationKeySize = HMACSHA256_KEY_SIZE;
break;
case "HMACSHA384":
_UseHMACSHA = true;
_HashSize = HMACSHA384_HASH_SIZE;
_AutoGenValidationKeySize = HMACSHA384_KEY_SIZE;
break;
case "HMACSHA512":
_UseHMACSHA = true;
_HashSize = HMACSHA512_HASH_SIZE;
_AutoGenValidationKeySize = HMACSHA512_KEY_SIZE;
break;
default:
...
可以看到,只有MD5签名算法将 _UseHMASHA设置为false,其他算法都将其设置为true。除此之外,还根据签名算法设置_HashSize 为相应hash长度。所以计算MD5 hahs时进入 HashDataUsingNonKeyedAlgorithm()函数,计算其他算法hash时进入 GetHMACSHA1Hash() 函数。
我们先看使用MD5签名算法时进入的 HashDataUsingNonKeyedAlgorithm() 函数:
private static byte[] HashDataUsingNonKeyedAlgorithm(HashAlgorithm hashAlgo, byte[] buf, byte[] modifier,
int start, int length, byte[] validationKey)
{
int totalLength = length + validationKey.Length + ((modifier != null) ? modifier.Length : 0);
byte [] bAll = new byte[totalLength];
Buffer.BlockCopy(buf, start, bAll, 0, length);
if (modifier != null) {
Buffer.BlockCopy(modifier, 0, bAll, length, modifier.Length);
}
Buffer.BlockCopy(validationKey, 0, bAll, length, validationKey.Length);
if (hashAlgo != null) {
return hashAlgo.ComputeHash(bAll);
} else {
byte[] newHash = new byte[MD5_HASH_SIZE];
int hr = UnsafeNativeMethods.GetSHA1Hash(bAll, bAll.Length, newHash, newHash.Length);
Marshal.ThrowExceptionForHR(hr);
return newHash;
}
}
这里的modifier 的来源我们稍后再议,其长度一般为4个字节。HashDataUsingNonKeyedAlgorithm() 函数流程如下:
- 申请一块新的内存,其长度为data length + validationkey.length + modifier.length
- 将data,modifier,validationkey 拷贝到新分配的内存里。特殊的是,modifier 和 vavlidationkey 都是从紧挨着data的地方开始拷贝,这就导致了validationkey 会 覆盖掉modifier。所以真正的内存分配为: data + validationkey + ‘\x00’*modifier.length
- 根据MD5算法设置hash长度,即newHash。关于这一点,代码中有各种算法产生hash值的长度设定:
private const int MD5_KEY_SIZE = 64;
private const int MD5_HASH_SIZE = 16;
private const int SHA1_KEY_SIZE = 64;
private const int HMACSHA256_KEY_SIZE = 64;
private const int HMACSHA384_KEY_SIZE = 128;
private const int HMACSHA512_KEY_SIZE = 128;
private const int SHA1_HASH_SIZE = 20;
private const int HMACSHA256_HASH_SIZE = 32;
private const int HMACSHA384_HASH_SIZE = 48;
private const int HMACSHA512_HASH_SIZE = 64;
各种算法对应的Hash长度分别为 MD5:16 SHA1:20 MACSHA256:32 HMACSHA384:48 HMACSHA512:64, 全都不同。
- 调用UnsafeNativeMethods.GetSHA1Hash() 函数进行hash计算。该函数是从webengine4.dll 里导入的一个函数。第一次看到这里,我有一些疑问,为什么MD5算法要调用GetSHA1Hash函数呢?这个疑问先保留。我们先看其他算法是如何生成hash的。
计算其他算法的hash时调用了一个自己写的GetHMACSHA1Hash() 函数,其实现如下:
private static byte[] GetHMACSHA1Hash(byte[] buf, byte[] modifier, int start, int length) {
if (start < 0 || start > buf.Length)
throw new ArgumentException(SR.GetString(SR.InvalidArgumentValue, "start"));
if (length < 0 || buf == null || (start + length) > buf.Length)
throw new ArgumentException(SR.GetString(SR.InvalidArgumentValue, "length"));
byte[] hash = new byte[_HashSize];
int hr = UnsafeNativeMethods.GetHMACSHA1Hash(buf, start, length,
modifier, (modifier == null) ? 0 : modifier.Length,
s_inner, s_inner.Length, s_outer, s_outer.Length,
hash, hash.Length);
if (hr == 0)
return hash;
_UseHMACSHA = false;
return null;
}
可以看到,其内部直接调用的UnsafeNativeMethods.GetHMACSHA1Hash() 函数,该函数也是从webengine4.dll里导入的一个函数。和之前看生成MD5 hash值时有一样的疑问,为什么是GetHMACSHA1HAsh?为什么多种算法都进入这一个函数?根据他们参数的特点,而且之前看到各个算法生成hash的长度不同,我们可以猜测,或许是该函数内部根据hash长度来选择使用什么算法。
把 webengine4.dll 拖进ida里。查看GetSHA1Hash() 函数和 GetHMACSHA1Hash() 函数,特点如下:
GetHMACSHA1Hash:
二者都进入了GetAlgorithmBasedOnHashSize() 函数,看来我们的猜测没错,确实是通过hash长度来选择算法。
6. EncryptOrDecryptData 加密解密函数
我们之前看到,无论是开启加密的情况下,还是采用AES\3DES签名算法的情况下,都会进入 MachineKeySection.EncryptOrDecryptData() 函数,那么该函数内部是怎么样的流程呢?
先来看一下该函数的声明和注释:
internal static byte[] EncryptOrDecryptData(bool fEncrypt, byte[] buf, byte[] modifier, int start, int length, bool useValidationSymAlgo, bool useLegacyMode, IVType ivType, bool signData)
/* This algorithm is used to perform encryption or decryption of a buffer, along with optional signing (for encryption)
* or signature verification (for decryption). Possible operation modes are:
*
* ENCRYPT + SIGN DATA (fEncrypt = true, signData = true)
* Input: buf represents plaintext to encrypt, modifier represents data to be appended to buf (but isn't part of the plaintext itself)
* Output: E(iv + buf + modifier) + HMAC(E(iv + buf + modifier))
*
* ONLY ENCRYPT DATA (fEncrypt = true, signData = false)
* Input: buf represents plaintext to encrypt, modifier represents data to be appended to buf (but isn't part of the plaintext itself)
* Output: E(iv + buf + modifier)
*
* VERIFY + DECRYPT DATA (fEncrypt = false, signData = true)
* Input: buf represents ciphertext to decrypt, modifier represents data to be removed from the end of the plaintext (since it's not really plaintext data)
* Input (buf): E(iv + m + modifier) + HMAC(E(iv + m + modifier))
* Output: m
*
* ONLY DECRYPT DATA (fEncrypt = false, signData = false)
* Input: buf represents ciphertext to decrypt, modifier represents data to be removed from the end of the plaintext (since it's not really plaintext data)
* Input (buf): E(iv + plaintext + modifier)
* Output: m
*
* The 'iv' in the above descriptions isn't an actual IV. Rather, if ivType = IVType.Random, we'll prepend random bytes ('iv')
* to the plaintext before feeding it to the crypto algorithms. Introducing randomness early in the algorithm prevents users
* from inspecting two ciphertexts to see if the plaintexts are related. If ivType = IVType.None, then 'iv' is simply
* an empty string. If ivType = IVType.Hash, we use a non-keyed hash of the plaintext.
*
* The 'modifier' in the above descriptions is a piece of metadata that should be encrypted along with the plaintext but
* which isn't actually part of the plaintext itself. It can be used for storing things like the user name for whom this
* plaintext was generated, the page that generated the plaintext, etc. On decryption, the modifier parameter is compared
* against the modifier stored in the crypto stream, and it is stripped from the message before the plaintext is returned.
*
* In all cases, if something goes wrong (e.g. invalid padding, invalid signature, invalid modifier, etc.), a generic exception is thrown.
*/
注释开头说明:该函数用于加密/解密,可选择的进行签名/校验。一共有4中情况:加密+签名、只加密、解密+校验、只解密。重点是其中的加密+签名、解密+校验。
- 加密+签名:fEncrypt = true, signData = true输入:待加密的原始数据,modifier输出:E(iv + buf + modifier) + HMAC(E(iv + buf + modifier))(上述公式中E表示加密,HMAC表示签名)
- 解密+校验:fEncrypt = false, signData = true输入:带解密的加密数据,modifier,buf 即为上面的 E(iv + m + modifier) + HMAC(E(iv + m + modifier))输出:m
老实说,只看注释,我们似乎已经可以明白该函数是如何进行加密和签名的了,操起python 就可以学习伪造加密的viewstate了(开玩笑)。不过我们还是看一下他的代码:
internal static byte[] EncryptOrDecryptData(bool fEncrypt, byte[] buf, byte[] modifier, int start, int length, bool useValidationSymAlgo, bool useLegacyMode, IVType ivType, bool signData)
该函数有9个参数:
- 第1个参数 fEncrypt 表示是加密还是解密,true为加密,false 为解密;
- 第2~5个参数 buf、modifier、start、length 为与原始数据相关;
- 第6个参数 useValidationSymAlgo 表示加密是否使用与签名相同的算法;
- 第7个参数useLegacyMode 与自定义算法有关,一般为false;
- 第8个参数 ivType与加密中使用的初始向量iv 有关,根据注释,旧的 IPType.Hash 已经被去除,现在默认使用IPType.Random;
- 第9个参数 signData 表示是否签名/校验。
关于第6个参数 useValidationSymAlgo 有一些细节要说:
我们知道,在Serialize 函数下有两种情况会进入 EncryptOrDecryptData 函数:
(1)由于web.config 配置中开启加密功能,直接进入 EncryptOrDecryptData() 函数:
此时EncryptOrDecryptData () 参数有5个。
(2)在进入GetEncodeData() 函数后,由于使用了AES/3DES 签名算法,导致再次进入 EncryptOrDecryptData() 函数:
此时EncryptOrDecryptData () 参数有6个。
二者参数个数不同,说明是进入了不同的重载函数。
细细观察会发现,由于使用了AES/3DES签名算法导致进入 EncryptOrDecryptData () 时,第6个参数 useValidationSymAlgo 为true。意义何在呢?因为先进入GetEncodedData() 函数,说明没有开启加密功能,此时由于使用的是AES/3DES签名算法,导致需要在签名后再次EncryptOrDecryptData () 函数。进入EncryptOrDecryptData() 就需要决定使用什么加密算法。所以第6个参数为true,表示加密使用和签名同样的算法。另外多说一句,这种情况下会有两次签名,在GetEncodedData() 里一次,进入EncryptOrDecryptData() 后又一次(后面会看到)。
下面代码将有关解密和校验的操作隐去,只介绍加密与签名的部分。
// 541~543行
System.IO.MemoryStream ms = new System.IO.MemoryStream();
ICryptoTransform cryptoTransform = GetCryptoTransform(fEncrypt, useValidationSymAlgo, useLegacyMode);
CryptoStream cs = new CryptoStream(ms, cryptoTransform, CryptoStreamMode.Write);
这一段是先调用GetCryptoTransform 获取加密工具,而后通过CryptoStream 将数据流链接到加密转换流。不了解这一过程的可以查看微软相关文档。
关键在于GetCryptoTransform() 是如何选择加密工具的?该函数的3个参数中似乎并无算法相关。观其代码:
private static ICryptoTransform GetCryptoTransform(bool fEncrypt, bool useValidationSymAlgo, bool legacyMode)
{
SymmetricAlgorithm algo = (legacyMode ? s_oSymAlgoLegacy : (useValidationSymAlgo ? s_oSymAlgoValidation : s_oSymAlgoDecryption));
lock(algo)
return (fEncrypt ? algo.CreateEncryptor() : algo.CreateDecryptor());
}
algo 表示相应的算法类,那么关键便是 s_oSymAlgoValidation 和 s_oSymAlgoDecryption,察其来历:
ConfigureEncryptionObject() 函数:
switch (Decryption)
{
case "3DES":
s_oSymAlgoDecryption = CryptoAlgorithms.CreateTripleDES();
break;
case "DES":
s_oSymAlgoDecryption = CryptoAlgorithms.CreateDES();
break;
case "AES":
s_oSymAlgoDecryption = CryptoAlgorithms.CreateAes();
break;
case "Auto":
if (dKey.Length == 8) {
s_oSymAlgoDecryption = CryptoAlgorithms.CreateDES();
} else {
s_oSymAlgoDecryption = CryptoAlgorithms.CreateAes();
}
break;
}
if (s_oSymAlgoDecryption == null) // Shouldn't happen!
InitValidationAndEncyptionSizes();
switch(Validation)
{
case MachineKeyValidation.TripleDES:
if (dKey.Length == 8) {
s_oSymAlgoValidation = CryptoAlgorithms.CreateDES();
} else {
s_oSymAlgoValidation = CryptoAlgorithms.CreateTripleDES();
}
break;
case MachineKeyValidation.AES:
s_oSymAlgoValidation = CryptoAlgorithms.CreateAes();
break;
}
看来在网站初始化时就已将相应的加密类分配好了。
继续观察 EncryptOrDecryptData() 的代码:
// 第545~579行
// DevDiv Bugs 137864: Add IV to beginning of data to be encrypted.
// IVType.None is used by MembershipProvider which requires compatibility even in SP2 mode (and will set signData = false).
// MSRC 10405: If signData is set to true, we must generate an IV.
bool createIV = signData || ((ivType != IVType.None) && (CompatMode > MachineKeyCompatibilityMode.Framework20SP1));
if (fEncrypt && createIV)
{
int ivLength = (useValidationSymAlgo ? _IVLengthValidation : _IVLengthDecryption);
byte[] iv = null;
switch (ivType) {
case IVType.Hash:
// iv := H(buf)
iv = GetIVHash(buf, ivLength);
break;
case IVType.Random:
// iv := [random]
iv = new byte[ivLength];
RandomNumberGenerator.GetBytes(iv);
break;
}
Debug.Assert(iv != null, "Invalid value for IVType: " + ivType.ToString("G"));
cs.Write(iv, 0, iv.Length);
}
cs.Write(buf, start, length);
if (fEncrypt && modifier != null)
{
cs.Write(modifier, 0, modifier.Length);
}
cs.FlushFinalBlock();
byte[] paddedData = ms.ToArray();
这一段开头是在生成IV。IV是加密时使用的初始向量,应保证其随机性,防止重复IV导致密文被破解。
- ivLength为64。这里随机生成64个字节作为iv。
- 三次调用 cs.Write(),分别写入iv、buf、modifier。cs即为前面生成的CryptoStream类实例,用于将数据流转接到加密流。这里与我们前面所说的公式 E(iv + buf + modifier) 对应上了。
- 调用ms.ToArray() ,即返回加密完成后的生成的字节序列。
继续观察 EncryptOrDecryptData() 的代码:
// 第550~644行
// DevDiv Bugs 137864: Strip IV from beginning of unencrypted data
if (!fEncrypt && createIV)
{
// strip off the first bytes that were random bits
...
}
else
{
bData = paddedData;
}
...
// At this point:
// If fEncrypt = true (encrypting), bData := Enc(iv + buf + modifier)
// If fEncrypt = false (decrypting), bData := plaintext
if (fEncrypt && signData) {
byte[] hmac = HashData(bData, null, 0, bData.Length);
byte[] bData2 = new byte[bData.Length + hmac.Length];
Buffer.BlockCopy(bData, 0, bData2, 0, bData.Length);
Buffer.BlockCopy(hmac, 0, bData2, bData.Length, hmac.Length);
bData = bData2;
}
// At this point:
// If fEncrypt = true (encrypting), bData := Enc(iv + buf + modifier) + HMAC(Enc(iv + buf + modifier))
// If fEncrypt = false (decrypting), bData := plaintext
// And we're done
return bData;
这里是最后一部,将加密后生成的字节序列传给HashData,让其生成hash值,并缀在字节序列后面。
这就与前面的公式 E(iv + buf + modifier) + HMAC(E(iv + buf + modifier)) 对应上了。
看完 EncryptOrDecryptData() 函数的代码,我么也明白了其流程,总结下来其实就一个公式,没错就是 E(iv + buf + modifier) + HMAC(E(iv + buf + modifier)) 。
7. modifier 的来历
在前面进行签名和加密的过程中,都使用了一个关键变量叫做modifier,该变量同密钥一起用于签名和加密。该变量来自于 GetMacKeyModifier() 函数:
// This will return the MacKeyModifier provided in the LOSFormatter constructor or
// generate one from Page if EnableViewStateMac is true.
private byte[] GetMacKeyModifier() {
if (_macKeyBytes == null) {
// Only generate a MacKeyModifier if we have a page
if (_page == null) {
return null;
}
// Note: duplicated (somewhat) in GetSpecificPurposes, keep in sync
// Use the page's directory and class name as part of the key (ASURT 64044)
uint pageHashCode = _page.GetClientStateIdentifier();
string viewStateUserKey = _page.ViewStateUserKey;
if (viewStateUserKey != null) {
// Modify the key with the ViewStateUserKey, if any (ASURT 126375)
int count = Encoding.Unicode.GetByteCount(viewStateUserKey);
_macKeyBytes = new byte[count + 4];
Encoding.Unicode.GetBytes(viewStateUserKey, 0, viewStateUserKey.Length, _macKeyBytes, 4);
}
else {
_macKeyBytes = new byte[4];
}
_macKeyBytes[0] = (byte)pageHashCode;
_macKeyBytes[1] = (byte)(pageHashCode >> 8);
_macKeyBytes[2] = (byte)(pageHashCode >> 16);
_macKeyBytes[3] = (byte)(pageHashCode >> 24);
}
return _macKeyBytes;
}
函数流程:
- 函数开头先通过 _page.GetClientStateIdentifier 计算出一个 pageHashCode;
- 如果有viewStateUserKey,则modifier = pageHashCode + ViewStateUsereKey;
- 如果没有viewStateUserKey,则modifier = pageHashCode
先看pageHashCode 来历:
// This is a non-cryptographic hash code that can be used to identify which Page generated
// a __VIEWSTATE field. It shouldn't be considered sensitive information since its inputs
// are assumed to be known by all parties.
internal uint GetClientStateIdentifier() {
// Use non-randomized hash code algorithms instead of String.GetHashCode.
// Use the page's directory and class name as part of the key (ASURT 64044)
// We need to make sure that the hash is case insensitive, since the file system
// is, and strange view state errors could otherwise happen (ASURT 128657)
int pageHashCode = StringUtil.GetNonRandomizedHashCode(TemplateSourceDirectory, ignoreCase:true);
pageHashCode += StringUtil.GetNonRandomizedHashCode(GetType().Name, ignoreCase:true);
return (uint)pageHashCode;
}
从注释中也可以看出,计算出directory 和 class name 的hash值,相加并返回。这样pageHashCode 就有4个字节了。所以我们可以手动计算一个页面的 pageHashCode,directory 和 class name 应当分别是网站集路径和网站集合名称。除此之外也可以从页面中的隐藏字段”__VIEWSTATEGENERATOR” 中提取。便如下图:
“__VIEWSTATEGENERATOR” 与 pageHashCode 的关系在这里:
再看ViewStateUserKey 的来历:
按照官方说法:ViewStateUserKey 即 :在与当前页面关联的ViewState 变量中为单个用户分配标识符。
可见,ViewStateUserKey 是一个随机字符串值,且要保证与用户关联。如果网站使用了ViewStateUserKey,我们应当在SessionID 或 cookie 中去猜。在CVE-20202-0688 中,便是取 SessionID 作为ViewStateUserKey。
8. 伪造ViewState
经过上面长篇大论的贴代码、分析。我们已经大致明白了ASP.NET 生成和解析ViewState 的流程。这有助帮助我们理解如何伪造 ViewState。当然了伪造 ViewState 仍然需要 泄露web.config,知晓其 密钥与算法。
- 如果签名算法不是AES/3DES,无论是否开启加密功能,我们只需要根据其签名算法和密钥,生成一个签名的ViewState。由于发送该ViewState的时候没有使用”__VIEWSTATEENCRYPTED” 字段,导致ASP.NET 在解析时直接进入GetDecodedData() 进行签名校验,而不再执行解密步骤。
- 如果签名算法是 AES/3DES,无论是否开启加密功能,我们只需按照先前所讲,对数据先签名一次,再加密一次,再签名一次。 然后发送给服务端,ASP.NET 进入 GetDecodedData(),然后先进 EncryptOrDecryptData() 进行一次校验和解密,出来后再进行一次校验。
换种表达方式,无论使用什么签名算法,无论是否开启加密功能,我们伪造ViewState时,就按照没有开启加密功能情况下的正常步骤,去伪造ViewState。
9.附录:
[1] ysoserial.net
https://github.com/pwntester/ysoserial.net
[2] viwgen (python 写的viewstate生成工具,不依赖.NET,方便自动化脚本使用)
https://github.com/0xacb/viewgen
[3] 什么是View State 及其在ASP.NET中的工作方式
https://www.c-sharpcorner.com/UploadFile/225740/what-is-view-state-and-how-it-works-in-Asp-Net53/
[4] 微软官方文档:ASP.NET服务器控件概述
https://docs.microsoft.com/zh-cn/troubleshoot/aspnet/server-controls
[5]《MSDN杂志》文章:ViewState 安全
https://docs.microsoft.com/en-us/archive/msdn-magazine/2010/july/security-briefs-view-state-security
[6] 安全通告KB2905247
[7] 使用ViewState
http://appetere.com/post/working-with-viewstate
[8] Exhange CVE-2020-0688
发表评论
您还未登录,请先登录。
登录