projectdiscover之naabu 端口扫描器源码学习
2021-02-12 00:28:47 Author: mp.weixin.qq.com(查看原文) 阅读量:313 收藏

大家新年快乐~ 边看春晚边看代码,写了一篇学习记录~

ProjectDiscovery组织开源了很多自动化扫描的内部工具和研究,例如 subfinder被动子域名发现工具nuclei基于模板的可配置快速扫描工具naabu端口扫描器dnsprobe dns解析器httpx多功能http工具包,它们都是基于 Go语言编写,并且在实际渗透中有极大的作用。我非常喜欢这个组织开源的软件,它也是我学习 Go语言的动力之一,所以计划写一个系列文章来研究下它们的代码。

介绍

naabu的项目地址是:https://github.com/projectdiscovery/naabu

几个特性:

  • 基于syn/connect两种模式扫描

  • 多种输入类型支持,包括HOST / IP / CIDR表示法。

  • 自动处理多个子域之间的重复主机

  • Stdinstdout支持集成到工作流中

  • 易于使用的轻量级资源

  1. naabu -host hackerone.com

  2. __

  3. ___ ___ ___ _/ / __ __

  4. / _ \/ _ \/ _ \/ _ \/ // /

  5. /_//_/\_,_/\_,_/_.__/\_,_/ v2.0.3

  6. projectdiscovery.io

  7. [WRN] Use with caution. You are responsible for your actions

  8. [WRN] Developers assume no liability and are not responsible for any misuse or damage.

  9. [INF] Running SYN scan with root privileges

  10. [INF] Found 4 ports on host hackerone.com (104.16.100.52)

  11. hackerone.com:80

  12. hackerone.com:443

  13. hackerone.com:8443

  14. hackerone.com:8080

扫描方式

扫描相关的代码在 v2/pkg/scan目录

cdn check

顾名思义,跟踪一下,发现cdn检查调用的是 github.com/projectdiscovery/cdncheck中的项目。

通过接口获取一些CDN的ip段,判断ip是否在这些ip段中

  1. // scrapeCloudflare scrapes cloudflare firewall's CIDR ranges from their API

  2. func scrapeCloudflare(httpClient *http.Client) ([]string, error) {

  3. resp, err := httpClient.Get("https://www.cloudflare.com/ips-v4")

  4. if err != nil {

  5. return nil, err

  6. }

  7. defer resp.Body.Close()

  8. data, err := ioutil.ReadAll(resp.Body)

  9. if err != nil {

  10. return nil, err

  11. }

  12. body := string(data)

  13. cidrs := cidrRegex.FindAllString(body, -1)

  14. return cidrs, nil

  15. }

  16. // scrapeIncapsula scrapes incapsula firewall's CIDR ranges from their API

  17. func scrapeIncapsula(httpClient *http.Client) ([]string, error) {

  18. req, err := http.NewRequest(http.MethodPost, "https://my.incapsula.com/api/integration/v1/ips", strings.NewReader("resp_format=text"))

  19. if err != nil {

  20. return nil, err

  21. }

  22. req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

  23. resp, err := httpClient.Do(req)

  24. if err != nil {

  25. return nil, err

  26. }

  27. defer resp.Body.Close()

  28. data, err := ioutil.ReadAll(resp.Body)

  29. if err != nil {

  30. return nil, err

  31. }

  32. body := string(data)

  33. cidrs := cidrRegex.FindAllString(body, -1)

  34. return cidrs, nil

  35. }

  36. // scrapeAkamai scrapes akamai firewall's CIDR ranges from ipinfo

  37. func scrapeAkamai(httpClient *http.Client) ([]string, error) {

  38. resp, err := httpClient.Get("https://ipinfo.io/AS12222")

  39. if err != nil {

  40. return nil, err

  41. }

  42. defer resp.Body.Close()

  43. data, err := ioutil.ReadAll(resp.Body)

  44. if err != nil {

  45. return nil, err

  46. }

  47. body := string(data)

  48. cidrs := cidrRegex.FindAllString(body, -1)

  49. return cidrs, nil

  50. }

  51. // scrapeSucuri scrapes sucuri firewall's CIDR ranges from ipinfo

  52. func scrapeSucuri(httpClient *http.Client) ([]string, error) {

  53. resp, err := httpClient.Get("https://ipinfo.io/AS30148")

  54. if err != nil {

  55. return nil, err

  56. }

  57. defer resp.Body.Close()

  58. data, err := ioutil.ReadAll(resp.Body)

  59. if err != nil {

  60. return nil, err

  61. }

  62. body := string(data)

  63. cidrs := cidrRegex.FindAllString(body, -1)

  64. return cidrs, nil

  65. }

  66. func scrapeProjectDiscovery(httpClient *http.Client) ([]string, error) {

  67. resp, err := httpClient.Get("https://cdn.projectdiscovery.io/cdn/cdn-ips")

  68. if err != nil {

  69. return nil, err

  70. }

  71. defer resp.Body.Close()

  72. data, err := ioutil.ReadAll(resp.Body)

  73. if err != nil {

  74. return nil, err

  75. }

  76. body := string(data)

  77. cidrs := cidrRegex.FindAllString(body, -1)

  78. return cidrs, nil

  79. }

