frp改造3-CDN(好起来了)

    渗透测试 lz520520 4年前 (2020-11-22) 4058次浏览

    0x00 前言

    在之前实现了cs的域前置,就起了心思把代理通道也做成域前置的,有人可能会说cs不是自带socks通道吗,但使用过的应该也知道,那个通道仅仅是凑合能用的层面,毕竟不是专门做通道的,而在多款代理工具中测试,发现frp是较稳定的一款工具,所以就有了frp改造的过程,这里做了三次尝试,前两次失败后本来已经放弃了,后续受大佬指点发现了新的idea从而解决了frp域前置的问题。

     

    0x01 第一次尝试

    应该分析的稀烂,就不贴图了,因为要域前置,考虑到的就是http协议封装,第一次是想在所有传输数据包前封装http协议,发现改动frp项目的send/recv不管用,他还调用第三方库做最外层的封装,导致需要改动第三方库,当时没太多精力分析就放弃了。

     

    0x02 第二次尝试

    因为不甘心,觉得还有实现的可能性然后就进一步分析frp的通信数据包,之前也提到最外层会封装一个第三方库协议,没辙,就只能去分析呗。
    目前测试下来,已找到所有数据最终的发送接口
    D:\Go\code\pkg\mod\github.com\hashicorp\yamux@v0.0.0-20181012175058-2f1d1f20f75d\session.go
    waitForSendErr函数,通过channel传入发送结构体 s.sendCh <- xxxx, 该函数发送数据存在body部分,用于传输具体数据

    frp改造3-CDN(好起来了)

    sendNoWait函数,仅往channel里写入头部,这个应该是用于空闲时心跳维持

    frp改造3-CDN(好起来了)

     

    而最终提取数据发送的是函数是send()

    frp改造3-CDN(好起来了)

    所以要加上http header,就在最终位置加就行了。

    是怎么跟踪到上述函数的,因为golang里针对IO读写提供了interface,实现了Writer接口的Write和Reader接口的Read方法即可,所以在stream.go函数里找到这两个方法,跟踪下去就到了上面,通过channel来等待发送数据。

    frp改造3-CDN(好起来了)

    frp改造3-CDN(好起来了)

    还需要解决的问题是,读取接口。最终定位到session.go的recvLoop()方法,这里的s.bufRead实际为net.TCPConn,上面send方法最终里面也是通过net.TCPConn来发送的

    frp改造3-CDN(好起来了)

     

    做了一个小测试,在yamux包的session.go里添加几个函数,在send里调用AddPrefix函数提前加上前缀,在recvLoop里调用RemovePrefix去除,来看看数据交互是否有问题,不过CDN,测试下来,一切正常。
    如下所示,因为考虑过CDN头部长度会变,没法很好过滤头部,所以使用banner来作为标识,接收时移除banner之前所有头部即可,当然还需要重新计算原始数据包长度,更新Content-Length,否则会导致过CDN被截断。

    frp改造3-CDN(好起来了)

    这里用于判断是frps还是frpc,因为模拟的数据包不一样,一个是http请求一个是http响应

    frp改造3-CDN(好起来了)

    最终测试CDN可以转发到vps上,但后续连接会报错,分析下来发现流量不是一个会话了,会存在问题,没法让每次http请求都是在一个会话上。
    哎,分析代码,分析数据包构造了半天,最终还是失败了。

    0x03 第三此尝试

    时隔一个多月,这个方案突然有了转机,原来一直考虑的是在数据外封装一层http协议来转发,但经过CDN转发会存在会话不一致的问题,因为本身也只是模拟http协议,没法完全实现http会话功能等。
    但发现frp除非支持TCP传输之外,还支持websocket传输,websocket是一种持久化的协议,他是基于http的补充。
    Websocket只需要一次HTTP握手,后续整个通讯过程都是建立在一次连接/状态中,交换的数据不再需要http头,避免了HTTP的非状态性,服务端会一直知道你的信息,直到你关闭请求,这样就解决了nginx等代理服务器要反复解析HTTP协议,还要查看identity info的信息。同时由客户主动询问,转换为服务器(推送)有信息的时候就发送(也就是可以从server端主动向client推消息,当然客户端还是可以主动发送信息过来的)
    参考链接:https://www.zhihu.com/question/20215561

    说了这么多,cf是支持websocket的,并测试无改造程序也可以过CDN建立通道,但不太灵活,因为无法手动指定CDN IP,必须使用域名才能过CDN。
    为了更灵活配置,就开始动手改造,思路是配置里指定CDN IP,额外添加CDN域名设置选项,用于填充第一次的http请求头。

    client配置文件[common]对应结构体新增一个websocket_domain参数,在读取配置文件的时候,判断该值是否设置,如果无,则该值设置为server_addr,这样可以兼容对websocket_domain参数的操作不会影响原本的代码。
    然后在ConnectServerByProxyWithTLS函数内调用的函数和自身调用它的函数都添加websocket_domain参数
    pkg/util/net/conn.go

    frp改造3-CDN(好起来了)

    改动还涉及第三方库的调整,这个主要是因为websocket封装不是自实现的。
    D:\environ\Go\code\pkg\mod\golang.org\x\net@v0.0.0-20200520004742-59133d7f0dd7\websocket\hybi.go

    frp改造3-CDN(好起来了)

    调用堆栈

    frp改造3-CDN(好起来了)

    0x04 验证

    参考配置

    根据以上配置成功经过CDN连接server,并测试通道代理无问题。

    frp改造3-CDN(好起来了)

    frp改造3-CDN(好起来了)

    然后还有个地方,他请求的是URI是frp,这个太明显了,必须改。
    pkg/util/net/websocket.go

    frp改造3-CDN(好起来了)

    接下来,还有一个需求,原来一直考虑配置文件问题,然后改成命令行,让配置文件不落地,其实命令行特征也很明显,只是程序停止后不会有痕迹,但如果运行过程中人工检查进程,一目了然,并且进程监控工具也能看得很清楚。
    所以我考虑还是用回配置文件,至少不看配置文件是没啥特征,只要读取后删除配置文件就好了呀,这个就很简单,我多添加了一个配置文件参数delete,用于判断是否自动删除配置文件。

    0x05 结语

    这次frp域前置测试断断续续持续了好久,还是在于对通道协议理解太少,使用frp过程中,才慢慢优化了,从原来的tcp转变为kcp,最后才发现还有websocket。也分析了frp数据包的交互过程,算是对这款工具有了新的认识吧,这边再总结几点
    1. websocket过CDN,不支持http代理,如果在需要http代理才能出网的场景下,请使用tcp协议。
    2. TCP模块的传输协议分为tcp/kcp/websocket,推荐以下几个场景
    websocket:首推websocket,支持过CDN隐藏IP,并且websocket长连接特性。
    kcp: 目标环境延迟大,使用KCP(基于UDP)可有效降低延迟,且UDP协议可能会更隐蔽些。
    tcp: 需要http代理才能出网的环境
    3. 推荐使用http和https默认端口外联,根据实战经验,一些特殊场景这两个端口是成功率最高的。

     

     

    0x06 补充 Websocket

    一、WebSocket是HTML5出的东西(协议),也就是说HTTP协议没有变化,或者说没关系,但HTTP是不支持持久连接的(长连接,循环连接的不算)

    首先HTTP有1.1和1.0之说,也就是所谓的keep-alive,把多个HTTP请求合并为一个,但是Websocket其实是一个新协议,跟HTTP协议基本没有关系,只是为了兼容现有浏览器的握手规范而已,也就是说它是HTTP协议上的一种补充可以通过这样一张图理解

    frp改造3-CDN(好起来了)

    二、Websocket是什么样的协议,具体有什么优点首先,Websocket是一个持久化的协议,相对于HTTP这种非持久的协议来说。简单的举个例子吧,用目前应用比较广泛的PHP生命周期来解释。
    1) HTTP的生命周期通过Request来界定,也就是一个Request 一个Response,那么在HTTP1.0中,这次HTTP请求就结束了。在HTTP1.1中进行了改进,使得有一个keep-alive,也就是说,在一个HTTP连接中,可以发送多个Request,接收多个Response。但是请记住 Request = Response , 在HTTP中永远是这样,也就是说一个request只能有一个response。而且这个response也是被动的,不能主动发起。

    效果就是使用HTTP协议进行握手建立websocket会话,而后续的所有流量就不再需要发送http头部请求,直接发送socket数据即可,可保持长连接。

    https://www.zhihu.com/question/20215561

     


    Security , 版权所有丨如未注明 , 均为原创丨
    转载请注明原文链接:frp改造3-CDN(好起来了)
    喜欢 (13)