背景知识

  1. ysoserial包含了许多针对通用Java依赖包的小工具链,可以在正常情况下通过使目标程序进行不安全的反序列化实现攻击利用。

  2. Gadgets

    /*
     * 	BadAttributeValueExpException.readObject()
     *		PropertysetItem.readObject()
     * 			NestedMethodProperty.readObject()
     *				NestedMethodProperty.initialize()
     *					MethodProperty.initGetterMethod(simplePropertyName, propertyClass);
     *		PropertysetItem.toString()
     *			PropertysetItem.getItemProperty()
     *				NestedMethodProperty.getValue()
     *					Method.invoke()
     *						TemplatesImpl.getOutputProperties()
     *							TemplateImpl.newTransformer()
     *   								TemplateImpl.getTransletInstance()
     *   									TemplateImpl.defineTransletClasses()
     *                                      Class.newInstance() /* Invoke malicious constructor of malicious class*/
     */
    
  3. Java序列化

    • Java原生序列化和反序列化将属性的类名同时写入到了序列化流中
    • Java反序列化时会调用ObjectInputStream的readObject()方法来进行反序列化
    • 无论反序列化是否成功,只要读取到类名就会加载这个类,如果该类有自定义的readObjec()就执行自定义的readObject()方法,如果没有就按照默认的readObject()流程执行,因此只要readObject()存在调用点,无论业务代码中是否使用了这些类,都会执行readObject()过程从而执行恶意代码
  4. Vaadin

    • Vaadin是一个集成了JavaScript Web组件的Java UI框架,基于该框架,开发者可以使用Java语言开发出高质量的用户界面。

验证过程

  • 配置环境

    • com.vaadin:vaadin-server:7.7.14
    • com.vaadin:vaadin-shared:7.7.14
    • xalan:xalan:2.7.2
    • jdk1.8(非必须)
  • 修改ysoserial的PayloadRunner,因为它默认的攻击命令是clack.exe,但Ubuntu是没有这条指令的,因此需要对getFirstExistingFile()进行修改,在Ubuntu系统中因为存在gnome-calculator,所以如果攻击成功会启动Ubuntu的计算器

    image-20210806105905436

    private static String getDefaultTestCmd() {
        return getFirstExistingFile(
            "C:\\Windows\\System32\\calc.exe",
            "/Applications/Calculator.app/Contents/MacOS/Calculator",
            "/usr/bin/gnome-calculator",
            "/usr/bin/kcalc"
        );
    }
      
    private static String getFirstExistingFile(String ... files) {
        //        return "calc.exe";
        for (String path : files) {
            if (new File(path).exists()) {
                return path;
            }
        }
        throw new UnsupportedOperationException("no known test executable");
    }
    
  • 在IDEA中启动Click.java

    • 运行之前

      image-20210808130524282

    • 运行之后

      image-20210808130621595

