作者: [email protected]知道创宇404实验室
日期:2023年7月10日
由于金蝶云星空能够使用 format 参数指定数据格式为二进制,攻击者可以通过发送由 BinaryFormatter 恶意序列化后的数据让服务端进行危险的 BinaryFormatter 反序列化操作。反序列化过程中没有对数据进行签名或校验,导致攻击者可以在未授权状态下进行服务器远程代码执行。
刚接触 .NET 不久,正巧遇上了金蝶反序列化漏洞,本篇文章将从入门学习如何调试——分析金蝶反序列化漏洞。
金蝶云星空 < 6.2.1012.4
7.0.352.16 < 金蝶云星空 <7.7.0.202111
8.0.0.202205 < 金蝶云星空 < 8.1.0.20221110
https://www.heshuyun.com/265.html
本文选择漏洞版本 7.6,安装就不用说,下载地址里面都有。
安装完成后访问能打开就行,如图 1 所示:
dnSpy 是一个调试器和 .NET 程序集编辑器。即使没有任何可用的源代码,你也可以使用它来编辑和调试程序集。
https://github.com/dnSpy/dnSpy
Process Hacker是一款免费、强大的多用途工具,可帮助你监控系统资源、调试软件和检测恶意软件。
https://processhacker.sourceforge.io/
已知漏洞的路径如下,现在需要通过该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 所示。
参考网上公开的 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 所示。
因为 Create 方法中的 requestExtractor = new JQueryRequestExtractor(request, isGet);,其内部会根据 request 传递的值来进行属性的赋值给 this.form,如图 21 所示。
待后续调用到 this.Format 时,则会自动触发 Format 定义,如图 22 所示。
如图 23,传递 format 参数为 3。
接下来根据这个属性值来进行匹配,为3正好能匹配到 Binary(当然这里 format 赋值为 Binary 也是可以的),如图 24 所示。
一开始以为 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"]}
分析到反序列化执行点发现,这里是先进行反序列化,之后 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。
这里只举了一个较为经典的案例,除此之外还有很多。
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攻击链)。
本篇文章算是我从.NET入门到调试分析第一个漏洞,虽然一路上踩得坑还是不少,但是收获还是挺多的。本文主要讲了用 dnsPy 进行附加进程调试,至于VSstudio 调试以及一些编译优化入门可以看一下这篇文章:https://paper.seebug.org/1894/。
[3]https://paper.seebug.org/901/
[4]https://learn.microsoft.com/zh-cn/dotnet/standard/serialization/binaryformatter-security-guide
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/2089/