一、概述
前两篇主要分析些工具集,已经针对web服务的指纹和端口指纹信息进行识别,并没有真正开始扫描。本篇主要分析如何进行IP存活探测以及tcp扫描实现。
项目来源:https://github.com/XinRoom/go-portScan/blob/main/util/file.go
二、/core/host/ 目录(进行主机的ping探测存活)
/core/host/ping.go
此代码主要是用来进行主机存活的。
首先设置判断变量和常见端口列表
var CanIcmp bool // 用于标识是否支持发送 ICMP 包。
var TcpPingPorts = []uint16{80, 22, 445, 23, 443, 81, 161, 3389, 8080, 8081}//是用于 TCP Ping 的默认常见端口列表。
函数列表分析:
- func IcmpOK(host string) bool
函数尝试直接发送 ICMP 包来检查主机是否存活。
// IcmpOK 直接发ICMP包 func IcmpOK(host string) bool { pinger, err := ping.NewPinger(host) if err != nil { return false } pinger.SetPrivileged(true) pinger.Count = 1 pinger.Timeout = 800 * time.Millisecond if pinger.Run() != nil { // Blocks until finished. return err return false } if stats := pinger.Statistics(); stats.PacketsRecv > 0 { return true } return false }
- func PingOk(host string) bool
函数尝试通过执行不同操作系统的 Ping 命令来检查主机是否存活。
// PingOk Ping命令模式 func PingOk(host string) bool { switch runtime.GOOS { case "linux": cmd := exec.Command("ping", "-c", "1", "-W", "1", host) var out bytes.Buffer cmd.Stdout = &out cmd.Run() if strings.Contains(out.String(), "ttl=") { return true } case "windows": cmd := exec.Command("ping", "-n", "1", "-w", "500", host) var out bytes.Buffer cmd.Stdout = &out cmd.Run() if strings.Contains(out.String(), "TTL=") { return true } case "darwin": cmd := exec.Command("ping", "-c", "1", "-t", "1", host) var out bytes.Buffer cmd.Stdout = &out cmd.Run() if strings.Contains(out.String(), "ttl=") { return true } } return false }
- func TcpPing(host string, ports []uint16, timeout time.Duration) (ok bool)
函数使用 TCP 连接在指定端口上对主机进行存活探测,在连接成功或者连接被拒绝时,都会判断主机为存活状态。
func TcpPing(host string, ports []uint16, timeout time.Duration) (ok bool) { var wg sync.WaitGroup // 创建一个 WaitGroup 用于等待所有端口的探测完成 ctx, cancel := context.WithCancel(context.Background()) // 创建一个上下文和取消函数 d := net.Dialer{ // 创建一个 Dialer,用于建立 TCP 连接 Timeout: timeout + time.Second, // 设置连接超时时间 KeepAlive: 0, // 禁用 KeepAlive } for _, port := range ports { // 遍历端口列表 time.Sleep(10 * time.Millisecond) // 间隔一段时间再探测下一个端口,避免过于频繁的连接 wg.Add(1) // 每个端口探测前增加 WaitGroup 计数 go func(_port uint16) { // 并发进行端口探测 conn, err := d.DialContext(ctx, "tcp", fmt.Sprintf("%s:%d", host, _port)) // 尝试建立 TCP 连接 if conn != nil { // 如果成功建立连接 conn.Close() // 关闭连接 ok = true // 设置存活状态为 true } else if err != nil && strings.Contains(err.Error(), "refused it") { // 如果连接被拒绝 ok = true // 设置存活状态为 true } if ok { // 如果已经确认存活 cancel() // 取消其他端口的探测 } wg.Done() // 完成当前端口的探测 }(_port) // 传入端口号进行端口探测 } wg.Wait() // 等待所有端口的探测完成 return // 返回存活状态 }
- func init()
函数在包加载时运行,它尝试在本地(127.0.0.1)发送 ICMP 包来检查系统是否支持 ICMP。
func init() { if IcmpOK("127.0.0.1") { CanIcmp = true } }
- func IsLive(ip string, tcpPing bool, tcpTimeout time.Duration) (ok bool)
IsLive
函数是用来检查主机是否存活的主要函数。如果支持 ICMP,则使用IcmpOK
函数;否则,使用PingOk
函数。如果前面的检查未能确定主机存活且tcpPing
参数为真,则尝试使用TcpPing
函数进行 TCP 存活探测。func IsLive(ip string, tcpPing bool, tcpTimeout time.Duration) (ok bool) { if CanIcmp { ok = IcmpOK(ip) } else { ok = PingOk(ip) } if !ok && tcpPing { ok = TcpPing(ip, TcpPingPorts, tcpTimeout) } return }
ps:总体而言,这些函数提供了多种方法来检查主机是否存活,包括使用 ICMP 包、执行系统命令(如 Ping 命令)以及 TCP 连接到常见端口。这样做可以在不同系统和网络环境下更全面地检查主机的存活状态
三、port端口解析,以及tcp扫描
1、/core/port/port.go
这个文件代码主要包含了端口扫描相关的逻辑和数据结构。
- var TopTcpPorts = []uint16{} 常见端口列表
- 各结构体列表
type Scanner interface {//这是一个接口,定义了扫描器的操作,包括关闭、等待、扫描指定 IP 和端口,以及等待速率限制器。
Close()
Wait()
Scan(ip net.IP, dst uint16) error
WaitLimiter() error
}
// OpenIpPort retChan,这个结构体表示一个开放的 IP 和端口,包含 IP 地址、端口号、服务名和可能的 HTTP 信息。
type OpenIpPort struct {
Ip net.IP
Port uint16
Service string
HttpInfo *HttpInfo
}
// Option ...这个结构体包含了扫描的参数配置,如速率限制、超时时间、网卡信息和是否进行服务探测等。
type Option struct {
Rate int // 每秒速度限制, 单位: s, 会在1s内平均发送, 相当于每个包之间的延迟
Timeout int // TCP连接响应延迟, 单位: ms
NextHop string // pcap dev name
FingerPrint bool // 服务探测
Httpx bool // HttpInfo 探测
}
// HttpInfo Http服务基础信息,这个结构体存储了 HTTP 服务的基础信息,包括状态码、响应包大小、URL、重定向路径、标题、服务名、TLS 信息和 Web 指纹
type HttpInfo struct {
StatusCode int // 状态码
ContentLen int // 相应包大小
Url string // Url
Location string // 302、301重定向路径
Title string // 标题
Server string // 服务名
TlsCN string // tls使用者名称
TlsDNS []string // tlsDNS列表
Fingers []string // 识别到的web指纹
}
- func (op OpenIpPort) String() string
这个函数是为了实现
Stringer
接口,这个接口包含一个String()
方法,用于定义该类型的字符串表示形式。对于OpenIpPort
类型的实例,这个函数会返回其 IP 地址、端口号以及可能的服务信息的字符串表示形式func (op OpenIpPort) String() string { buf := strings.Builder{} buf.WriteString(op.Ip.String()) buf.WriteString(":") buf.WriteString(strconv.Itoa(int(op.Port))) if op.Service != "" { buf.WriteString(" ") buf.WriteString(op.Service) } if op.HttpInfo != nil { buf.WriteString("\n") buf.WriteString(op.HttpInfo.String()) } return buf.String() }
- func (hi *HttpInfo) String() string
这个函数也是实现
Stringer
接口的方法。对于HttpInfo
类型的实例,这个函数返回其包含的 HTTP 信息的字符串表示形式,包括状态码、响应包大小、URL、重定向路径、标题、服务名和 Web 指纹等。func (hi *HttpInfo) String() string { if hi == nil { return "" } var buf strings.Builder buf.WriteString(fmt.Sprintf("[HttpInfo]%s StatusCode:%d ContentLen:%d Title:%s ", hi.Url, hi.StatusCode, hi.ContentLen, hi.Title)) if hi.Location != "" { buf.WriteString("Location:" + hi.Location + " ") } if hi.TlsCN != "" { buf.WriteString("TlsCN:" + hi.TlsCN + " ") } if len(hi.TlsDNS) > 0 { buf.WriteString("TlsDNS:" + strings.Join(hi.TlsDNS, ",") + " ") } if hi.Server != "" { buf.WriteString("Server:" + hi.Server + " ") } if len(hi.Fingers) != 0 { buf.WriteString(fmt.Sprintf("Fingers:%s ", hi.Fingers)) } return buf.String() }
- func ParsePortRangeStr(portStr string) (out [][]uint16, err error)
这个函数用于解析端口字符串,将其转换为端口范围的列表。
// ParsePortRangeStr 解析端口字符串 func ParsePortRangeStr(portStr string) (out [][]uint16, err error) { portsStrGroup := strings.Split(portStr, ",")//逗号分隔的端口 var portsStrGroup3 []string var portStart, portEnd uint64 for _, portsStrGroup2 := range portsStrGroup { if portsStrGroup2 == "top1000" { continue } portsStrGroup3 = strings.Split(portsStrGroup2, "-")//-作为端口范围的 portStart, err = strconv.ParseUint(portsStrGroup3[0], 10, 16)//返回端口的整数值,10进制,类型为int16 if err != nil { return } portEnd = portStart if len(portsStrGroup3) == 2 { portEnd, err = strconv.ParseUint(portsStrGroup3[1], 10, 16) } if err != nil { return } out = append(out, []uint16{uint16(portStart), uint16(portEnd)}) } return }
- func IsInPortRange(port uint16, portRanges [][]uint16) bool
这个函数用于检查指定端口是否在端口范围内
// IsInPortRange 判断port是否在端口范围里 func IsInPortRange(port uint16, portRanges [][]uint16) bool { for _, portRange := range portRanges { if port >= portRange[0] && port <= portRange[1] { return true } } return false }
- func ShuffleParseAndMergeTopPorts(portStr string) (ports []uint16, err error)
这个函数主要实现了对端口的解析、合并和随机化处理。它会解析传入的端口字符串,根据配置信息选取一些端口,优先使用常见的 TCP 端口,然后从用户指定的端口范围中选择未被选取的端口,并最终随机排序这些端口
// ShuffleParseAndMergeTopPorts shuffle parse portStr and merge TopTcpPorts func ShuffleParseAndMergeTopPorts(portStr string) (ports []uint16, err error) { if portStr == "" { ports = TopTcpPorts //未指定则用默认top端口 return } var portRanges [][]uint16 portRanges, err = ParsePortRangeStr(portStr) if err != nil { return } // 优先发送top端口 selectTopPort := make(map[uint16]struct{}) // TopPort hasTopStr := strings.Contains(portStr, "top1000") for _, _port := range TopTcpPorts { if hasTopStr || IsInPortRange(_port, portRanges) { //检测端口是否在范围内 selectTopPort[_port] = struct{}{} ports = append(ports, _port) } } selectPort := make(map[uint16]struct{}) // OtherPort for _, portRange := range portRanges { var ok bool for _port := portRange[0]; _port <= portRange[1]; _port++ { if _port == 0 { continue } if _, ok = selectTopPort[_port]; ok { continue } else if _, ok = selectPort[_port]; ok { continue } selectPort[_port] = struct{}{} ports = append(ports, _port) //得到所有端口,并将top端口排在前面 if _port == 65535 { break } } } if len(ports) == 0 { err = errors.New("ports len is 0") return } // 端口随机化 skip := uint64(len(selectTopPort)) // 跳过Top _ports := make([]uint16, len(ports)) copy(_ports, ports) sf := util.NewShuffle(uint64(len(ports)) - skip) if sf != nil { for i := skip; i < uint64(len(_ports)); i++ { ports[i] = _ports[skip+sf.Get(i-skip)] } } return }
ps:这些函数主要提供了对端口进行解析、筛选和随机化的功能,以便用于端口扫描和服务探测。它将常见的 TCP 端口列表与用户输入的端口范围合并,随机化排序以减少扫描的可预测性。
2、/core/port/tcp/tcp.go
整体来说,这个代码文件定义了一个 TCP 端口扫描器,可以根据指定的 IP 地址和端口号对目标进行扫描,并可选地进行服务探测和 HTTP 信息探测。同时,它实现了速率限制以及 goroutine 的管理,确保扫描操作的安全性和效率。
var DefaultTcpOption = port.Option{//这里定义了默认的 TCP 扫描选项,包括扫描速率和超时时间等。
Rate: 1000,
Timeout: 800,
}
type TcpScanner struct {//管理 TCP 端口扫描器的状态和操作。结构体中包含了需要的字段和方法。
ports []uint16 // 指定端口
retChan chan port.OpenIpPort // 返回值队列
limiter *limiter.Limiter
ctx context.Context
timeout time.Duration
isDone bool
option port.Option
wg sync.WaitGroup
}
- func NewTcpScanner(retChan chan port.OpenIpPort, option port.Option) (ts *TcpScanner, err error)
这个函数是一个构造器,用于创建一个新的 TCP 扫描器实例。它接收一个返回值通道
retChan
和扫描选项option
,并返回一个TcpScanner
的指针。函数首先对传入的选项进行验证,确保速率大于等于 10,超时时间大于 0。然后,它初始化了一个TcpScanner
结构体实例,设置了返回通道、速率限制器、上下文、超时时间和其他选项,并将该实例赋值给ts
,最后返回该实例和可能的错误。// NewTcpScanner Tcp扫描器 func NewTcpScanner(retChan chan port.OpenIpPort, option port.Option) (ts *TcpScanner, err error) { // option verify if option.Rate < 10 { err = errors.New("rate can not be set less than 10") // 如果速率小于 10,则返回错误 return } if option.Timeout <= 0 { err = errors.New("timeout can not be set to 0") // 如果超时时间小于等于 0,则返回错误 return } // 初始化 TcpScanner 结构体 ts = &TcpScanner{ retChan: retChan, // 设置返回通道 limiter: limiter.NewLimiter(limiter.Every(time.Second/time.Duration(option.Rate)), option.Rate/10), // 设置速率限制器 ctx: context.Background(), // 初始化上下文 timeout: time.Duration(option.Timeout) * time.Millisecond, // 设置超时时间 option: option, // 设置选项 } return // 返回 TcpScanner 实例和可能的错误 }
- func (ts *TcpScanner) Scan(ip net.IP, dst uint16) error
这个函数用于执行对指定 IP 和目标端口进行扫描的操作。它会启动一个 goroutine,在其中进行端口扫描并将结果发送到
retChan
通道中。函数首先检查扫描器是否已关闭,然后将一个任务添加到等待组wg
中。接着,它初始化了一个port.OpenIpPort
结构体实例,表示正在扫描的 IP 和端口。接下来的部分涉及服务指纹识别和 HTTP 信息探测的逻辑。如果设置了相应的选项,它将调用相关的函数进行识别并填充openIpPort
结构体中的信息。最后,如果没有进行服务指纹识别或者 HTTP 信息探测,它将尝试通过net.DialTimeout
进行连接,判断端口是否开放,并将结果发送到通道中。// Scan 对指定IP和dis port进行扫描 func (ts *TcpScanner) Scan(ip net.IP, dst uint16) error { if ts.isDone { return errors.New("scanner is closed") // 如果扫描器已关闭,则返回错误 } ts.wg.Add(1) // 增加等待组中的任务数 go func() { defer ts.wg.Done() // 标记任务结束 //fmt.Println(1) openIpPort := port.OpenIpPort{ Ip: ip, Port: dst, } var isDailErr bool if ts.option.FingerPrint { openIpPort.Service, isDailErr = fingerprint.PortIdentify("tcp", ip, dst, 2*time.Second) // 进行服务指纹识别 if isDailErr { return // 如果识别过程出错,直接返回 } } if ts.option.Httpx && (openIpPort.Service == "" || openIpPort.Service == "http" || openIpPort.Service == "https") { openIpPort.HttpInfo, isDailErr = fingerprint.ProbeHttpInfo(ip, dst, 2*time.Second) // 进行 HTTP 信息探测 if isDailErr { return // 如果探测过程出错,直接返回 } if openIpPort.HttpInfo != nil { if strings.HasPrefix(openIpPort.HttpInfo.Url, "https") { openIpPort.Service = "https" // 如果是 HTTPS,则标记为 HTTPS 服务 } else { openIpPort.Service = "http" // 如果是 HTTP,则标记为 HTTP 服务 } } } if !ts.option.FingerPrint && !ts.option.Httpx { conn, _ := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", ip, dst), ts.timeout) // 尝试连接端口 if conn != nil { conn.Close() // 如果连接成功,则关闭连接 } else { return // 如果连接失败,直接返回 } } ts.retChan <- openIpPort // 将扫描结果发送到通道 }() return nil }
- 辅助函数
func (ts *TcpScanner) Wait()
func (ts *TcpScanner) Close()
func (ts *TcpScanner) WaitLimiter() error
//这个方法用于等待所有启动的 goroutine 完成扫描操作。它会等待 wg 等待组中的所有 goroutine 完成。 func (ts *TcpScanner) Wait() { ts.wg.Wait() } // Close chan这个方法用于关闭 retChan 通道,表示扫描已经完成。它还会设置 isDone 标志,表示扫描器已关闭 func (ts *TcpScanner) Close() { ts.isDone = true close(ts.retChan) } // WaitLimiter Waiting for the speed limit这个方法用于等待速率限制器。它会通过 limiter 控制扫描的速率,以确保按照设定的速率发送扫描请求 func (ts *TcpScanner) WaitLimiter() error { return ts.limiter.Wait(ts.ctx) }