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

开发和代码是作为一名安全人员不可或缺的能力,而我们现在学习就可以以前人开发的工具入手,学习其代码逻辑,设计理念等等。从而写出更好的属于自己的工具。

本篇文章以几款扫描器为例,分析其前期对目标探测的模块进行入手学习。

Fscan

在读取完各种参数后,进入到解析ip中

image.png
若传入的不是文件且包含端口的ip,则先分割出ip和port然后丢入ParseIPs​进行解析,如果没有携带端口则直接进入ParseIPs,若是文件则进行文件处理后再解析,所以我们跟进到ParseIPs​中

  1. func ParseIP(host string, filename string, nohosts ...string) (hosts []string, err error) {
  2. if filename == "" && strings.Contains(host, ":") {
  3. //192.168.0.0/16:80
  4. hostport := strings.Split(host, ":")
  5. if len(hostport) == 2 {
  6. host = hostport[0]
  7. hosts = ParseIPs(host)
  8. Ports = hostport[1]
  9. }
  10. } else {
  11. hosts = ParseIPs(host)
  12. if filename != "" {
  13. var filehost []string
  14. filehost, _ = Readipfile(filename)
  15. hosts = append(hosts, filehost...)
  16. }
  17. }
  18. //nohosts不扫描的ip
  19. //..篇幅省略
  20. //去重
  21. hosts = RemoveDuplicate(hosts)
  22. if len(hosts) == 0 && len(HostPort) == 0 && host != "" && filename != "" {
  23. err = ParseIPErr
  24. }
  25. return
  26. }

image.png

主要是这个对于A段扫描时处理不够完善,fscan为了避免扫描过多的ip采用了随机扫描的方式,如果用户就是需要扫描整个/8网段,则可能会遗漏,我们可以对其修改如下

  1. func parseIP(ip string) []string {
  2. reg := regexp.MustCompile(`[a-zA-Z]+`)
  3. switch {
  4. case ip == "192":
  5. return parseIP("192.168.0.0/8")
  6. case ip == "172":
  7. return parseIP("172.16.0.0/12")
  8. case ip == "10":
  9. return parseIP("10.0.0.0/8")
  10. // 扫描/8时,只扫网关和随机IP,避免扫描过多IP
  11. case strings.HasSuffix(ip, "/8"):
  12. return parseIP8(ip)
  13. //解析 /24 /16 /8 /xxx 等
  14. case strings.Contains(ip, "/"):
  15. return parseIP2(ip)
  16. //可能是域名,用lookup获取ip
  17. case reg.MatchString(ip):
  18. // _, err := net.LookupHost(ip)
  19. // if err != nil {
  20. // return nil
  21. // }
  22. return []string{ip}
  23. //192.168.1.1-192.168.1.100
  24. case strings.Contains(ip, "-"):
  25. return parseIP1(ip)
  26. //处理单个ip
  27. default:
  28. testIP := net.ParseIP(ip)
  29. if testIP == nil {
  30. return nil
  31. }
  32. return []string{ip}
  33. }
  34. }

这里我直接就参考dddd的写法改写了

  1. func parseIP8(ip string) []string {
  2. var AllIP []string
  3. for _, i := range CIDRToIP(ip) {
  4. AllIP = append(AllIP, i.String())
  5. }
  6. return AllIP
  7. }
  8. func CIDRToIP(cidr string) (IPs []net.IP) {
  9. _, network, _ := net.ParseCIDR(cidr)
  10. first := FirstIP(network)
  11. last := LastIP(network)
  12. return pairsToIP(first, last)
  13. }
  14. func FirstIP(network *net.IPNet) net.IP {
  15. return network.IP
  16. }
  17. func LastIP(network *net.IPNet) net.IP {
  18. firstIP := FirstIP(network)
  19. mask, _ := network.Mask.Size()
  20. size := math.Pow(2, float64(32-mask))
  21. lastIP := toIP(toInt(firstIP) + uint32(size) - 1)
  22. return net.ParseIP(lastIP)
  23. }
  24. func toIP(i uint32) string {
  25. buf := bytes.NewBuffer([]byte{})
  26. _ = binary.Write(buf, binary.BigEndian, i)
  27. b := buf.Bytes()
  28. return fmt.Sprintf("%v.%v.%v.%v", b[0], b[1], b[2], b[3])
  29. }
  30. func toInt(ip net.IP) uint32 {
  31. var buf = []byte(ip)
  32. if len(buf) > 12 {
  33. buf = buf[12:]
  34. }
  35. buffer := bytes.NewBuffer(buf)
  36. var i uint32
  37. _ = binary.Read(buffer, binary.BigEndian, &i)
  38. return i
  39. }
  40. func pairsToIP(ip1, ip2 net.IP) (IPs []net.IP) {
  41. start := toInt(ip1)
  42. end := toInt(ip2)
  43. for i := start; i <= end; i++ {
  44. IPs = append(IPs, net.ParseIP(toIP(i)))
  45. }
  46. return IPs
  47. }