connect扫描

naabu的connect扫描就是简单的建立一个tcp连接

  1. // ConnectVerify is used to verify if ports are accurate using a connect request

  2. func (s *Scanner) ConnectVerify(host string, ports map[int]struct{}) map[int]struct{} {

  3. for port := range ports {

  4. conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", host, port), s.timeout)

  5. if err != nil {

  6. delete(ports, port)

  7. continue

  8. }

  9. gologger.Debugf("Validated active port %d on %s\n", port, host)

  10. conn.Close()

  11. }

  12. return ports

  13. }

syn扫描

syn扫描只能在unix操作系统上运行,如果是windows系统,会切换到connect扫描。

syn扫描的原理是只用发一个syn包,节省发包时间,而完整的tcp需要进行三次握手。

获取空闲端口

初始化时,获取空闲端口,并监听这个端口

  1. import github.com/phayes/freeport

  2. func NewScannerUnix(scanner *Scanner) error {

  3. rawPort, err := freeport.GetFreePort()

  4. if err != nil {

  5. return err

  6. }

  7. scanner.listenPort = rawPort

  8. tcpConn, err := net.ListenIP("ip4:tcp", &net.IPAddr{IP: net.ParseIP(fmt.Sprintf("0.0.0.0:%d", rawPort))})

  9. if err != nil {

  10. return err

  11. }

  12. scanner.tcpPacketlistener = tcpConn

  13. var handlers Handlers

  14. scanner.handlers = handlers

  15. scanner.tcpChan = make(chan *PkgResult, chanSize)

  16. scanner.tcpPacketSend = make(chan *PkgSend, packetSendSize)

  17. return nil

  18. }

监听网卡

获取网卡名称

SetupHandlerUnix 监听网卡

  1. const (

  2. maxRetries = 10

  3. sendDelayMsec = 10

  4. chanSize = 1000

  5. packetSendSize = 2500

  6. snaplen = 65536

  7. readtimeout = 1500

  8. )

  9. func SetupHandlerUnix(s *Scanner, interfaceName string) error {

  10. inactive, err := pcap.NewInactiveHandle(interfaceName)

  11. if err != nil {

  12. return err

  13. }

  14. err = inactive.SetSnapLen(snaplen)

  15. if err != nil {

  16. return err

  17. }

  18. readTimeout := time.Duration(readtimeout) * time.Millisecond

  19. if err = inactive.SetTimeout(readTimeout); err != nil {

  20. s.CleanupHandlers()

  21. return err

  22. }

  23. err = inactive.SetImmediateMode(true)

  24. if err != nil {

  25. return err

  26. }

  27. handlers := s.handlers.(Handlers)

  28. handlers.Inactive = append(handlers.Inactive, inactive)

  29. handle, err := inactive.Activate()

  30. if err != nil {

  31. s.CleanupHandlers()

  32. return err

  33. }

  34. handlers.Active = append(handlers.Active, handle)

  35. // Strict BPF filter

  36. // + Packets coming from target ip

  37. // + Destination port equals to sender socket source port

  38. err = handle.SetBPFFilter(fmt.Sprintf("tcp and dst port %d and tcp[13]=18", s.listenPort))

  39. if err != nil {

  40. s.CleanupHandlers()

  41. return err

  42. }

  43. s.handlers = handlers

  44. return nil

  45. }

从网卡中过滤数据包 tcpanddst port%dandtcp[13]=18

%d 即第一步获取的空闲端口,tcp[13]=18 即tcp的第十三位偏移的值为18,即仅抓取TCP SYN标记的数据包。

监听数据

