扫描器解析日记之目标探测

本篇文章以几款扫描器为例,分析其前期对目标探测的模块进行入手学习。 Fscan ===== 在读取完各种参数后,进入到解析ip中 ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/10/attach-a933f94e5b5ba824ef618ffc5ce1095a5845c7a2.png) 若传入的不是文件且包含端口的ip,则先分割出ip和port然后丢入ParseIPs​进行解析,如果没有携带端口则直接进入ParseIPs,若是文件则进行文件处理后再解析,所以我们跟进到ParseIPs​中 ```java func ParseIP(host string, filename string, nohosts ...string) (hosts []string, err error) { if filename == "" && strings.Contains(host, ":") { //192.168.0.0/16:80 hostport := strings.Split(host, ":") if len(hostport) == 2 { host = hostport[0] hosts = ParseIPs(host) Ports = hostport[1] } } else { hosts = ParseIPs(host) if filename != "" { var filehost []string filehost, _ = Readipfile(filename) hosts = append(hosts, filehost...) } } //nohosts不扫描的ip //..篇幅省略 //去重 hosts = RemoveDuplicate(hosts) if len(hosts) == 0 && len(HostPort) == 0 && host != "" && filename != "" { err = ParseIPErr } return } ``` ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/10/attach-d5a7bcb5a0b32947f46231d904e4dfbb9278794f.png) 主要是这个对于A段扫描时处理不够完善,fscan为了避免扫描过多的ip采用了随机扫描的方式,如果用户就是需要扫描整个/8网段,则可能会遗漏,我们可以对其修改如下 ```java func parseIP(ip string) []string { reg := regexp.MustCompile(`[a-zA-Z]+`) switch { case ip == "192": return parseIP("192.168.0.0/8") case ip == "172": return parseIP("172.16.0.0/12") case ip == "10": return parseIP("10.0.0.0/8") // 扫描/8时,只扫网关和随机IP,避免扫描过多IP case strings.HasSuffix(ip, "/8"): return parseIP8(ip) //解析 /24 /16 /8 /xxx 等 case strings.Contains(ip, "/"): return parseIP2(ip) //可能是域名,用lookup获取ip case reg.MatchString(ip): // _, err := net.LookupHost(ip) // if err != nil { // return nil // } return []string{ip} //192.168.1.1-192.168.1.100 case strings.Contains(ip, "-"): return parseIP1(ip) //处理单个ip default: testIP := net.ParseIP(ip) if testIP == nil { return nil } return []string{ip} } } ``` 这里我直接就参考dddd的写法改写了 ```java func parseIP8(ip string) []string { var AllIP []string for _, i := range CIDRToIP(ip) { AllIP = append(AllIP, i.String()) } return AllIP } func CIDRToIP(cidr string) (IPs []net.IP) { _, network, _ := net.ParseCIDR(cidr) first := FirstIP(network) last := LastIP(network) return pairsToIP(first, last) } func FirstIP(network *net.IPNet) net.IP { return network.IP } func LastIP(network *net.IPNet) net.IP { firstIP := FirstIP(network) mask, _ := network.Mask.Size() size := math.Pow(2, float64(32-mask)) lastIP := toIP(toInt(firstIP) + uint32(size) - 1) return net.ParseIP(lastIP) } func toIP(i uint32) string { buf := bytes.NewBuffer([]byte{}) _ = binary.Write(buf, binary.BigEndian, i) b := buf.Bytes() return fmt.Sprintf("%v.%v.%v.%v", b[0], b[1], b[2], b[3]) } func toInt(ip net.IP) uint32 { var buf = []byte(ip) if len(buf) > 12 { buf = buf[12:] } buffer := bytes.NewBuffer(buf) var i uint32 _ = binary.Read(buffer, binary.BigEndian, &i) return i } func pairsToIP(ip1, ip2 net.IP) (IPs []net.IP) { start := toInt(ip1) end := toInt(ip2) for i := start; i <= end; i++ { IPs = append(IPs, net.ParseIP(toIP(i))) } return IPs } ``` 效果如下 ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/10/attach-c02946d0755baf6f22cf61f8e59389b0ec4ef191.png) 接着就是初始化一些http客户端的参数 web poc的线程数 ThreadsNum​、代理类型 DownProxy​ 和超时时间 Timeout​。主要目的是配置一个 http.Client​ 实例,在进行 HTTP 请求时使用指定的代理和超时设置 ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/10/attach-8c9c7e9856ea7ca19b4ad9afa003c205f74639e9.png) 如果noping参数为false,或者扫描参数为icmp则进行CheckLive​的存活探测 ​common.LogWG.Wait()​这个是日志同步,等待所有日志记录操作完成。确保所有日志记录操作都已完成,避免日志丢失。 ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/10/attach-f15bb6b9f9fbb1ac84fd2a6217b4572b5e0bdd11.png) ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/10/attach-1b14046b13cbda99de4c20892f8541b0c59054f4.png) 监听一个通道(chanHosts​)以接收IP地址。当接收到一个IP地址时,它检查该IP是否不在ExistHosts​映射中,并且是否在hostslist​中。如果两个条件都为真,则将IP添加到ExistHosts​和AliveHosts​中,并打印一条消息。 这里利用一个ExistHosts\[ip\] = struct{}{}​ 以利用Go语言的映射(map)特性来实现一个集合(set)的功能。由于映射中的值可以是任意类型,而这里使用的是空结构体 struct{}{}​,所以实际上我们并不关心值本身,而是关心键(即IP地址)是否存在。 通过这种方式,我们可以快速地检查一个IP地址是否已经存在于 ExistHosts​ 中,如果存在,则不需要再次添加。同时,由于空结构体不占用任何内存空间,所以这种做法也非常节省内存。 ```java func CheckLive(hostslist []string, Ping bool) []string { //创建一个缓冲通道,容量为 hostslist 的长度 chanHosts := make(chan string, len(hostslist)) go func() { for ip := range chanHosts { if _, ok := ExistHosts[ip]; !ok && IsContain(hostslist, ip) { ExistHosts[ip] = struct{}{} if common.Silent == false { if Ping == false { fmt.Printf("(icmp) Target %-15s is alive\n", ip) } else { fmt.Printf("(ping) Target %-15s is alive\n", ip) } } AliveHosts = append(AliveHosts, ip) } livewg.Done() } }() ``` 下面就是选择用ping还是icmp进行探测 默认ping参数为false,所以优先尝试监听本地icmp, 进入RunIcmp1​ 这个函数主要逻辑就是 1. 遍历hostslist​切片中的每个IP地址。 2. 对于每个IP地址,发送一个ICMP回显请求到该IP地址。 3. 在后台运行一个goroutine,监听ICMP回显应答。 4. 如果收到ICMP回显应答,表示目标IP地址存活,将其添加到AliveHosts​列表中。 5. 等待一段时间,如果AliveHosts​列表中的IP地址数量与hostslist​中的IP地址数量相匹配,则表示所有IP地址都已探测完成。 ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/10/attach-cd77bc2af530c3982bf440d50f22bd67b6b425c1.png) 若是权限不够的话则会选择使用RunPing​进行探测 具体实现在ExecCommandPing​函数 根据回显,返回 true​ 则 ping 成功,否则返回 false​。 ```java func RunPing(hostslist []string, chanHosts chan string) { var wg sync.WaitGroup limiter := make(chan struct{}, 50) for _, host := range hostslist { wg.Add(1) limiter <- struct{}{} go func(host string) { if ExecCommandPing(host) { livewg.Add(1) chanHosts <- host } <-limiter wg.Done() }(host) } wg.Wait() } ``` ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/10/attach-45c1dac3c6bb621a1585192f155913da810d46aa.png) 在某些环境配置中会有echo 1 &gt; /proc/sys/net/ipv4/icmp\_echo\_ignore\_all​ 这种就不会返回任何ICMP的响应。而fscan默认的扫描策略强依赖于ICMP协议,所以有可能在一些情况漏掉部分资产。 ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/10/attach-bec6b739ef1bb01d69af8395513bfe8fa29fb9b8.png) 具体实现,通过建立一个TCP的连接,如果连接成功则会记录一个成功消息并将主机地址发送到一个通道。 ```java func PortConnect(addr Addr, respondingHosts chan<- string, adjustedTimeout int64, wg *sync.WaitGroup) { host, port := addr.ip, addr.port conn, err := common.WrapperTcpWithTimeout("tcp4", fmt.Sprintf("%s:%v", host, port), time.Duration(adjustedTimeout)*time.Second) if err == nil { defer conn.Close() address := host + ":" + strconv.Itoa(port) result := fmt.Sprintf("%s open", address) common.LogSuccess(result) wg.Add(1) respondingHosts <- address } ``` ​WrapperTcpWithTimeout​实现了一个TCP的包装器 ```java func WrapperTcpWithTimeout(network, address string, timeout time.Duration) (net.Conn, error) { d := &net.Dialer{Timeout: timeout} return WrapperTCP(network, address, d) } func WrapperTCP(network, address string, forward *net.Dialer) (net.Conn, error) { //get conn var conn net.Conn if Socks5Proxy == "" { var err error conn, err = forward.Dial(network, address) if err != nil { return nil, err } } else { dailer, err := Socks5Dailer(forward) if err != nil { return nil, err } conn, err = dailer.Dial(network, address) if err != nil { return nil, err } } return conn, nil } ``` GoGo ==== 看完了fscan的探测逻辑,接下来可以看看其他工具的这部分的相关逻辑,这里拿dddd以及gogo为例 直接跟进到gogo的默认直接扫描逻辑 ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/10/attach-993a539045549f1c289dd54725dedb888961abf4.png) 跟进plugin.Dispatch(result)​ ```java func Dispatch(result *pkg.Result) { defer func() { if err := recover(); err != nil { logs.Log.Errorf("scan %s unexcept error, %v", result.GetTarget(), err) panic(err) } }() atomic.AddInt32(&RunOpt.Sum, 1) if result.Port == "137" || result.Port == "nbt" { nbtScan(result) return } else if result.Port == "135" || result.Port == "wmi" { wmiScan(result) return } else if result.Port == "oxid" { oxidScan(result) return } else if result.Port == "icmp" || result.Port == "ping" { icmpScan(result) return } else if result.Port == "snmp" || result.Port == "161" { snmpScan(result) return } else if result.Port == "445" || result.Port == "smb" { smbScan(result) if RunOpt.Exploit == "ms17010" { ms17010Scan(result) } else if RunOpt.Exploit == "smbghost" || RunOpt.Exploit == "cve-2020-0796" { smbGhostScan(result) } else if RunOpt.Exploit == "auto" || RunOpt.Exploit == "smb" { ms17010Scan(result) smbGhostScan(result) } return } else if result.Port == "mssqlntlm" { mssqlScan(result) return } else if result.Port == "winrm" { winrmScan(result) return } else { initScan(result) } .... ... ``` 可以看到,它对一些特定端口服务针对性的做了一些定制化的扫描,比如135(wmi)、161(snmp), 一般大部分情况来说是是不会有其他情况占用的。 ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/10/attach-3827d92d0badb7b1ba6a5f91277d770b53a8a815.png) 跟进到默认的initScan ```java func initScan(result *pkg.Result) { var bs []byte target := result.GetTarget() if pkg.ProxyUrl != nil && strings.HasPrefix(pkg.ProxyUrl.Scheme, "http") { // 如果是http代理, 则使用http库代替socket conn := result.GetHttpConn(RunOpt.Delay) resp, err := pkg.HTTPGet(conn, "http://"+target) if err != nil { return } if err != nil { result.Err = err return } result.Open = true pkg.CollectHttpResponse(result, resp) } else { defer func() { // 如果进行了各种探测依旧为tcp协议, 则收集tcp端口状态 if result.Protocol == "tcp" { if result.Err != nil { result.Error = result.Err.Error() if RunOpt.Debug { result.ErrStat = handleError(result.Err) } } } }() conn, err := pkg.NewSocket("tcp", target, RunOpt.Delay) if err != nil { result.Err = err return } defer conn.Close() result.Open = true // 启发式扫描探测直接返回不需要后续处理 if result.SmartProbe { return } result.Status = "open" bs, err = conn.Read(RunOpt.Delay) if err != nil { senddataStr := fmt.Sprintf("GET /%s HTTP/1.1\r\nHost: %s\r\n\r\n", result.Uri, target) bs, err = conn.Request([]byte(senddataStr), DefaultMaxSize) if err != nil { result.Err = err } } pkg.CollectSocketResponse(result, bs) } //所有30x,400,以及非http协议的开放端口都送到http包尝试获取更多信息 if result.Status == "400" || result.Protocol == "tcp" || (strings.HasPrefix(result.Status, "3") && bytes.Contains(result.Content, []byte("location: https"))) { systemHttp(result, "https") } else if strings.HasPrefix(result.Status, "3") { systemHttp(result, "http") } return } ``` 这里不关注代理功能先,我们看默认是进入了pkg.NewSocket进行探测,封装了一个Socket的结构体,使用了go自带的net库实现的TCP的连接 ```java func NewSocket(network, target string, delay int) (*Socket, error) { s := &Socket{ Timeout: time.Duration(delay) * time.Second, } var conn net.Conn var err error if ProxyDialTimeout != nil { conn, err = ProxyDialTimeout(network, target, s.Timeout) } else { conn, err = net.DialTimeout(network, target, s.Timeout) } if err != nil { return nil, err } s.Conn = conn return s, nil } type Socket struct { Conn net.Conn Count int Timeout time.Duration } ``` 最后还会将所有的30x,400,以及非http协议的开放端口都送到http包尝试获取更多信息。 ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/10/attach-dad829c224f2254735faacae1bce472a7087c873.png) 可以发现,gogo的话其实为了尽可能的优化体积以及兼容性,绝大部分功能都是采用go自带库的进行实现,对性能的占用也能达到一个比较好的效果。 其设计理念和细节值得我们去慢慢学习。 Dddd ==== dddd呢则是更偏向于外网的扫描器,我们也是重点就看他的主要扫描逻辑 ```java // 端口扫描 if len(ips) > 0 { if !structs.GlobalConfig.SkipHostDiscovery { var ICMPAlive []string // ICMP 探测存活 if !structs.GlobalConfig.NoICMPPing { ICMPAlive = common.CheckLive(ips, false) } // TCP 探测存活 var TCPAlive []string if structs.GlobalConfig.TCPPing { // 获取没有存活的进行探测 var uncheck []string for _, ip := range ips { index := utils.GetItemInArray(ICMPAlive, ip) if index == -1 { uncheck = append(uncheck, ip) } } gologger.Info().Msg("TCP存活探测") common.PortScan = false tcpAliveIPPort := common.PortScanTCP(uncheck, "80,443,3389,445,22", structs.GlobalConfig.NoPortString, structs.GlobalConfig.TCPPortScanTimeout) for _, tIPPort := range tcpAliveIPPort { t := strings.Split(tIPPort, ":") TCPAlive = append(TCPAlive, t[0]) } } ``` 首先是进行ICMP的探测存活,包括后面的TCP探测端口扫描,跟进之后发现其实实现代码大致是相同的 ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/10/attach-433c316a4dbd96b86c0dcd84710047a5f188abe5.png) ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/10/attach-b98b6cfeca52fac9a2510f9abccf8a22771bef51.png) 但是他这里比fscan多了一种SYN的扫描,是调用masscan进行SYN端口扫描 ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/10/attach-75aa19120a990429ba849635b07d4c87aec01006.png) 后面对结果经过处理后,调用Httpx进行获取相关ip的http响应 ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/10/attach-06e1955424934e24321e852114baaa795789c937.png) 总结 == 其前期的探测逻辑也就到此为止,后续都是Web扫描,漏洞扫描,目录爆破等等功能,我们留到后面的文章进行分析。 本篇主要是对一些扫描器的代码进行阅读分析,并且从中发现一些借鉴学习的点或者其中一些不足之处,能够进行相关优化的地方,为想要以后自己开发扫描器的师傅提供一点学习的思路。站在巨人的肩膀上走的更远。 参考 == <https://chainreactors.github.io/wiki/gogo> <https://github.com/shadow1ng/fscan> <https://github.com/SleepingBag945/dddd> <https://xz.aliyun.com/t/15318>
原始链接: https://forum.butian.net/share/3841
侵权请联系站方: [email protected]

相关推荐

换一批