作者: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 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1846/