通过pcap监听数据

  1. func TCPReadWorkerPCAPUnix(s *Scanner) {

  2. defer s.CleanupHandlers()

  3. var wgread sync.WaitGroup

  4. handlers := s.handlers.(Handlers)

  5. for _, handler := range handlers.Active {

  6. wgread.Add(1)

  7. go func(handler *pcap.Handle) {

  8. defer wgread.Done()

  9. var (

  10. eth layers.Ethernet

  11. ip4 layers.IPv4

  12. tcp layers.TCP

  13. )

  14. // Interfaces with MAC (Physical + Virtualized)

  15. parserMac := gopacket.NewDecodingLayerParser(layers.LayerTypeEthernet, ð, &ip4, &tcp)

  16. // Interfaces without MAC (TUN/TAP)

  17. parserNoMac := gopacket.NewDecodingLayerParser(layers.LayerTypeIPv4, &ip4, &tcp)

  18. var parsers []*gopacket.DecodingLayerParser

  19. parsers = append(parsers, parserMac, parserNoMac)

  20. decoded := []gopacket.LayerType{}

  21. for {

  22. data, _, err := handler.ReadPacketData()

  23. if err == io.EOF {

  24. break

  25. } else if err != nil {

  26. continue

  27. }

  28. for _, parser := range parsers {

  29. if err := parser.DecodeLayers(data, &decoded); err != nil {

  30. continue

  31. }

  32. for _, layerType := range decoded {

  33. if layerType == layers.LayerTypeTCP {

  34. if !s.IPRanger.Contains(ip4.SrcIP.String()) {

  35. gologger.Debugf("Discarding TCP packet from non target ip %s\n", ip4.SrcIP.String())

  36. continue

  37. }

  38. // We consider only incoming packets

  39. if tcp.DstPort != layers.TCPPort(s.listenPort) {

  40. continue

  41. } else if tcp.SYN && tcp.ACK {

  42. s.tcpChan <- &PkgResult{ip: ip4.SrcIP.String(), port: int(tcp.SrcPort)}

  43. }

  44. }

  45. }

  46. }

  47. }

  48. }(handler)

  49. }

  50. wgread.Wait()

  51. }

如果dstport为我们监听的端口,并且标志位是 syn+ack,就将端口和ip加入到结果中。

发送数据包

核心内容是从之前监听的tcp发送。

  1. // SendAsyncPkg sends a single packet to a port

  2. func (s *Scanner) SendAsyncPkg(ip string, port int, pkgFlag PkgFlag) {

  3. // Construct all the network layers we need.

  4. ip4 := layers.IPv4{

  5. SrcIP: s.SourceIP,

  6. DstIP: net.ParseIP(ip),

  7. Version: 4,

  8. TTL: 255,

  9. Protocol: layers.IPProtocolTCP,

  10. }

  11. tcpOption := layers.TCPOption{

  12. OptionType: layers.TCPOptionKindMSS,

  13. OptionLength: 4,

  14. OptionData: []byte{0x05, 0xB4},

  15. }

  16. tcp := layers.TCP{

  17. SrcPort: layers.TCPPort(s.listenPort),

  18. DstPort: layers.TCPPort(port),

  19. Window: 1024,

  20. Seq: s.tcpsequencer.Next(),

  21. Options: []layers.TCPOption{tcpOption},

  22. }

  23. if pkgFlag == SYN {

  24. tcp.SYN = true

  25. } else if pkgFlag == ACK {

  26. tcp.ACK = true

  27. }

  28. err := tcp.SetNetworkLayerForChecksum(&ip4)

  29. if err != nil {

  30. if s.debug {

  31. gologger.Debugf("Can not set network layer for %s:%d port: %s\n", ip, port, err)

  32. }

  33. } else {

  34. err = s.send(ip, s.tcpPacketlistener, &tcp)

  35. if err != nil {

  36. if s.debug {

  37. gologger.Debugf("Can not send packet to %s:%d port: %s\n", ip, port, err)

  38. }

  39. }

  40. }

  41. }

  42. // send sends the given layers as a single packet on the network.

  43. func (s *Scanner) send(destIP string, conn net.PacketConn, l ...gopacket.SerializableLayer) error {

  44. buf := gopacket.NewSerializeBuffer()

  45. if err := gopacket.SerializeLayers(buf, s.serializeOptions, l...); err != nil {

  46. return err

  47. }

  48. var (

  49. retries int

  50. err error

  51. )

  52. send:

  53. if retries >= maxRetries {

  54. return err

  55. }

  56. _, err = conn.WriteTo(buf.Bytes(), &net.IPAddr{IP: net.ParseIP(destIP)})

  57. if err != nil {

  58. retries++

  59. // introduce a small delay to allow the network interface to flush the queue

  60. time.Sleep(time.Duration(sendDelayMsec) * time.Millisecond)

  61. goto send

  62. }

  63. return err

  64. }

