我的上一篇文章,详细地讲述了gadgetinspector挖掘java反序列化利用链的原理,在明白了gadgetinspector的原理细节后,我们其实会发现它还存在着一部分的缺点:
gadgetinspector.PassthroughDiscovery.PassthroughDataflowMethodVisitor#visitMethodInsn
Set<Integer> passthrough = passthroughDataflow.get(new MethodReference.Handle(new ClassReference.Handle(owner), name, desc)); if (passthrough != null) { for (Integer passthroughDataflowArg : passthrough) { //判断是否和同一方法体内的其它方法返回值关联,有关联则添加到栈底,等待执行return时保存 resultTaint.addAll(argTaint.get(passthroughDataflowArg)); } }
可以想到,如果调用的是一个接口interface中定义的方法,那么,在gadgetinspector对其扫码期间,并不在被扫码程序的Runtime,那么,就没办法取确定实际上的实现method。
我看过有文章分析,可以通过查找该方法method的实现(接口的实现类中的方法)进行污染判断,不过,这种方式还是具有缺陷性,例如,这个接口存在着两个实现类,那么,从上述代码就可以看到,只能选择其中一个实现方法的污染结果进行判断。
gadgetinspector.CallGraphDiscovery.ModelGeneratorMethodVisitor#visitMethodInsn
//记录参数流动关系 //argIndex:当前方法参数索引,srcArgIndex:对应上一级方法的参数索引 discoveredCalls.add(new GraphCall( new MethodReference.Handle(new ClassReference.Handle(this.owner), this.name, this.desc), new MethodReference.Handle(new ClassReference.Handle(owner), name, desc), srcArgIndex, srcArgPath, argIndex));
如果调用的是一个接口interface中定义的方法,那么,在gadgetinspector对其扫码期间,并不在被扫码程序的Runtime,那么,也就没办法取确定实际上的实现method。
不过,对于这种缺陷,我们是不是可以考虑,通过列举所有的接口实现类出来,并把他们加入到调用链中?这个办法,有好处也有坏处,好处即是能全部Runtime时不管能不能执行到的实现都加进去了。而坏处也是因为这点,会造成路径爆炸,假如接口类有几十个实现类,如果把它们都加入到调用链中(不管Runtime到底是否能走到这个实现),造成的路径爆炸问题会非常严重。
而本篇文章,是围绕着第3、4点而讲,即讲述如何改造gadgetinspector,使它能够挖掘Fastjson的gadget chain
对于看过gadgetinspector,并且看懂了的小伙伴来说,能够发现,对于一种新序列化方式的gadget chain挖掘,gadgetinspector做到了很好的适配。
public interface GIConfig { String getName(); SerializableDecider getSerializableDecider(Map<MethodReference.Handle, MethodReference> methodMap, InheritanceMap inheritanceMap); ImplementationFinder getImplementationFinder(Map<MethodReference.Handle, MethodReference> methodMap, Map<MethodReference.Handle, Set<MethodReference.Handle>> methodImplMap, InheritanceMap inheritanceMap); SourceDiscovery getSourceDiscovery(); }
public class JacksonDeserializationConfig implements GIConfig { @Override public String getName() { return "jackson"; } @Override public SerializableDecider getSerializableDecider(Map<MethodReference.Handle, MethodReference> methodMap, InheritanceMap inheritanceMap) { return new JacksonSerializableDecider(methodMap); } @Override public ImplementationFinder getImplementationFinder(Map<MethodReference.Handle, MethodReference> methodMap, Map<MethodReference.Handle, Set<MethodReference.Handle>> methodImplMap, InheritanceMap inheritanceMap) { return new JacksonImplementationFinder(getSerializableDecider(methodMap, inheritanceMap)); } @Override public SourceDiscovery getSourceDiscovery() { return new JacksonSourceDiscovery(); } }
从上述代码中,可以看到,想要增加新的反序列化类型的挖掘,需要的是实现GIConfig接口,并通过实现类构造三个组件:
我们可以看看jackson对于这三个组件的具体实现是怎么样的:
public class JacksonSerializableDecider implements SerializableDecider { ... @Override public Boolean apply(ClassReference.Handle handle) { Boolean cached = cache.get(handle); if (cached != null) { return cached; } Set<MethodReference.Handle> classMethods = methodsByClassMap.get(handle); if (classMethods != null) { for (MethodReference.Handle method : classMethods) { //该类,只要有无参构造方法,就通过决策 if (method.getName().equals("<init>") && method.getDesc().equals("()V")) { cache.put(handle, Boolean.TRUE); return Boolean.TRUE; } } } cache.put(handle, Boolean.FALSE); return Boolean.FALSE; } }
很明显,jackson对于是否可被反序列化的判断就是是否存在无参构造方法。
public class JacksonImplementationFinder implements ImplementationFinder { private final SerializableDecider serializableDecider; public JacksonImplementationFinder(SerializableDecider serializableDecider) { this.serializableDecider = serializableDecider; } @Override public Set<MethodReference.Handle> getImplementations(MethodReference.Handle target) { Set<MethodReference.Handle> allImpls = new HashSet<>(); // For jackson search, we don't get to specify the class; it uses reflection to instantiate the // class itself. So just add the target method if the target class is serializable. if (Boolean.TRUE.equals(serializableDecider.apply(target.getClassReference()))) { allImpls.add(target); } return allImpls; } }
而对于判断是否有效实现类,也是借用到了JacksonSerializableDecider,通过它判断,只要具有无参构造方法,那么就是有效的实现类。
public class JacksonSourceDiscovery extends SourceDiscovery { @Override public void discover(Map<ClassReference.Handle, ClassReference> classMap, Map<MethodReference.Handle, MethodReference> methodMap, InheritanceMap inheritanceMap) { final JacksonSerializableDecider serializableDecider = new JacksonSerializableDecider(methodMap); for (MethodReference.Handle method : methodMap.keySet()) { if (serializableDecider.apply(method.getClassReference())) { if (method.getName().equals("<init>") && method.getDesc().equals("()V")) { addDiscoveredSource(new Source(method, 0)); } if (method.getName().startsWith("get") && method.getDesc().startsWith("()")) { addDiscoveredSource(new Source(method, 0)); } if (method.getName().startsWith("set") && method.getDesc().matches("\\(L[^;]*;\\)V")) { addDiscoveredSource(new Source(method, 0)); } } } } }
对于source搜索组件的逻辑,jackson的处理也非常简单,就是只要有无参构造方法或getter、setter,就能被标识为source起点类
最后,在实现了这三个组件之后,还有最后的一步,需要把他们的构造放到上述所讲的JacksonDeserializationConfig,也就是GIConfig的实现类中,并最后,放到配置库中ConfigRepository:
public class ConfigRepository { private static final List<GIConfig> ALL_CONFIGS = Collections.unmodifiableList(Arrays.asList( new JavaDeserializationConfig(), new JacksonDeserializationConfig(), new XstreamDeserializationConfig())); public static GIConfig getConfig(String name) { for (GIConfig config : ALL_CONFIGS) { if (config.getName().equals(name)) { return config; } } return null; } }
除了三个组件确定节点有效性以外,最终数据流是否能触发到slink,亦是需要进行判断的。而gadgetinspector是这么做的:
gadgetinspector.GadgetChainDiscovery#isSink
private boolean isSink(MethodReference.Handle method, int argIndex, InheritanceMap inheritanceMap) { if (method.getClassReference().getName().equals("java/io/FileInputStream") && method.getName().equals("<init>")) { return true; } if (method.getClassReference().getName().equals("java/io/FileOutputStream") && method.getName().equals("<init>")) { return true; } if (method.getClassReference().getName().equals("java/nio/file/Files") && (method.getName().equals("newInputStream") || method.getName().equals("newOutputStream") || method.getName().equals("newBufferedReader") || method.getName().equals("newBufferedWriter"))) { return true; } if (method.getClassReference().getName().equals("java/lang/Runtime") && method.getName().equals("exec")) { return true; } /* if (method.getClassReference().getName().equals("java/lang/Class") && method.getName().equals("forName")) { return true; } if (method.getClassReference().getName().equals("java/lang/Class") && method.getName().equals("getMethod")) { return true; } */ // If we can invoke an arbitrary method, that's probably interesting (though this doesn't assert that we // can control its arguments). Conversely, if we can control the arguments to an invocation but not what // method is being invoked, we don't mark that as interesting. if (method.getClassReference().getName().equals("java/lang/reflect/Method") && method.getName().equals("invoke") && argIndex == 0) { return true; } if (method.getClassReference().getName().equals("java/net/URLClassLoader") && method.getName().equals("newInstance")) { return true; } if (method.getClassReference().getName().equals("java/lang/System") && method.getName().equals("exit")) { return true; } if (method.getClassReference().getName().equals("java/lang/Shutdown") && method.getName().equals("exit")) { return true; } if (method.getClassReference().getName().equals("java/lang/Runtime") && method.getName().equals("exit")) { return true; } if (method.getClassReference().getName().equals("java/nio/file/Files") && method.getName().equals("newOutputStream")) { return true; } if (method.getClassReference().getName().equals("java/lang/ProcessBuilder") && method.getName().equals("<init>") && argIndex > 0) { return true; } if (inheritanceMap.isSubclassOf(method.getClassReference(), new ClassReference.Handle("java/lang/ClassLoader")) && method.getName().equals("<init>")) { return true; } if (method.getClassReference().getName().equals("java/net/URL") && method.getName().equals("openStream")) { return true; } // Some groovy-specific sinks if (method.getClassReference().getName().equals("org/codehaus/groovy/runtime/InvokerHelper") && method.getName().equals("invokeMethod") && argIndex == 1) { return true; } if (inheritanceMap.isSubclassOf(method.getClassReference(), new ClassReference.Handle("groovy/lang/MetaClass")) && Arrays.asList("invokeMethod", "invokeConstructor", "invokeStaticMethod").contains(method.getName())) { return true; } return false; }
代码有点多,但是不难看懂,其实就是对于一条执行链最末端的判断,基本都是判断是否属于某个类的某个方法,或者是否是某个类的子类、某个接口的实现类的某个方法。只要满足判断的特征,那么就证明这条链的可用性。
在添加Fastjson前,参考jackson三个组件,我们需要去了解Fastjson的一些特性:
通过阅读Fastjson的代码,在"@type"的处理部分
com.alibaba.fastjson.parser.ParserConfig#checkAutoType(java.lang.String, java.lang.Class<?>, int)方法调用后,会返回一个class类对象
clazz = config.checkAutoType(typeName, null, lexer.getFeatures());
紧接着,根据class类型获取反序列化工具类
ObjectDeserializer deserializer = config.getDeserializer(clazz);
对于大部分可利用的反序列化链,只要没有JSONType、JSONCreator注解,以及不是jre中一些特定的类、guava等,基本都会走到
com.alibaba.fastjson.parser.ParserConfig#createJavaBeanDeserializer
创建JavaBeanInfo
JavaBeanInfo beanInfo = JavaBeanInfo.build(clazz , type , propertyNamingStrategy ,false , TypeUtils.compatibleWithJavaBean , jacksonCompatible );
在build方法中,会对构造方法进行获取并判断
Constructor[] constructors = clazz.getDeclaredConstructors(); Constructor<?> defaultConstructor = null; if ((!kotlin) || constructors.length == 1) { if (builderClass == null) { defaultConstructor = getDefaultConstructor(clazz, constructors); } else { defaultConstructor = getDefaultConstructor(builderClass, builderClass.getDeclaredConstructors()); } }
static Constructor<?> getDefaultConstructor(Class<?> clazz, final Constructor<?>[] constructors) { if (Modifier.isAbstract(clazz.getModifiers())) { return null; } Constructor<?> defaultConstructor = null; for (Constructor<?> constructor : constructors) { if (constructor.getParameterTypes().length == 0) { defaultConstructor = constructor; break; } } if (defaultConstructor == null) { if (clazz.isMemberClass() && !Modifier.isStatic(clazz.getModifiers())) { Class<?>[] types; for (Constructor<?> constructor : constructors) { if ((types = constructor.getParameterTypes()).length == 1 && types[0].equals(clazz.getDeclaringClass())) { defaultConstructor = constructor; break; } } } } return defaultConstructor; }
综上代码,可以清晰的得到,Fastjson对于大部分这些类的反序列化时,优先通过获取无参构造方法实例化,如果没有无参构造方法,则选择一个参数(参数类型和自身一致)的构造方法。但如果都获取不到,那么就会走到下面的逻辑:
} else if (!isInterfaceOrAbstract) {
String className = clazz.getName();
String[] paramNames = null;
if (kotlin && constructors.length > 0) {
paramNames = TypeUtils.getKoltinConstructorParameters(clazz);
creatorConstructor = TypeUtils.getKoltinConstructor(constructors, paramNames);
TypeUtils.setAccessible(creatorConstructor);
} else {
for (Constructor constructor : constructors) {
...
paramNames = lookupParameterNames;
creatorConstructor = constructor;
}
}
}
可以看到,若是没有无参和一参(和自身class类型一致)构造方法的话,就会遍历构造方法,取最后一个。
因为任何的class都会存在着构造方法,那么也就是说,对于Fastjson,没有构造方法的限制。
PS:而关于注解部分,大部分第三方依赖都不会用到Fastjson的注解,这部分我们暂且不加入,因为gadgetinspector对于方法扫码的时候还没有做到存储注解,如果需要做这样的改造的话,需要做一部分的改造,这篇文章暂且不提。
因为Fastjson反序列化时,并不是直接反射Field设值,而是智能的提取出相应的setter、getter方法等,然后通过这些方法提取得到字段名称,接着进行设值
对于反序列化时会调用哪个特征的方法,由于网络上有一部分博文已经描述总结得很详细了,故而,我这边也不再贴代码了。
setter:
getter:
由上一节分析得出,只要存在构造方法,就能被Fastjson反序列化,因此,对于SerializableDecider的apply方法的逻辑实现,全部返回true就可以了。
public class FastjsonSerializableDecider implements SerializableDecider { public FastjsonSerializableDecider(Map<MethodReference.Handle, MethodReference> methodMap) { } @Override public Boolean apply(ClassReference.Handle handle) { return Boolean.TRUE; } }
考虑到Fastjson具有反序列化黑名单的机制,如果各位想要减少已被禁用链的输出,可以在这里加入黑名单。
根据前面列出的规则,创建Fastjson的SourceDiscovery
public class FastjsonSourceDiscovery extends SourceDiscovery { @Override public void discover(Map<ClassReference.Handle, ClassReference> classMap, Map<MethodReference.Handle, MethodReference> methodMap, InheritanceMap inheritanceMap) { final FastjsonSerializableDecider serializableDecider = new FastjsonSerializableDecider( methodMap); for (MethodReference.Handle method : methodMap.keySet()) { if (serializableDecider.apply(method.getClassReference())) { if (method.getName().startsWith("get") && method.getDesc().startsWith("()")) { if (method.getDesc().matches("\\(L[^;]*;\\)L.+?;")) { String fieldName = method.getName().charAt(3) + method.getName().substring(4); String desc = method.getDesc() .substring(method.getDesc().indexOf(")L") + 2, method.getDesc().length() - 1); MethodReference.Handle handle = new MethodReference.Handle( method.getClassReference(), "set" + fieldName, desc); if (!methodMap.containsKey(handle) && method.getDesc().matches("\\(L[^;]*;\\)Ljava/util/Collection;") || method.getDesc().matches("\\(L[^;]*;\\)Ljava/util/Map;") || method.getDesc().matches("\\(L[^;]*;\\)Ljava/util/concurrent/atomic/AtomicBoolean;") || method.getDesc().matches("\\(L[^;]*;\\)Ljava/util/concurrent/atomic/AtomicInteger;") || method.getDesc().matches("\\(L[^;]*;\\)Ljava/util/concurrent/atomic/AtomicLong;")){ addDiscoveredSource(new Source(method, 0)); } } } if (method.getName().startsWith("set") && method.getDesc().matches("\\(L[^;]*;\\)V")) { addDiscoveredSource(new Source(method, 1)); } } } } }
因为该Finder类,基本都是用到SerializableDecider决策者就可以了,那么这个实现就非常简单
public class FastjsonImplementationFinder implements ImplementationFinder {
private final SerializableDecider serializableDecider;
public FastjsonImplementationFinder(SerializableDecider serializableDecider) {
this.serializableDecider = serializableDecider;
}
@Override
public Set<MethodReference.Handle> getImplementations(MethodReference.Handle target) {
Set<MethodReference.Handle> allImpls = new HashSet<>();
// For jackson search, we don't get to specify the class; it uses reflection to instantiate the
// class itself. So just add the target method if the target class is serializable.
if (Boolean.TRUE.equals(serializableDecider.apply(target.getClassReference()))) {
allImpls.add(target);
}
return allImpls;
}
}
public class FastjsonDeserializationConfig implements GIConfig {
@Override
public String getName() {
return "fastjson";
}
@Override
public SerializableDecider getSerializableDecider(Map<MethodReference.Handle, MethodReference> methodMap, InheritanceMap inheritanceMap) {
return new FastjsonSerializableDecider(methodMap);
}
@Override
public ImplementationFinder getImplementationFinder(Map<MethodReference.Handle, MethodReference> methodMap,
Map<MethodReference.Handle, Set<MethodReference.Handle>> methodImplMap,
InheritanceMap inheritanceMap) {
return new FastjsonImplementationFinder(getSerializableDecider(methodMap, inheritanceMap));
}
@Override
public SourceDiscovery getSourceDiscovery() {
return new FastjsonSourceDiscovery();
}
}
public class ConfigRepository {
private static final List<GIConfig> ALL_CONFIGS = Collections.unmodifiableList(Arrays.asList(
new JavaDeserializationConfig(),
new JacksonDeserializationConfig(),
new XstreamDeserializationConfig(),
new FastjsonDeserializationConfig()));
public static GIConfig getConfig(String name) {
for (GIConfig config : ALL_CONFIGS) {
if (config.getName().equals(name)) {
return config;
}
}
return null;
}
}
因为Fastjson反序列化RCE很多的打法,基本都是jndi-lookup实现,但是我看到gadgetinspector中并没有该slink的判断,因此,加入该slink的判断,以对其进行优化
gadgetinspector.GadgetChainDiscovery#isSink
在该方法末尾添加jndi-lookup判断即可
if (inheritanceMap.isSubclassOf(method.getClassReference(), new ClassReference.Handle("javax/naming/Context"))
&& method.getName().equals("lookup")) {
return true;
}
至此,gadgetinspector的改造就完成了,那么接下来,我们以一个已有gadget chain的jar进行扫码挖掘,测试一下效果
例:HikariCP-3.4.1.jar
扫码挖掘结果:
sun/usagetracker/UsageTrackerClient.setup(Ljava/io/File;)V (1)
java/io/FileInputStream.<init>(Ljava/io/File;)V (1)
org/apache/log4j/jmx/LayoutDynamicMBean.setAttribute(Ljavax/management/Attribute;)V (1)
java/lang/reflect/Method.invoke(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object; (0)
com/sun/org/apache/xml/internal/serializer/ToStream.setOutputFormat(Ljava/util/Properties;)V (1)
com/sun/org/apache/xml/internal/serializer/ToStream.init(Ljava/io/Writer;Ljava/util/Properties;ZZ)V (2)
com/sun/org/apache/xml/internal/serializer/CharInfo.getCharInfo(Ljava/lang/String;Ljava/lang/String;)Lcom/sun/org/apache/xml/internal/serializer/CharInfo; (0)
com/sun/org/apache/xml/internal/serializer/CharInfo.<init>(Ljava/lang/String;Ljava/lang/String;Z)V (1)
java/net/URL.openStream()Ljava/io/InputStream; (0)
com/sun/management/jmx/TraceListener.setFile(Ljava/lang/String;)V (1)
java/io/FileOutputStream.<init>(Ljava/lang/String;Z)V (1)
com/zaxxer/hikari/HikariConfig.setMetricRegistry(Ljava/lang/Object;)V (1)
com/zaxxer/hikari/HikariConfig.getObjectOrPerformJndiLookup(Ljava/lang/Object;)Ljava/lang/Object; (1)
javax/naming/InitialContext.lookup(Ljava/lang/String;)Ljava/lang/Object; (1)
org/apache/log4j/jmx/AppenderDynamicMBean.setAttribute(Ljavax/management/Attribute;)V (1)
org/apache/log4j/jmx/AppenderDynamicMBean.getAttribute(Ljava/lang/String;)Ljava/lang/Object; (1)
java/lang/reflect/Method.invoke(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object; (0)
org/apache/log4j/varia/LevelMatchFilter.setLevelToMatch(Ljava/lang/String;)V (1)
org/apache/log4j/helpers/OptionConverter.toLevel(Ljava/lang/String;Lorg/apache/log4j/Level;)Lorg/apache/log4j/Level; (0)
java/lang/reflect/Method.invoke(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object; (0)
org/apache/log4j/jmx/LayoutDynamicMBean.setAttribute(Ljavax/management/Attribute;)V (1)
org/apache/log4j/jmx/LayoutDynamicMBean.getAttribute(Ljava/lang/String;)Ljava/lang/Object; (1)
java/lang/reflect/Method.invoke(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object; (0)
org/apache/log4j/jmx/AppenderDynamicMBean.setAttribute(Ljavax/management/Attribute;)V (1)
java/lang/reflect/Method.invoke(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object; (0)
可以看到,其中我们新加入的jndi-lookup的slink,顺利的挖掘到一个可用的gadget chain:
com/zaxxer/hikari/HikariConfig.setMetricRegistry(Ljava/lang/Object;)V (1)
com/zaxxer/hikari/HikariConfig.getObjectOrPerformJndiLookup(Ljava/lang/Object;)Ljava/lang/Object; (1)
javax/naming/InitialContext.lookup(Ljava/lang/String;)Ljava/lang/Object; (1)
当然,这个gadget chain早在1.2.60就被黑名单禁了,哈哈!还有就是,文章难免某个地方会搞错,希望各位dalao阅读之后可以不吝指教。
新年将至,也祝各位小伙伴能挖到好洞,过一个愉快的肥年,谢谢!