跳转到帖子

游客您好,欢迎来到黑客世界论坛!您可以在这里进行注册。

赤队小组-代号1949(原CHT攻防小组)在这个瞬息万变的网络时代,我们保持初心,创造最好的社区来共同交流网络技术。您可以在论坛获取黑客攻防技巧与知识,您也可以加入我们的Telegram交流群 共同实时探讨交流。论坛禁止各种广告,请注册用户查看我们的使用与隐私策略,谢谢您的配合。小组成员可以获取论坛隐藏内容!

TheHackerWorld官方

ava反序列化过程中 RMI JRMP 以及JNDI多种利用方式详解

精选回复

发布于

ava反序列化过程中 RMI JRMP 以及JNDI多种利用方式详解

5fffa585dc008.png

Java反序列化过程中 RMI JRMP 以及JNDI多种利用方式详解

前言

Java反序列化漏洞一直都是Java Web漏洞中比较难以理解的点,尤其是碰到了RMI和JNDI种种概念时,就更加的难以理解了。笔者根据网上各类相关文章中的讲解,再结合自己对RMI JRMP以及JNDI等概念的理解,对 RMI客户端、服务端以及rmiregistry之间的关系,和三方之间的多种攻击方式进行了详细的介绍,希望能对各位读者学习Java Web安全有所帮助。

RPC框架原理简介

首先讲这些之前要明白一个概念,所有编程中的高级概念,看似很高级的一些功能什么的,都是建立于最基础的代码之上的,再高级也他离不开JDK。

例如此次涉及到的分布式的概念,就是通过java的socket,序列化,反序列化和反射来实现的。

举例说明 客户端要调用服务端的A对象的A方法,客户端会生成A对象的代理对象,代理对象里通过用Socket与服务端建立联系,然后将A方法以及调用A方法是要传入的参数序列化好通过socket传输给服务端,服务端接受反序列化接受到的数据,然后通过反射调用A对象的A方法并将参数传入,最终将执行结果返回给客户端,给人一种客户端在本地调用了服务端的A对象的A方法的错觉。

RMI流程源码分析

到后来JAVA RMI这块也不例外 但是为了方便更灵活的调用发展成了以下的样子

在客户端(远程方法调用者)和服务端(远程方法提供者)之间又多了一个丙方也就所谓的Registry也就是注册中心。

启动这个注册中心的代码非常简单,如下所示

这个Registry是一个单独的程序 路径位于/Library/Java/JavaVirtualMachines/jdk1.8.0_221.jdk/Contents/Home/bin/rmiregistry

1607310463_5fcd9c7f83fbe559f5417.png!sma

刚刚所示的启动RMIRegistry的代码,也就是调用了这个rmiregistry可执行程序而已。

简单follow一下代码

public static Registry createRegistry(int port) throws RemoteException {
return new RegistryImpl(port);
}
public RegistryImpl(final int var1) throws RemoteException {
this.bindings = new Hashtable(101);
if (var1 == 1099 && System.getSecurityManager() != null) {
try {
......
} else {
LiveRef var2 = new LiveRef(id, var1);
this.setup(new UnicastServerRef(var2, RegistryImpl::registryFilter));
}

}

简单follow一下代码

public static Registry createRegistry(int port) throws RemoteException {
return new RegistryImpl(port);
}
public RegistryImpl(final int var1) throws RemoteException {
this.bindings = new Hashtable(101);
if (var1 == 1099 && System.getSecurityManager() != null) {
try {
......
} else {
LiveRef var2 = new LiveRef(id, var1);
this.setup(new UnicastServerRef(var2, RegistryImpl::registryFilter));
}

}

很简单 没啥东西 liveRef里面就四个属性

public class LiveRef implements Cloneable {
//指向一个TCPEndpoint对象,指定的Registry的ip地址和端口号
private final Endpoint ep;
//一个目前不知道做什么用的id号
private final ObjID id;
//为null
private transient Channel ch;
//为true
private final boolean isLocal;
......
}

this.setup(new UnicastServerRef(var2, RegistryImpl::registryFilter));这段里面有个参数RegistryImpl::registryFilter这个东西就是jdk1.8.121版本以后添加的registryFilter专门用来校验传递进来的反序列化的类的,不在反序列化白名单内的类就不准进行反序列化操作,具体的方法代码如下

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;
}
}
}