其他

修改ulimit

大多数类UNIX操作系统(包括Linux和macOS)在每个进程和每个用户的基础上提供了系统资源的限制和控制(如线程,文件和网络连接)的方法。这些“ulimits”阻止单个用户使用太多系统资源。

  1. import (

  2. _ "github.com/projectdiscovery/fdmax/autofdmax"

  3. )

修改ulimit,只针对unix系统

fdmax.go

  1. // +build !windows

  2. package fdmax

  3. import (

  4. "runtime"

  5. "golang.org/x/sys/unix"

  6. )

  7. const (

  8. UnixMax uint64 = 999999

  9. OSXMax uint64 = 24576

  10. )

  11. type Limits struct {

  12. Current uint64

  13. Max uint64

  14. }

  15. func Get() (*Limits, error) {

  16. var rLimit unix.Rlimit

  17. err := unix.Getrlimit(unix.RLIMIT_NOFILE, &rLimit)

  18. if err != nil {

  19. return nil, err

  20. }

  21. return &Limits{Current: uint64(rLimit.Cur), Max: uint64(rLimit.Max)}, nil

  22. }

  23. func Set(maxLimit uint64) error {

  24. var rLimit unix.Rlimit

  25. rLimit.Max = maxLimit

  26. rLimit.Cur = maxLimit

  27. // https://github.com/golang/go/issues/30401

  28. if runtime.GOOS == "darwin" && rLimit.Cur > OSXMax {

  29. rLimit.Cur = OSXMax

  30. }

  31. return unix.Setrlimit(unix.RLIMIT_NOFILE, &rLimit)

  32. }

随机IP PICK

  1. import "github.com/projectdiscovery/ipranger"

ipranger 实现就是来自masscan的随机化地址扫描算法

在 https://paper.seebug.org/1052 写过

随机化地址扫描

在读取地址后,如果进行顺序扫描,伪代码如下

  1. for (i = 0; i < range; i++) {

  2. scan(i);

  3. }

但是考虑到有的网段可能对扫描进行检测从而封掉整个网段,顺序扫描效率是较低的,所以需要将地址进行随机的打乱,用算法描述就是设计一个 打乱数组的算法,Masscan是设计了一个加密算法,伪代码如下

  1. range = ip_count * port_count;

  2. for (i = 0; i < range; i++) {

  3. x = encrypt(i);

  4. ip = pick(addresses, x / port_count);

  5. port = pick(ports, x % port_count);

  6. scan(ip, port);

  7. }

随机种子就是 i的值,这种加密算法能够建立一种一一对应的映射关系,即在[1...range]的区间内通过 i来生成[1...range]内不重复的随机数。同时如果中断了扫描,只需要记住 i的值就能重新启动,在分布式上也可以根据 i来进行。

  • 如果对这个加密算法感兴趣可以看 Ciphers with Arbitrary Finite Domains 这篇论文。

可缓存的hashmap

ipranger中使用了 github.com/projectdiscovery/hmap/store/hybrid

看了下代码,是一个带缓存功能的hashmap,也带有超时时间。

所有添加的目标(ip)会加入到缓存中,让我想到 ksubdomain中也有实现类似的功能,不过是在内存中进行,导致目标很多的时候内存操作会有点问题。如果用这个库应该可以解决这个问题 。

总结

naabu的代码架构很清晰,一个文件完成一个功能,通过看文件名就知道这个实现了什么功能,所以看代码的时候很轻松。

  1. 但是从代码来看,naabu只是实现了在linux上的 syn,在Windows上会使用三次握手的tcp连接(基于pcap,可以实现在windows上组合tcp发包的,但naabu没有实现)。

  2. naabu的目标添加是先循环读取目标一遍,如果目标cidr地址很大,会造成很多内存占用(虽然也会有硬盘缓存),如果边读取边发送就没有这种烦恼,但naabu不是这样的。

  3. naabu的重试次数,不是对某个ip:port的发送失败的重试,是对所有目标的重试。。

naabu还不是心中完美的扫描器 - =


文章来源: http://mp.weixin.qq.com/s?__biz=MzU2NzcwNTY3Mg==&mid=2247483936&idx=1&sn=02088885793e73fa8b1d1aea2545cefe&chksm=fc986b07cbefe21151bd45374df07c62b497cf41b5f0573abea4b389bc7c58c427449291257c#rd
如有侵权请联系:admin#unsafe.sh