作者:HuanGMz@知道創宇404實驗室
時間:2020年10月30日
.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
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1386/
暫無評論