发布于周五 15:334天前 ## JDK原生反序列化漏洞的发现和绕过过程 最近的一个渗透项目中,通过nmap扫描了一个Jetty服务。 使用dirsearch扫描路径`/metrics/`,但是之后就找不到了。 从客户提供的Windows账户使用RDP登录,找到开放该端口的服务,使用Everything找到一个zip安装包,拖回来进行安装分析。 安装时,我注意到有一个设置代理的选项。将其设置为我打嗝的地址,并等待稍后可能发生的惊喜。 安装完成后,为其添加调试参数,然后将依赖的jar包导入到IDEA中进行调试。选择一些感兴趣的调试点开始调试。 ### 1.通过设置代理,发现反序列化端点(从客户端) 起初,我主要想找到`/metrics/` 背后的内容,所以我尝试了随机GET 和POST 请求。 请求响应完成后,并没有结束。我在burp中看到这样的记录。 序列化对象通过http 请求在网络上传输。 于是我先将POST body直接设置为ysoserial/Urldns.jar(检测小工具)的payload,但是dnslog中没有任何响应。我想如果反序列化成功的话,至少应该有检测Windows/Linux的记录。可以看到发送的检测payload没有反序列成功。 同时,我注意到burp的响应中有一个看起来像Exception的stackTrace。 (但是,屏幕截图显示了命令行输入错误的情况) 通过在客户端代码中设置适当的断点,找到发起HTTP请求的地方,复制下来,用Java代码构造请求。 ### 2.观察请求/响应,设置正确的数据格式,输出服务器异常 由于当时我已经有了客户端依赖的jar包,所以我在IDEA中搜索了返回的响应类:`ResponseMessage`。 在客户端找到这个类。所以readObject恢复响应对象,然后取出里面的Exception对象并打印stackTrace。 ```` ResponseMessage 响应消息=readResponse(connection.getInputStream()); System.out.println(responseMessage); responseMessage.getException().printStackTrace(); ```` 通过分析异常栈我们知道,对于客户端请求的序列化数据,服务端并不是直接将其反序列化为对象,而是先读取一个Integer类型,然后根据这个Integer值读取后续这个大小的字节,然后进行反序列化。 所以我在发送之前修改了它。 ### 3.反序列化小工具检测 最近正好在学习Jackson + Spring-aop这个小工具,所以就干脆先试试这个。庆幸的是,这次终于成功进入了反序列化利用流程。 虽然服务器没有spring-aop依赖。 此处不使用Urldns.jar 进行小工具检测。既然我们已经获得了客户端的依赖jar,那么我们就可以先分析一下这里有什么好东西。或许服务器端也是类似这样的。 经过分析,我们得到这样的结果: - jython(没有此依赖项) - Commons-Collections(版本是系列2,已修复,关键类不再可序列化) - Groovy(版本2.4.21,已修复) - commons-fileupload(版本1.3.3,已修复) - Commons-Beanutils(没有1 个系列,只有2 个) 注意,根据客户端的推测,服务器端使用的是commons-beanutils2。 https://github.com/melloware/commons-beanutils2 奇怪我以前没见过,github上0星。但快速浏览了一下,发现关键的小工具仍然存在,只是包名变了。 修改包名,在本地测试,发现可以用。 构建payload并发送给服务器后,报了这样的错误: ```` java.lang.UnsupportedOperationException: 启用Java 安全性时,将禁用对反序列化TemplatesImpl 的支持。这可以通过将jdk.xml.enableTemplatesImplDeserialization 系统属性设置为true 来覆盖。 在java.xml/com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.readObject(TemplatesImpl.java:270) `` 当我看到这个时,我只想将这个属性`jdk.xml.enableTemplatesImplDeserialization`覆盖为true。后来发现这其实是因为SecurityManager开启了。参考[本文](https://c0d3p1ut0s.github.io/%E6%94%BB%E5%87%BBJava%E6%B2%99%E7%AE%B1/)并尝试了各种绕过但失败。其策略是比较严格的。 ### 4.从反序列化到JNDI注入 分析不依赖Commons-Collections 的CommonsBeanutils2 小工具。 ```` java.util.PriorityQueue#readObject . java.util.Comparator#compare org.apache.commons.beanutils2.BeanComparator#compare org.apache.commons.beanutils2.PropertyUtils#getProperty . Xxx#getYyy()(条件:可序列化,使用空参数的getter方法) com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#getOutputProperties(现有) ```` TemplatesImpl这里使用的getOutputProperties是使用最广泛的getter,因为它存在于jdk中,并且可以自定义任何类,并且可以通过各种方式进行扩展(内存马等),但这条路目前被堵住了。您必须更改为单独的getter。我忘记在哪里看到的了。据说在各种dataSource中getConnection()然后使用jdbc url是很方便的。但经检测,服务器没有postgresql、mysql、h2等数据库驱动,只有Oracle。所以我放弃了jdbc url路径。 最后找到了`oracle.jdbc.rowset.OracleCachedRowSet#getConnection`,可以通过getter方法将反序列化转为JNDI注入。 从之前的报错中我们已经知道服务器使用了更高版本的jdk,所以我们要思考如何绕过JNDI注入限制。 绕过限制的方法主要有两种: - 1.使用反序列化 - 2.使用本地javax.naming.spi.ObjectFactory中的javax.naming.Reference来携带payload 但不考虑反序列化,否则JNDI注入将无法转移。 对象工厂?看看目标环境有什么?首先想到的当然是Tomcat自带的著名的`forceString`属性,依赖于`org.apache.naming.factory.BeanFactory`中的Reference。只需要找到一个接受单个String类型参数的方法就可以完成RCE。方法名称没有限制。千兰大师总结了很多(ELProcessor、Groovy等)。如果你很有钱的话就不用担心这个问题。您需要担心的是Tomcat版本是否在范围内。因为Tomcat7没有添加这个功能,而更高版本的Tomcat(9.0.63、8.5.79)则删除了这个功能。 [这里](https://bz.apache.org/bugzilla/show_bug.cgi?id=65736)讨论了删除`forceString` 函数的官方讨论, [这里](https://github.com/apache/tomcat/blob/9.0.63/java/org/apache/naming/factory/BeanFactory.java)可以看到9.0.63确实删除了forceString功能。 不放弃吗?只要在本地尝试一下就知道了。 服务器版本是什么? 直接发个包,让服务器报错: 9.0.64是修复版本。 因此,forceString 的简单路径不再可用。 可用选项: - 1.(反序列化getter)从反序列化getter转来的。既然不行,那试试其他的getter方法怎么样? - 2.(JNDI注入setter)这条路被堵住了,因为像xyz(String Payload)这样的方法不能使用,但是setAbc(String Payload)有没有好的恶意方法呢? ### 5. 没有forceString:反序列化getter/JNDI 注入setter? 通过研究2022年北京网络安全大会上@青蓝的《探索JNDI攻击》,我知道虽然forceString不能用于执行RCE,但仍然有其他方法可以执行敏感操作。 他在ppt中介绍了使用commons-configuration(2)/groovy + tomcat-jdbc.jar实现System.setProperty()的效果。 让我明白一下,这里的原理是首先找到一个特殊的ObjectFactory:`org.apache.tomcat.jdbc.naming.GenericNamingResourcesFactory`(tomcat-jdbc.jar)。与org.apache.naming.factory.BeanFactory(catalina.jar)相比,它支持调用类中的所有方法,包括静态方法,只要以set开头即可。并且`org.apache.naming.factory.BeanFactory`根据属性找到对应的setter方法(只有当属性abc存在时才会调用方法setAbc(String value))。如果我的理解有什么错误,请指正。 附加`org.apache.naming.factory.BeanFactory`: `org.apache.tomcat.jdbc.naming.GenericNamingResourcesFactory`: 另一个特殊类是“org.apache.commons.configuration2.SystemConfiguration”(commons-configuration2-*.jar) 或“org.apache.commons.configuration.SystemConfiguration”(commons-configuration-*.jar)。 其setSystemProperties方法可以设置系统属性,即 ```` 系统.setProperty() ```` setSystemProperties方法接收一个名为`fileName`的String类型参数,但实际上最终会构造成一个URL对象,所以不仅可以传入本地文件,还可以传入网络请求, 这样我们就可以在我们控制的Web服务器上放一个文件,内容是每行一个 ```` 键=值 ```` 想想之前TemplatesImpl的失败,不就是因为`jdk.xml.enableTemplatesImplDeserialization`系统属性导致的吗?我在这里发现了一个机会,所以我先改变了这个属性。 我满心欢喜地再次发送了有效负载,却再次收到此错误: 嗯,还不错。至少说明设置系统属性的小工具是有效的。 该系统属性已被更改,但仍然无法成功利用。还有其他值得更改的系统属性吗? ### 6. 应用程序启动后,System.setProperty() 能否绕过JNDI 注入限制? 回想千兰大师在ppt中提到的,可以尝试修改这两个系统属性作为JNDI注入缓解措施: ```` com.sun.jndi.ldap.object.trustURLCodebase=true com.sun.jndi.rmi.object.trustURLCodebase=true ```` 继续测试。 修改后发现使用JNDIExploit的`/Basic/Command/`依然不成功。于是我尝试在本地搭建环境,看看是否真的能成功。 我先使用Spring环境尝试一下(实际上是java-sec-code): 关键点是这个类:`com.sun.naming.internal.VersionHelper` 在加载Class时,会判断其私有静态最终属性“TRUST_URL_CODE_BASE”的值。只有当它为true 时,才能从远程URL 中拉取该类。当第一次加载此类时,将根据当时的系统属性“com.sun.jndi.ldap.object.trustURLCodebase”分配该值。因此,即使我们稍后在代码中将系统属性设置为true,“TRUST_URL_CODE_BASE”也将不再从系统属性中获取值。所以我们不必急于使用它。 从下图可以看出,spring-boot启动时会加载这个类: 起初,我以为只有spring-boot 才会出现这种情况。我们的目标环境是tomcat,实际情况可能不是这样。之后我又在tomcat环境下进行了测试。 发现使用JNDI注入而不绕过`/Basic/Command/`仍然失败。仍然落在这个“TRUST_URL_CODE_BASE”上。 为了让我们能够观察到该属性被赋值的时刻,调试参数也设置为`server=y,suspend=y`。 调用堆栈是: ````
创建帐户或登录后发表意见