这个白名单先暂且放一放,后面用到了再说。执行完new UnicastServerRef(var2, RegistryImpl::registryFilter)后简单看一下UnicastServerRef对象里的内容

1607310478_5fcd9c8e7369cb6ee98e4.png!sma

setup方法内容

private void setup(UnicastServerRef var1) throws RemoteException {
this.ref = var1;
var1.exportObject(this, (Object)null, true);
}

UnicastServerRef.exportObject()方法内容

public Remote exportObject(Remote var1, Object var2, boolean var3) throws RemoteException {
//获取RegistryImpl的class对象
Class var4 = var1.getClass();

Remote var5;
try {
//Util.createProxy返回的值为RegistryImpl_Stub,这个stub在后面会进行讲解
var5 = Util.createProxy(var4, this.getClientRef(), this.forceStubUse);
} catch (IllegalArgumentException var7) {
throw new ExportException("remote object implements illegal remote interface", var7);
}
//RegistryImpl_Stub继承自RemoteStub判断成功
if (var5 instanceof RemoteStub) {
//为Skeleton赋值,通过this.skel = Util.createSkeleton(var1)来进行赋值,最终Util.createSkeleton(var1)返回的结果为一个RegistryImpl_Skel对象,这个Skeleton后面也会讲
this.setSkeleton(var1);
}
//实例化一个Target对象
Target var6 = new Target(var1, this, var5, this.ref.getObjID(), var3);
//做一个绑定这个target对象里有stub的相关信息
this.ref.exportObject(var6);
this.hashToMethod_Map = (Map)hashToMethod_Maps.get(var4);
//最终LocateRegistry.createRegistry(1099)会返回一个RegistryImpl_Stub对象
//同时启动rmiregistry,并监听指定端口
return var5;
}

很好这样启动rmiregistry的过程就简单分析完毕了,但是此时有一个问题,就是为什么会需要rmiregistry这么一个注册机制?客户端和服务端之间直接通过Socket互相调用不就好了么?就像马士兵老师的代码那样。很明显那个只是简单的讲解原理的用例,实际生产环境中肯定不会这么简单。

首先看下面这个RMI简单的流程图

1607310491_5fcd9c9ba5d5eb66e4d63.png!sma

在考虑为什么需要这个rmiregistry之前,先思考一个比较尴尬的问题。就是客户端(远程方法调用方)要想调用服务端(远程放方法服务方)的话,客户端要怎样才能知道服务端用来提供远程方法调用服务的ip地址和端口号?你说直接事先商量好然后写死在代码里面?可是服务方提供的端口号都是随机的啊,总不能我服务端每增加一个新的远程方法提供类就手动指定一个新的端口号吧?

所以现在就很尴尬,陷入了一个死循环,客户端想要调用服务端的方法客户端就需要先知道服务端的地址和对应的端口号,但是客户端又不知道因为没人告诉他。。。所以就相当的头疼。

此时就有了rmiregistry这么一个东西,我们先把rmiregistry称为丙方,功能很简单,服务端每新提供一个远程方法,都会来丙方(rmiregistry)这里注册一下,写明提供该方法远程条用服务的ip地址以及所对应的端口以及别的一些信息。

如下面的代码所示,首先我们如果要写一个提供远程方法调用服务的类,首先先写一个接口并继承Remote接口,

public interface IHello extends Remote {
//sayHello就是客户端要调用的方法,需要抛出RemoteException
public String sayHello()throws RemoteException;
}

然后写一个类来实现这个接口

package com.rmiTest.IHelloImpl;

