最近看到Kingdee星空反序列化漏洞各种各样的接口,本文将分析其漏洞原理及绕过思路,入门学习.NET程序的调试过程。
参照网络上Kingdee星空安装教程搭建即可,调试工具使用dnspy:
1、搜索dll文件定位exe程序
2、附加进程到dnSpy中
调试-->附加到进程(P)-->附加
3、搜索要调试的程序集
调试-->窗口-->模块(如果搜不到说明dll文件还没加载进来,先触发使其加载后再搜索)
4、断点调试
注意:如果调试时dnSpy报错:无法获取局部变量或参数的值,因为它在此时不可用,可能是被优化了
根据 dnSpy的wiki描述:创建ini文件禁用编译优化(创建完后需重启iis服务器);配置环境变量。
根据网络上的POC可以发现漏洞的url路径都是以.kdsvc
结尾的,我们在web.config文件中搜索可以发现handlers对Http Request请求做了处理:
path="*kdsvc"
处理程序要处理的请求路径,即当应用程序收到路径以kdsvc结尾的请求时进行处理
verb="*"
处理程序要处理的HTTP动作,在这里*表示该处理程序将处理所有类型的HTTP请求包括GET、POST等
type="xxx,xxx"
处理程序的类型,这里的类型由两部分组成,处理程序类型的完全限定名,命令空间(dll文件)
断点入口找到dll文件展开后定位到具体类:Kingdee.BOS.ServiceFacade.KDServiceFx.KDServiceHandler
,该类是对传入的url参数进行处理后返回程序实例KDSVCHandler:
查看该处理器,第一个方法为ProcessRequest
:
其中方法RequestExtractor#Creat
是对HTTP Request请求做处理,判断请求方法、是否为json格式:
我们可以看到方法IsContainsJson
根据ContentType是否包含json返回flag:
然后通过requestExtractor = new JQueryRequestExtractor(request, isGet)
将request传递的值进行属性的赋值:
继续跟进该实例的方法RequestExcuteRuntime#StartRequest
,该方法用于处理HTTP请求并根据请求的情况执行相应的逻辑包含返回相应状态码等:
方法RequestExcuteRuntime#StartRequest
里调用了方法RequestExcuteRuntime#BeginRquest
,方法在处理完会话信息后,继续执行到69行我们可以发现这里的path为我们传递的url ,通过webCtx.Context.Server.MapPath(path)
生成一个localFile,接着调用了ServiceTypeManager#BuidServiceType
方法:
跟进该方法可以发现,首先会判断localFile是否包含common.kdsvc
,如果包含会进入ServiceTypeManager.ReflectServiceType
方法,该方法会通过查找缓存或在程序集中搜索提取出类名和方法名等:
接着RequestExcuteRuntime#BeginRquest
方法会走到RequestExcuteRuntime.pipeline.ExcuteRequest(kdserviceContext)
处:
跟进方法可以看到,对Modules的遍历会进入不同的OnProcess方法(是对request请求校验:分别为是否为https、session信息、version信息以及API鉴权):
最后一次循环会进入ExecuteServiceModule#OnProcess
方法中:
方法RequestExtractor#GetServiceParameters
用于处理参数的提取和验证(即payload位置这里重点关注下),对传入的参数进行判断,首先判断参数是否包含parameters
,如果包含则创建一个JSON数组;当payload传入的参数为ap0时,会进入else里遍历所有的ap+数字:
所以这里我们猜想payload的参数位置应该可以为两个:
{"parameters": "[\"payload\"]"}
{"ap*": "payload"}
对参数处理完后方法进入如下:
SerializerProxy serializeProxy = new SerializerProxy(requestExtractor.Format, contentEncoding, requestExtractor.Compressed, requestExtractor.UserAgent, requestExtractor.UrlForm_Encode);
this.Format=format对传入的参数format定义,我们传入的format=3会匹配到Binary(直接给format赋值为 Binary也是可以的):
接着会到ServiceExecutor#Execute
方法:
该方法首先需要用构造函数参数来实例化一个特定类型的对象obj且传参只能为context即KDServiceContext
,然后调用不同的serializer.Deserializer
进行反序列化处理,所以如果任意一个类型的构造函数支持传递该参数都可以进入反序列化:
进入的第一个反序列化方法为SerializerProxy#Deserialize
,该方法用来对参数类型做判断方法如下,参数类型不能为(string、int、byte、float...):
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);
}
}
最终进入BinaryFormatterProxy#Deserialize
方法中通过BinaryFormatter#Deserialize
方法实现反序列化(根据微软描述:将BinaryFormatter.Deserialize方法用于不受信任的输入时,该方法永远都不安全):
1、Url路径:程序集.满足条件的类.common.kdsvc
满足构造函数支持传递KDServiceContext类型,且传递参数不为上述SerializerProxy#Deserialize
方法中的类型即可,我们可以看到在程序集Kingdee.BOS.ServiceFacade.ServicesStub
中满足条件的类非常多,这里我们就找第一个类说明下:
再找一个满足传参的类,如:
SaveBizTipsInfos(string bizHost, List<IBizTipsInfos> tips)
2、参数
由于第一个参数为string所以我们需要用第二个参数来传递payload,这里我们用ysoserial.exe生成:
ysoserial.exe -c "a.cs;System.Windows.Forms.dll;System.Web.dll;System.dll" -f BinaryFormatter -o base64 -g ActivitySurrogateSelectorFromFile
parameters根据上述RequestExtractor#GetServiceParameters
方法可以得知,需要传递一个数组对象,同样由于第一个参数为string不能放payload,所以将paylaod放置在数组的第二个位置即可:
漏洞修复:启用金蝶数据签名校验格式KigndeeXml格式并禁用了二进制序列化:
<add key="KDSVCDefaultFormat" value="4"/>
<add key="EnabledKDSVCBinary" value="false"/>
绕过思路:修改format为4,即赋值为KingdeeXml,我们可以看到代码最终还是会通过BinaryFormatter#Deserialize
反序列化为KingdeeXMLPack对象:
KingdeeXMLPack kingdeeXMLPack = obj as KingdeeXMLPack;
if (kingdeeXMLPack != null)
{
BinaryFormatterProxy binaryFormatterProxy = new BinaryFormatterProxy(this.encoding, false);
obj = binaryFormatterProxy.Deserialize(kingdeeXMLPack.Data, type);
}
所以我们可以将参数修改为KingdeeXMLPack形式:
{"ap0": "<KingdeeXMLPack>BinaryPayload</KingdeeXMLPack>", "format": "4"}
我们发现也是可以成功执行的:
79 篇文章
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!