效果如下

image.png

接着就是初始化一些http客户端的参数

web poc的线程数 ThreadsNum​、代理类型 DownProxy​ 和超时时间 Timeout​。主要目的是配置一个 http.Client​ 实例,在进行 HTTP 请求时使用指定的代理和超时设置

image.png
如果noping参数为false,或者扫描参数为icmp则进行CheckLive​的存活探测

​common.LogWG.Wait()​这个是日志同步,等待所有日志记录操作完成。确保所有日志记录操作都已完成,避免日志丢失。
image.png

image.png

监听一个通道(chanHosts​)以接收IP地址。当接收到一个IP地址时,它检查该IP是否不在ExistHosts​映射中,并且是否在hostslist​中。如果两个条件都为真,则将IP添加到ExistHosts​和AliveHosts​中,并打印一条消息。

这里利用一个ExistHosts[ip] = struct{}{}​

以利用Go语言的映射(map)特性来实现一个集合(set)的功能。由于映射中的值可以是任意类型,而这里使用的是空结构体 struct{}{}​,所以实际上我们并不关心值本身,而是关心键(即IP地址)是否存在。

通过这种方式,我们可以快速地检查一个IP地址是否已经存在于 ExistHosts​ 中,如果存在,则不需要再次添加。同时,由于空结构体不占用任何内存空间,所以这种做法也非常节省内存。

  1. func CheckLive(hostslist []string, Ping bool) []string {
  2. //创建一个缓冲通道,容量为 hostslist 的长度
  3. chanHosts := make(chan string, len(hostslist))
  4. go func() {
  5. for ip := range chanHosts {
  6. if _, ok := ExistHosts[ip]; !ok &amp;&amp; IsContain(hostslist, ip) {
  7. ExistHosts[ip] = struct{}{}
  8. if common.Silent == false {
  9. if Ping == false {
  10. fmt.Printf("(icmp) Target %-15s is alive\n", ip)
  11. } else {
  12. fmt.Printf("(ping) Target %-15s is alive\n", ip)
  13. }
  14. }
  15. AliveHosts = append(AliveHosts, ip)
  16. }
  17. livewg.Done()
  18. }
  19. }()

下面就是选择用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

若是权限不够的话则会选择使用RunPing​进行探测

具体实现在ExecCommandPing​函数

根据回显,返回 true​ 则 ping 成功,否则返回 false​。

  1. func RunPing(hostslist []string, chanHosts chan string) {
  2. var wg sync.WaitGroup
  3. limiter := make(chan struct{}, 50)
  4. for _, host := range hostslist {
  5. wg.Add(1)
  6. limiter <- struct{}{}
  7. go func(host string) {
  8. if ExecCommandPing(host) {
  9. livewg.Add(1)
  10. chanHosts <- host
  11. }
  12. <-limiter
  13. wg.Done()
  14. }(host)
  15. }
  16. wg.Wait()
  17. }

image.png

在某些环境配置中会有echo 1 > /proc/sys/net/ipv4/icmp_echo_ignore_all​ 这种就不会返回任何ICMP的响应。而fscan默认的扫描策略强依赖于ICMP协议,所以有可能在一些情况漏掉部分资产。

image.png

具体实现,通过建立一个TCP的连接,如果连接成功则会记录一个成功消息并将主机地址发送到一个通道。

  1. func PortConnect(addr Addr, respondingHosts chan<- string, adjustedTimeout int64, wg *sync.WaitGroup) {
  2. host, port := addr.ip, addr.port
  3. conn, err := common.WrapperTcpWithTimeout("tcp4", fmt.Sprintf("%s:%v", host, port), time.Duration(adjustedTimeout)*time.Second)
  4. if err == nil {
  5. defer conn.Close()
  6. address := host + ":" + strconv.Itoa(port)
  7. result := fmt.Sprintf("%s open", address)
  8. common.LogSuccess(result)
  9. wg.Add(1)
  10. respondingHosts <- address
  11. }

​WrapperTcpWithTimeout​实现了一个TCP的包装器

  1. func WrapperTcpWithTimeout(network, address string, timeout time.Duration) (net.Conn, error) {
  2. d := &amp;net.Dialer{Timeout: timeout}
  3. return WrapperTCP(network, address, d)
  4. }
  5. func WrapperTCP(network, address string, forward *net.Dialer) (net.Conn, error) {
  6. //get conn
  7. var conn net.Conn
  8. if Socks5Proxy == "" {
  9. var err error
  10. conn, err = forward.Dial(network, address)
  11. if err != nil {
  12. return nil, err
  13. }
  14. } else {
  15. dailer, err := Socks5Dailer(forward)
  16. if err != nil {
  17. return nil, err
  18. }
  19. conn, err = dailer.Dial(network, address)
  20. if err != nil {
  21. return nil, err
  22. }
  23. }
  24. return conn, nil
  25. }

GoGo

看完了fscan的探测逻辑,接下来可以看看其他工具的这部分的相关逻辑,这里拿dddd以及gogo为例

直接跟进到gogo的默认直接扫描逻辑

image.png

跟进plugin.Dispatch(result)​

  1. func Dispatch(result *pkg.Result) {
  2. defer func() {
  3. if err := recover(); err != nil {
  4. logs.Log.Errorf("scan %s unexcept error, %v", result.GetTarget(), err)
  5. panic(err)
  6. }
  7. }()
  8. atomic.AddInt32(&amp;RunOpt.Sum, 1)
  9. if result.Port == "137" || result.Port == "nbt" {
  10. nbtScan(result)
  11. return
  12. } else if result.Port == "135" || result.Port == "wmi" {
  13. wmiScan(result)
  14. return
  15. } else if result.Port == "oxid" {
  16. oxidScan(result)
  17. return
  18. } else if result.Port == "icmp" || result.Port == "ping" {
  19. icmpScan(result)
  20. return
  21. } else if result.Port == "snmp" || result.Port == "161" {
  22. snmpScan(result)
  23. return
  24. } else if result.Port == "445" || result.Port == "smb" {
  25. smbScan(result)
  26. if RunOpt.Exploit == "ms17010" {
  27. ms17010Scan(result)
  28. } else if RunOpt.Exploit == "smbghost" || RunOpt.Exploit == "cve-2020-0796" {
  29. smbGhostScan(result)
  30. } else if RunOpt.Exploit == "auto" || RunOpt.Exploit == "smb" {
  31. ms17010Scan(result)
  32. smbGhostScan(result)
  33. }
  34. return
  35. } else if result.Port == "mssqlntlm" {
  36. mssqlScan(result)
  37. return
  38. } else if result.Port == "winrm" {
  39. winrmScan(result)
  40. return
  41. } else {
  42. initScan(result)
  43. }
  44. ....
  45. ...

可以看到,它对一些特定端口服务针对性的做了一些定制化的扫描,比如135(wmi)、161(snmp), 一般大部分情况来说是是不会有其他情况占用的。

image.png

跟进到默认的initScan

  1. func initScan(result *pkg.Result) {
  2. var bs []byte
  3. target := result.GetTarget()
  4. if pkg.ProxyUrl != nil &amp;&amp; strings.HasPrefix(pkg.ProxyUrl.Scheme, "http") {
  5. // 如果是http代理, 则使用http库代替socket
  6. conn := result.GetHttpConn(RunOpt.Delay)
  7. resp, err := pkg.HTTPGet(conn, "http://"+target)
  8. if err != nil {
  9. return
  10. }
  11. if err != nil {
  12. result.Err = err
  13. return
  14. }
  15. result.Open = true
  16. pkg.CollectHttpResponse(result, resp)
  17. } else {
  18. defer func() {
  19. // 如果进行了各种探测依旧为tcp协议, 则收集tcp端口状态
  20. if result.Protocol == "tcp" {
  21. if result.Err != nil {
  22. result.Error = result.Err.Error()
  23. if RunOpt.Debug {
  24. result.ErrStat = handleError(result.Err)
  25. }
  26. }
  27. }
  28. }()
  29. conn, err := pkg.NewSocket("tcp", target, RunOpt.Delay)
  30. if err != nil {
  31. result.Err = err
  32. return
  33. }
  34. defer conn.Close()
  35. result.Open = true
  36. // 启发式扫描探测直接返回不需要后续处理
  37. if result.SmartProbe {
  38. return
  39. }
  40. result.Status = "open"
  41. bs, err = conn.Read(RunOpt.Delay)
  42. if err != nil {
  43. senddataStr := fmt.Sprintf("GET /%s HTTP/1.1\r\nHost: %s\r\n\r\n", result.Uri, target)
  44. bs, err = conn.Request([]byte(senddataStr), DefaultMaxSize)
  45. if err != nil {
  46. result.Err = err
  47. }
  48. }
  49. pkg.CollectSocketResponse(result, bs)
  50. }
  51. //所有30x,400,以及非http协议的开放端口都送到http包尝试获取更多信息
  52. if result.Status == "400" || result.Protocol == "tcp" || (strings.HasPrefix(result.Status, "3") &amp;&amp; bytes.Contains(result.Content, []byte("location: https"))) {
  53. systemHttp(result, "https")
  54. } else if strings.HasPrefix(result.Status, "3") {
  55. systemHttp(result, "http")
  56. }
  57. return
  58. }

这里不关注代理功能先,我们看默认是进入了pkg.NewSocket进行探测,封装了一个Socket的结构体,使用了go自带的net库实现的TCP的连接

  1. func NewSocket(network, target string, delay int) (*Socket, error) {
  2. s := &amp;Socket{
  3. Timeout: time.Duration(delay) * time.Second,
  4. }
  5. var conn net.Conn
  6. var err error
  7. if ProxyDialTimeout != nil {
  8. conn, err = ProxyDialTimeout(network, target, s.Timeout)
  9. } else {
  10. conn, err = net.DialTimeout(network, target, s.Timeout)
  11. }
  12. if err != nil {
  13. return nil, err
  14. }
  15. s.Conn = conn
  16. return s, nil
  17. }
  18. type Socket struct {
  19. Conn net.Conn
  20. Count int
  21. Timeout time.Duration
  22. }

最后还会将所有的30x,400,以及非http协议的开放端口都送到http包尝试获取更多信息。

image.png

可以发现,gogo的话其实为了尽可能的优化体积以及兼容性,绝大部分功能都是采用go自带库的进行实现,对性能的占用也能达到一个比较好的效果。

其设计理念和细节值得我们去慢慢学习。

Dddd

dddd呢则是更偏向于外网的扫描器,我们也是重点就看他的主要扫描逻辑

  1. // 端口扫描
  2. if len(ips) > 0 {
  3. if !structs.GlobalConfig.SkipHostDiscovery {
  4. var ICMPAlive []string
  5. // ICMP 探测存活
  6. if !structs.GlobalConfig.NoICMPPing {
  7. ICMPAlive = common.CheckLive(ips, false)
  8. }
  9. // TCP 探测存活
  10. var TCPAlive []string
  11. if structs.GlobalConfig.TCPPing {
  12. // 获取没有存活的进行探测
  13. var uncheck []string
  14. for _, ip := range ips {
  15. index := utils.GetItemInArray(ICMPAlive, ip)
  16. if index == -1 {
  17. uncheck = append(uncheck, ip)
  18. }
  19. }
  20. gologger.Info().Msg("TCP存活探测")
  21. common.PortScan = false
  22. tcpAliveIPPort := common.PortScanTCP(uncheck, "80,443,3389,445,22",
  23. structs.GlobalConfig.NoPortString,
  24. structs.GlobalConfig.TCPPortScanTimeout)
  25. for _, tIPPort := range tcpAliveIPPort {
  26. t := strings.Split(tIPPort, ":")
  27. TCPAlive = append(TCPAlive, t[0])
  28. }
  29. }

首先是进行ICMP的探测存活,包括后面的TCP探测端口扫描,跟进之后发现其实实现代码大致是相同的

image.png

image.png

但是他这里比fscan多了一种SYN的扫描,是调用masscan进行SYN端口扫描

image.png

后面对结果经过处理后,调用Httpx进行获取相关ip的http响应

image.png

总结

其前期的探测逻辑也就到此为止,后续都是Web扫描,漏洞扫描,目录爆破等等功能,我们留到后面的文章进行分析。

本篇主要是对一些扫描器的代码进行阅读分析,并且从中发现一些借鉴学习的点或者其中一些不足之处,能够进行相关优化的地方,为想要以后自己开发扫描器的师傅提供一点学习的思路。站在巨人的肩膀上走的更远。

参考

https://chainreactors.github.io/wiki/gogo

https://github.com/shadow1ng/fscan

https://github.com/SleepingBag945/dddd

https://xz.aliyun.com/t/15318

  • 发表于 2024-11-04 09:00:01
  • 阅读 ( 2608 )
  • 分类:安全开发

2 条评论

Qiu_
Qiu_

4 篇文章

站长统计