shiro反序列化漏洞中有个经典的现象,就是不能用ysoserial自带的链去打commons-collections3的依赖。有很多文章分析过,但总觉得没完全看懂,差了点什么。那么调试源码详细分析下这个问题。
添加commonscollections3.2.1依赖后,尝试反序列化打cc6链失败,是因为反序列化输入流ClassResolvingObjectInputStream重写了resolveClass方法:
1 | protected Class<?> resolveClass(ObjectStreamClass osc) throws IOException, ClassNotFoundException { |
对比下正常反序列化流程的ObjectInputStream#resolveClass
1 | protected Class<?> resolveClass(ObjectStreamClass desc) |
看上去大差不差,只是将原生的Class.forName改成了ClassUtils.forName
1 | public static Class forName(String fqcn) throws UnknownClassException { |
可以看到实际上ClassUtils.forName并没有调用原生的Class.forName,而是改成调用了几个CL_ACCESSOR的loadClass。这个accessor是ClassUtils的内部类
1 | private static interface ClassLoaderAccessor { |
调试时发现反序列化时前两个if都是调用tomcat的ParallelWebAppClassLoader的loadClass进行加载,第三处是调用tomcat的AppClassLoader的loadClass加载。
这里有些文章就会分析说是因为ClassLoader#loadClass不能加载数组类,而Class#forName可以加载数组类,所以导致利用失败。
这个现象确实是存在的,比如这样:
1 | Class.forName("[Ljava.lang.String;");//成功 |
但也没必要当作一个固定的结论,可以分析下原因。
实际上Class#forName最后调用了一个native方法forName0对全类名进行了加载,而ClassLoader#loadClass并不能对应一个native方法,而是基于实现类的loadClass/findClass方法有不同实现的。我完全可以定义一个能加载数组类的ClassLoader
1 | class MyClassLoader extends ClassLoader { |
那么实际的类加载中loadClass是怎么实现的呢?先看看ClassLoader#loadClass
1 | protected Class<?> loadClass(String name, boolean resolve) |
这相当于loadClass的原型,是实现了双亲委派模型的。一般来说ClassLoader的具体实现类都不会修改这个loadClass,而是改findClass方法,改写具体的搜索类的逻辑。比如最常用的URLClassLoader类,也就是ExtClassLoader和AppClassLoader的父类,就没有重写loadClass,而是重写了findClass:
1 | protected Class<?> findClass(final String name) |
发现在这里将全类名转成了/a/b/c.class形式,然后调用defineClass去加载类文件。那么这里实际也解释了为什么常用的ClassLoader.getSystemClassLoader().loadClass无法加载数组,因为数组类的Class名称又带括号又带分号的,肯定对应不上文件。
但是这只能证明URLClassLoader不能加载数组类,tomcat中一般负责加载用户代码的类是ParallelWebappClassLoader,继承自WebappClassLoaderBase,实际上调用的也是WebappClassLoaderBase#loadClass
1 | public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { |
和前面说的一般不重写loadClass,而是重写findClass不同,实际上WebappClassLoaderBase重写了loadClass方法。这是因为tomcat设计上需要打破jdk默认的双亲委派机制。具体流程大概是这样:
1、先在tomcat的缓存中查找该类是否已经加载过
2、如果没有,在jdk的缓存里查找该类是否已经加载过
3、如果没有,则使用ExtClassLoader#loadClass加载,ExtClassLoader会走双亲委派。这里是为了在打破双亲委派的同时保留加载系统内置类的功能,绕过AppClassLoader。
4、如果没有加载成功,判断是否设置了delegate也就是遵循双亲委派,默认是false,就会调用WebAppClassLoader#findClass方法进行加载。
5、如果没有加载成功,会用父类加载器调用Class#forName进行加载,也就是用tomcat中的CommonClassLoader。
可以看到在这个过程里,实际上是既有Class#forName又有ClassLoader#loadClass的。而forName是可以加载数组类的,也就是说问题就出在WebAppClassLoader#findClass
1 | public Class<?> findClass(String name) throws ClassNotFoundException { |
看看findClassInternal
1 | protected Class<?> findClassInternal(String name) { |
具体流程先不看,name一开始就经过了binaryNameToPath
1 | private String binaryNameToPath(String binaryName, boolean withLeadingSlash) { |
实际上也是和URLClassLoader#findClass类似的改成/a/b/c.class形式,通过文件名用defineClass加载,那么一样也不能加载数组类。
到这里也就知道具体的解释了,ParallelWebappClassLoader加载范围内的类,实际上就是WEB-INF/classes和WEB-INF/lib这两个路径下的类,是不能加载数组类的。
那么其他路径下的数组类,也就是用CommonClassLoader进行forName的方法进行加载。forName是可以加载数组类的,但要确认用CommonClassLoader作为类加载器是否能加载不在CommonClassLoader路径下的类。实际上CommonClassLoader是一个URLClassLoader,其中的URL只有tomcat/lib下面的jar包。
那这里就有有一个比较微妙的地方,也是之前分析类加载时忽略的地方:Class#forName进行类加载时会遵循双亲委派吗?
实际上根据经验,答案是会。因为我们调用默认的Class#forName时实际上是调用的native方法forName0,配置的类加载器是AppClassLoader,而此时是可以加载java.lang.String之类的内置类的。
说了这么多,结论就是在tomcat中,ParallelWebappClassLoader#loadClass不能加载自己web项目下的数组类,包括WEB-INF/classes和WEB-INF/lib两个目录,其他路径下的数组类可以加载。
解决方法,最通用的自然就是让payload里不出现数组类。CC中Transformer数组类实际上有两种功能:
1、InvokerTransformer调用Runtime#exec时需要循环调用解决不能序列化的问题
2、无法控制调用点输入时需要用ConstantTransforer传递常量值
解决第一点的话,就是不用Runtime#exec作为触发点,改用TemplatesImpl。解决第二点,就需要调用链能完全控制一个输入参数,一直传递到执行点,满足这个条件的就是CC6。拼接一下就行了
1 | public class ShiroCC { |
也可以用JRMP之类的二次反序列化来绕过限制,但之前分析过JRMP链在JDK高版本(>8u241)是用不了的。
参考链接
https://blog.zsxsoft.com/post/35
http://blog.orange.tw/2018/03/pwn-ctf-platform-with-java-jrmp-gadget.html
http://redteam.today/2019/09/20/shiro%20%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%A4%8D%E7%8E%B0/