作者:Glassy
原文鏈接:https://g1asssy.com/2022/03/11/fuzz/

引言

安全防護產品在進行防護的時候是需要對流量中的數據進行處理的,同樣,被攻擊的應用也需要處理這些數據以保證業務的正常進行,然而在很多情況下,安全產品處理數據流的框架和應用處理數據流的框架往往不同,在針對常規數據方面,當然不會出現問題,然而一旦被防護應用的數據處理框架的兼容性大于安全產品數據處理框架的兼容性,那么就會出現這么一種情形:攻擊者提交的數據被應用正常解析,而安全產品解析失敗,這樣惡意的流量就會成功繞過安全產品進入被攻擊的應用。因此尋求目標機器與安全防護產品在數據處理能力上的兼容性差異便會成為突破安全產品的一種卓爾有效的手段。

實戰講解

基于json解析兼容性的示例

以Java為例,現階段市面上主流的處理json字符串的框架有fastjson、gson、jackson三種。常見的WAF為了保證對于各種語言開發應用的兼容性,一般會使用自寫的json解析器。由于筆者在進行fuzz的過程中發現,gson和jackson在兼容性方面幾乎沒有任何差異,猜測這兩種框架對于json數據的兼容應該代表著主流json解析器的能力,而fastjson在我的印象中一向擁有更強大的兼容性,所以產生想法,是否可以在一段正常的json數據中各個位置插入不同的字符使得gson(它代表著主流gson解析器)進行json解析時候報錯,而fastjson能夠正常解析,那么在對應用進行攻擊的時候,一旦發現應用使用了fastjson框架,便能構造出WAF認不出來但應用可以認出來的數據,從而突破WAF的防御,代碼思路如下

1、寫一個正常的json字符串。
2、在它的各種位置嘗試插入 1-65535 中的每個字符,生產一個非常規json。
3、將非常規json交給gson處理,報錯。
4、交給fastjson處理,正常,將該json記錄下來。

核心代碼如下

public static void jsonFuzz(String demo) {
        CheckFunc func = new CheckFunc() {
            @Override
            public void check(String origin, String fuzzData, char fuzzChar) {
                //由于打不風安全產品都會對字符串做trim處理,因此,如果fuzz的字符和原字符trim結果相同,基本沒什么意義
                if (!origin.trim().equals(fuzzData.trim())) {
                    try {
                        Entity entity = JSONObject.parseObject(fuzzData, Entity.class);
                        //********該測試用例中主要用來發掘fastjson兼容而gson不兼容的特性,但有些安全產品使用自研json解析器,json兼容性更差,則可以注釋掉下面代碼,直接fuzz出fastjson的全部特性
                        try {
                            Gson gson = new Gson();
                            Entity gsonEntity = gson.fromJson(fuzzData, Entity.class);
                            if (gsonEntity.toString().equals("Entity{num=666, content='test'}")) {
                                //如果gson能解了,就代表這個特性gson也是可以兼容的,說明這個fuzzData是無效數據,因為大家都能解,安全產品就具備對這種payload的防御能力了,所以直接return
                                return;
                            }
                        } catch (Exception ignored) {
                            //如果gson報錯了,我們直接忽略它,讓程序繼續往下走,因為我們期待的數據就是fastjson能解,而gson解不出來的數據
                        }
                        //**************************gson-end***************
                        if (entity.toString().equals("Entity{num=666, content='test'}")) {
                            System.out.println("charNum: " + (int) fuzzChar + "|char: " + fuzzChar + "|content: " + fuzzData);
                        }
                    } catch (Exception exception) {
                        if (exception instanceof JSONException) {

                        } else {
                            exception.printStackTrace();
                        }
                    }
                }
            }
        };
        int length = demo.length();
        for (int i = 0; i < length; i++) {
            System.out.println("*************************插入字符位置:" + i + "*************************");
            Utils.doFuzz(demo, i, HandleType.INSERT, func);
        }
        for (int i = 0; i < length; i++) {
            System.out.println("*************************替換字符位置:" + i + "*************************");
            Utils.doFuzz(demo, i, HandleType.REPLACE, func);
        }
    }

最終得出結果很多,歡迎大家去筆者github中拉取代碼跑一下看一看,這里舉出比較有代表性的幾種寫法,

*************************插入字符位置:1*************************
charNum: 12|char: |content: {(這里有一個ascii為12的字符)"num":666,"content":"test"}
charNum: 44|char: ,|content: {,"num":666,"content":"test"}
*************************插入字符位置:10*************************
charNum: 40|char: (|content: {"num":666(,"content":"test"}
charNum: 41|char: )|content: {"num":666),"content":"test"}
charNum: 43|char: +|content: {"num":666+,"content":"test"}
charNum: 44|char: ,|content: {"num":666,,"content":"test"}
charNum: 45|char: -|content: {"num":666-,"content":"test"}
charNum: 59|char: ;|content: {"num":666;,"content":"test"}
charNum: 66|char: B|content: {"num":666B,"content":"test"}
charNum: 76|char: L|content: {"num":666L,"content":"test"}
charNum: 83|char: S|content: {"num":666S,"content":"test"}
charNum: 91|char: [|content: {"num":666[,"content":"test"}
charNum: 93|char: ]|content: {"num":666],"content":"test"}
charNum: 123|char: {|content: {"num":666{,"content":"test"}

通過以上特性去構造sql注入的payload,很多安全產品都無法成功解析數據,取得value,從而失去了sql注入的檢測能力。(考慮到不少安全產品對于json采用流式解析,因此導致json異常的特殊字符一定要放在注入payload之前)

結論:Gson相對而言是一種功能較為強大的json解析器了,json兼容性其實算是比較優秀,在筆者的測試過程中也發現存在不少json是gson能解而市面上的WAF解不了了,大家可以去探索試驗一下。

基于數字字符兼容性的示例

這個特性同樣來自于Java,并且是一個大家耳熟能詳的函數,

Integer.parseInt(str)

筆者在對該函數進行fuzz的過程中,發現Java的parseInt是支持異形字的,測試代碼如下,

    public static void getParseIntFuzzResult() {
        for (char c = 0; c < 65535; c++) {
            String str = Character.toString(c);
            try {
                for (int num : numList) {
                    if (Integer.parseInt(str) == num && !String.valueOf(num).equals(str)) {
                        System.out.println(num + ":->charNum: " + (int) c + "|char: " + str);
                    }
                }
            } catch (Exception ignored) {

            }
        }
    }

測試結果如下(結果太長,同樣只截取部分)

那么這個場景在哪里應用呢,同樣是fastJson,審計fastjson的源碼,會發現它支持unicode編碼,并且在處理unicode編碼的使用用到了Integer.parseInt

代碼位置:com.alibaba.fastjson.parser.JSONLexerBase

 case 'u':
                    char c1 = this.next();
                    char c2 = this.next();
                    char c3 = this.next();
                    char c4 = this.next();
                    int val = Integer.parseInt(new String(new char[]{c1, c2, c3, c4}), 16);
                    hash = 31 * hash + val;
                    this.putChar((char)val);
                    break;

我們取一條最常見的json字符串,看一看它的變形能到什么程度, 同樣通過異形字的變形,無論在反序列的的利用上還是sql注入的利用上繞過安全產品都可以取得一個比較不錯的效果。

總結

通過兼容性突破安全產品的思路和場景當然遠不止這些,我相信在類似于xml解析中可能也會存在類似問題,文章權當是拋磚引玉引出一種思路,歡迎優秀的白帽子們深入探索,末尾給出本次試驗中的項目代碼方便各位調試,查看結果。


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1846/