Suo5 - 高性能 HTTP 代理隧道

去年 HW 时,我有幸做了一些后勤保障工作,主要是工具类的支持和部分漏洞武器化的工作。在用到某 webshell 的正向代理功能时,其速度和稳定性实在令我抓狂,经常用着用着就断掉了,多级代理更是不敢想。终于在一次莫名其妙崩掉后且无法恢复后,我决定花点时间写一个新的 HTTP Socks5 代理工具,这个工具就是 Suo5 的雏形。

基本原理

HTTP 代理隧道是什么,简单来说就是当你拥有了一个 webshell,想进一步探索内网时,可以借助该 webshell 构建一个 socks5 代理,使流量经由该 webshell 发送到内网。实现这一功能的经典的工具是 reGeorgNeo-reGeorg,后者是前者的继承和延续,做了比较多拓展和更新,我们就以它为例看下基本原理。

regorgy.png

使用时需要先上传一个服务端 tunnel.jsp 到目标 Web 服务中,在这里为了好听就叫他 ProxyBridge(PB) ,然后在本机启动一个客户端去连接这个 tunnel.jsp,连接成功后就在本机开启了一个 Sock5 服务,可以借助该服务访问 Inner Server 中做进一步测试。

要实现图中的这个通信链路,Socks5Handler(SH)需要实现一个 socks5 服务并监听,当有连接到来时,将连接信息封装为 HTTP 请求发送给 PB中,PB解析出要连接的目标然后尝试与目标建立 TCP 连接,并把连接状态通过 HTTP 响应反馈给 SH。当用户写入数据时,SH->PB->InnserServer,当远端返回数时 InnerServer->PB->SH。如此循环往复,一条虚拟的 TCP 连接就构建出来了。这条通信链路关键的点在于 SHPB之间的通信,本质上是在用 HTTP 包来构建 TCP 数据流,具体的只需要通信两端处理好连接的建立、数据交换和连接断开即可。但由于中间链路使用了 HTTP 请求响应来中转流量,实际使用过程往往感觉延迟比较高,速度也不太理想。

单车变摩托

去年 Beichen 师傅提出了一种新的方式来改善上述问题,具体可以看下这篇文章 Chunk-Proxy:仅需一条http请求创建的Socks代理隧道 ,这里总结一下结论: 利用 HTTP/1.1 的 Transfer-Encoding: Chunked 模式来发送 Body,如果发送端不主动关闭 Body,在获取到服务端响应后可以继续发送 Body 数据,继续获取服务器响应,如此往复是不是很像裸的 TCP 通信!

wireshark.jpg

这种通信模式相当于中间链路也是直连的,在使用体验上近似于 FRP 这类工具开的 Socks5 代理。

chunked.png

然而这个工具的实现有个比较大的问题是无法在反向代理的场景使用,最常见的 Nginx + Tomcat 的场景就无法使用。原因在于 Nginx 在处理 Chunked-Encoding类型的 Body 时会缓存 Body,直到发送端写完再把完整的 Body 发送给上游服务器,见 nginx-proxy_request_buffering

When HTTP/1.1 chunked transfer encoding is used to send the original request body, the request body will be buffered regardless of the directive value unless HTTP/1.1 is enabled for proxying.

与之相关的两个配置默认值是 proxy_request_buffering onproxy_http_version 1.0,基本没人会改这两个默认值,这几乎给这个技术在这个场景下判了死刑。

还有办法抢救一下吗?

有,这便是 Suo5做的事。

Suo5

尝试解决这个问题时,我在想能不能找个中间状态。如果不能在一个 HTTP 请求里实现双向 TCP 流式通信,那能不能退而求其次,在一个 HTTP 请求里实现单向 TCP 流式通信?想想我们经常用的文件下载功能,是不是就是类似的情况,当下载一个未知大小的文件时,服务器返回的 Body 格式就是一个数据流! 上面说的 Nginx 配置是针对 HTTP 请求的,HTTP 响应会不会也有类似的配置把这个想法 Cache 住呢。果然还有个 proxy_buffering 的配置,默认情况就会把上游服务的响应缓存后再发给用户。不过我发现这个可以绕过,在这个配置里有个说明:

Buffering can also be enabled or disabled by passing “yes” or “no” in the “X-Accel-Buffering” response header field.

也就是说如果给响应里增加一个 X-Accel-Buffering: no 的响应头,就能够改掉当前响应的缓存行为变成直接发给用户!响应的问题解决了,请求的问题从前面分析来看是无解的,于是我把 reGeorg 的那种短链接发送请求的方式移植过来了,形成了这样的通信方式: suo5.png

  • 写数据通过 TCP HTTP 的请求拆包发送,借助 Keepalive 机制也能获得很不错的性能以及较低延迟
  • 读数据通过 Chunked 的响应包实现,效果上类似 TCP 直连

我称这种通信模式为 Suo5 的 半双工 模式。当然, Suo5 也完美支持 全双工模式。更进一步的,Suo5 会在连接时自动判断当前有没有反代,如果没有则使用 全双工,有反代就 fallback 到 半双工模式,自动化拉满。 具体实现上也遇到一些问题,比较大的坑是 Go 的标准库 net/http需要发送完 Body 才能去读取响应,实在是太标准了。我被迫改了一个新的 http 库 zema1/rawhttp 来解决这个问题,没有连接池,没有标准化,没有默认处理行为,所见即所得,很适合用来做安全测试。其他的点感兴趣的直接看代码吧,不在这说了。

GUI

我曾是一个忠实的终端用户,感觉命令行程序才是黑客该用的东西。在用过一些图形化的安全工具后,我渐渐发现图形化才是生产力,真香啊!于是,我想给这个小工具做一个图形化界面再发出来。调研了一圈现在热门的跨平台图形开发技术,Electron 似乎仍是最好的选择。但我不喜欢 Electron,它又慢又重又大,写不好还容易有 RCE,被我果断放弃。 之前写 Rust 日报时,有注意到 tauri 这个项目,是 Rust 生态里最火的图形化开发项目。它通过调用系统的 WebView 来做前端渲染,而不是像 Electron 自带一个浏览器,启动一个程序耗费的资源和开启一个浏览器标签页类似。这样既能使用现代化的前端开发技术来构建界面,编译出的二进制又非常小,启动速度和开发体验也很理想,着实令人心动。

wails.webp

于是我找了 Go 里面类似原理的项目 wails,在这个项目的帮助下,我写了一个小巧又跨平台的 GUI 程序。其唯一的不足是兼容性差一些,仅 Windows11 和 MacOS 默认内置了 Webview,其他系统如果没装过类似的软件首次使用会弹窗下载一个依赖,安装后即可使用。GUI 版本的功能与命令行版完全一致,大家可以各取所需。

gui.jpg

这个工具我打算持续维护下去,直到我满意为止。地址:https://github.com/zema1/suo5


注1:suo5 的意思是 一把梭 socks5,感谢 swing 和 leommxj 两位师傅帮忙起的项目名