import com.rmiTest.IHello;

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
// 该类可以选择继承UnicastRemoteObject,也可以通过下面注释中的这种形式,其实本质都一样都是调用了
// exportObject()方法
// Remote remote = UnicastRemoteObject.exportObject(new HelloImpl());
// LocateRegistry.getRegistry("127.0.0.1",1099).bind("hello",remote);
public class HelloImpl extends UnicastRemoteObject implements IHello {

public HelloImpl() throws RemoteException {

}

@Override
public String sayHello() {
System.out.println("hello");
return "hello";
}
}

最后将这个HelloImpl类注册到也可以说是绑定到rmiregistry也就是丙方中

package com.rmiTest.provider;

import com.chouXiangTest.impl.HelloServiceImpl;
import com.rmiTest.IHelloImpl.HelloImpl;

import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;

public class RMIProvider {
public static void main(String[] args) throws RemoteException, AlreadyBoundException {
LocateRegistry.getRegistry("127.0.0.1",1099).bind("hello",new HelloImpl());
}
}

首先我们先跟一下HelloImpl这个远程对象的实例化过程,首先HelloImpl是UnicastRemoteObject的子类,所以HelloImpl在实例化时会先调用UnicastRemoteObject类的构造方法,其构造方法内容如下

protected UnicastRemoteObject(int port) throws RemoteException
{
//这个prot参数是用来指定远程方法对应的端口的,默认情况下是随机的,也可以手动传入参数来指定
this.port = port;
exportObject((Remote) this, port);
}

发现其会调用一个exportObject方法,继续跟进该方法

private static Remote exportObject(Remote obj, UnicastServerRef sref)
throws RemoteException
{
// if obj extends UnicastRemoteObject, set its ref.
if (obj instanceof UnicastRemoteObject) {
((UnicastRemoteObject) obj).ref = sref;
}
return sref.exportObject(obj, null, false);
}

继续跟进UnicastServerRef.exportObject方法,其内部代码如下

public Remote exportObject(Remote var1, Object var2, boolean var3) throws RemoteException {
//获取HelloImpl的class对象  
Class var4 = var1.getClass();

Remote var5;
try {
//这一步就是创建一个proxy对象,该proxy对象是实现了IHello接口,使用的Handler是RemoteObjectInvocationHandler
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);
}

Target var6 = new Target(var1, this, var5, this.ref.getObjID(), var3);
this.ref.exportObject(var6);
this.hashToMethod_Map = (Map)hashToMethod_Maps.get(var4);
return var5;
}

其中Util.createProxy()方法返回的结果如下图所示

1607310512_5fcd9cb0a250f1f690724.png!sma

继续跟入this.ref.exportObject(var6),经过一系列的嵌套调用,最终来到了TCPTransport的exportObject方法,该方法内容如下

public void exportObject(Target var1) throws RemoteException {
synchronized(this) {
//为远程方法开方一个端口
this.listen();
++this.exportCount;
}

boolean var2 = false;
boolean var12 = false;

try {
var12 = true;
super.exportObject(var1);
var2 = true;
var12 = false;
} finally {
if (var12) {
if (!var2) {
synchronized(this) {
this.decrementExportCount();
}
}

}
}

此处跟进this.listen()方法,

private void listen() throws RemoteException {
assert Thread.holdsLock(this);
//获取TCPEndpoint对象
TCPEndpoint var1 = this.getEndpoint();
//从TCPEndpoint对象中获取端口号,默认情况下是为0
int var2 = var1.getPort();
if (this.server == null) {
if (tcpLog.isLoggable(Log.BRIEF)) {
tcpLog.log(Log.BRIEF, "(port " + var2 + ") create server socket");
}

try {
//此方法执行完成后会随机分配一个端口号
this.server = var1.newServerSocket();
Thread var3 = (Thread)AccessController.doPrivileged(new NewThreadAction(new TCPTransport.AcceptLoop(this.server), "TCP Accept-" + var2, true));
var3.start();
} catch (BindException var4) {
throw new ExportException("Port already in use: " + var2, var4);

创建帐户或登录后发表意见

最近浏览 0

  • 没有会员查看此页面。