攻击原理

  1. 首先在生成Payload后打一个断点,来看一下真实的Payload长什么样子

    image-20210808132208956

  2. 由于最终序列化对象的最外层是javax.management.BadAttributeValueExpException(该类位于rt.jar下,因此99.9%可以反序列化出该类),首先看BadAttributeValueExpException.readObject()

    • 首先从中反序列化出所有的字段放到GetField中

      • 在反序列化字段的过程中一定会调用到val属性的反序列化方法,由于val属性为PropertysetItem类型并未自定义readObject方法

      • 接下来进入PropertysetItem中存放的元素的反序列化方法,此时元素类型为NestedMethodProperty,它实现了自定义readObject方法,因此看NestedMethodProperty.readObject()

        • 首先调用默认的反序列化方法反序列化出自身
        • 然后获取instance属性的Class对象和propertyName属性调用NestedMethodProperty.initialized()
        // NestedMethodProperty.readObject()
        private void readObject(java.io.ObjectInputStream in)
            throws IOException, ClassNotFoundException {
            in.defaultReadObject();
               
            initialize(instance.getClass(), propertyName);
        }
        
      • 接下来看NestedMethodProperty.initialized(),因为此时instance是com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl,因此此时放进去了com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getOutputProperties()方法到NestedMethodProperty.getMethods数组中

        这段代码过长且与攻击调用链关系不大,故没有放到本博客中,若有兴趣可以查看相应源代码

        • 根据传入的beanClass和propertyName获取beanClass中propertyName的getter方法,放入NestedMethodProperty的getMethods属性
        • 根据传入的beanClass和propertyName获取beanClass中propertyName的setter方法,放入NestedMethodProperty的setMethod属性
    • 从GetField中取出BadAttributeValueExpException的val对象

    • 根据对象类型调用不同的方法原val对象转换为字符串对象,由于System.getSecurityManager()如果没有特别处理这里将会返回为null,因此会进入该分支,进而调用valObj.toString()

    //BadAttributeValueExpException.readObject()
    private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        ObjectInputStream.GetField gf = ois.readFields();
           
    	/* 如果未关闭IDEA调试功能自动调用toString(),将在这一句攻击生效------------*/
        Object valObj = gf.get("val", null);
    	/* 如果未关闭IDEA调试功能自动调用toString(),将在这一句攻击生效------------*/
           
        if (valObj == null) {
            val = null;
        } else if (valObj instanceof String) {
            val= valObj;
        } else if (System.getSecurityManager() == null
                   || valObj instanceof Long
                   || valObj instanceof Integer
                   || valObj instanceof Float
                   || valObj instanceof Double
                   || valObj instanceof Byte
                   || valObj instanceof Short
                   || valObj instanceof Boolean) {
            val = valObj.toString();
        } else { // the serialized object is from a version without JDK-8019292 fix
            val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName();
        }
    }
    
  3. 因为val值是PropertysetItem类型,因此接下来看PropertysetItem.toString()

    • 它会遍历当时通过addItemProperty()放入的所有propertyId,将propertyId传入getItemProperty()获取当时放入的含有恶意载荷的NestedMethodProperty对象
    • 接下来就是触发漏洞的关键几步,它获取到当初放入的NestedMethodProperty之后,触发了它的getValue()方法
    // addItemProperty() put id and property into it
    private HashMap<Object, Property<?>> map = new HashMap<Object, Property<?>>();
    // addItemProperty() put id into it
    private LinkedList<Object> list = new LinkedList<Object>();
       
    public String toString() {
        String retValue = "";
       
        for (final Iterator<?> i = getItemPropertyIds().iterator(); i.hasNext();) {
            final Object propertyId = i.next();
            retValue += getItemProperty(propertyId).getValue();
            if (i.hasNext()) {
                retValue += " ";
            }
        }
        return retValue;
    }
       
    public Property getItemProperty(Object id) {
        return map.get(id);
    }
    
  4. 接下来看NestedMethodProperty.getValue(),它通过反射调用了NestedMethodPropertyinstance属性getMethods中的全部方法,这其中就包含之前初始化时放进去的com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getOutputProperties()方法,因此出发了该方法

    public T getValue() {
        try {
            Object object = instance;
            for (Method m : getMethods) {
                object = m.invoke(object);
                if (object == null) {
                    return null;
                }
            }
            return (T) object;
        } catch (final Throwable e) {
            throw new MethodException(this, e);
        }
    }
    
  5. 接下来看com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getOutputProperties(),其调用过程与Click1调用链完全一样

    1. TemplatesImpl.getOutputProperties()方法内部调用同类内的方法TemplatesImpl.newTransformer()

      public synchronized Properties getOutputProperties() { 
          try {
              return newTransformer().getOutputProperties();
          }
          catch (TransformerConfigurationException e) {
              return null;
          }
      }
      
    2. 接下来看TemplateImpl.newTransformer(),它首先调用了getTransletInstance()方法获取一个Translet,我们构造的command实际上被封装在了一个Translet类内,因此这是实现攻击关键的最后几步

      public synchronized Transformer newTransformer()
          throws TransformerConfigurationException 
      {
          TransformerImpl transformer;
            
          transformer = new TransformerImpl(getTransletInstance(), _outputProperties, _indentNumber, _tfactory);
            
          /* Leave out lots of code */
          return transformer;
      }
      
    3. 接下来看TemplateImpl.getTransletInstance()

      • 检查TemplateImpl._name属性是否为空,如果为空就返回为空,获取Translet失败,因此如果使恶意载荷Translet顺利被执行,TemplateImpl._name必须不为空
      • 检查TemplateImpl._bytecodes属性是否为空,看源码中对该属性的注释可知这个byte[][]类型的值内存储着translet类和一些辅助类的字节码
      • 由源码的注释可知,TemplateImpl._class中存储了所有从TemplateImpl._bytecodes中通过TemplateImpl.defineTransletClasses()方法解析出来的Class对象
      • TemplateImpl._transletIndex存储了TemplateImpl.defineTransletClasses()在解析字节码过程中发现的org.apache.xalan.xsltc.runtime.AbstractTranslet的子类在TemplateImpl._class中的地址
      • AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance()通过反射调用了我们构造的Template中包含恶意指令的Translet的恶意构造方法,攻击成功

独立思考

1、为什么在IDEA中调试Vaadin1调用链时漏洞会被提前触发?

  • 在调试Vaadin1调用链时,我很长时间以为攻击成功点位于BadAttributeValueExpException.readObject()的如下位置,经过对该语句前后的调用代码全部调试之后仍然找不到调用点,甚至gf.get()方法最后一句也不会触发漏洞利用

    private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        ObjectInputStream.GetField gf = ois.readFields();
        // (IDEA导致误判)攻击成功(假)前 -------------------------
        Object valObj = gf.get("val", null);
        // (IDEA导致误判)攻击成功(假)后 -------------------------
        /* Leave out lots of code */
    }
    
  • 一次操作让我发现只要调试时从IDEA跳转回BadAttributeValueExpException.readObject()时,IDEA的Debugger中会显示当前方法内的变量,在不到1s的时间后漏洞会被触发,因此我猜测是IDEA的Debugger显示当前变量时通过反射调用了这些变量的toString()方法,恰好Vaadin1调用链依赖toString()方法触发,由此导致漏洞会被提前触发。

  • 因此尝试关闭IDEA Debugger自动调用toString()功能,再次调试,发现顺利解决漏洞被提前触发的问题。

    image-20210808142611769

产生过的疑问

  1. 为什么在IDEA中调试Vaadin1调用链时漏洞会被提前触发?