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

admin 2024年10月9日14:14:01评论12 views字数 9116阅读30分23秒阅读模式

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

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

Fscan

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

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

若传入的不是文件且包含端口的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. }

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

主要是这个对于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. returnAllIP

  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. returnIPs

  47. }

效果如下

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

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

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

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

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

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

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

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

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

这里利用一个 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 &&IsContain(hostslist, ip){

  7. ExistHosts[ip]=struct{}{}

  8. if common.Silent==false{

  9. ifPing==false{

  10. fmt.Printf("(icmp) Target %-15s is aliven", ip)

  11. }else{

  12. fmt.Printf("(ping) Target %-15s is aliven", 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地址都已探测完成。

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

若是权限不够的话则会选择使用 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. ifExecCommandPing(host){

  9. livewg.Add(1)

  10. chanHosts <- host

  11. }

  12. <-limiter

  13. wg.Done()

  14. }(host)

  15. }

  16. wg.Wait()

  17. }

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

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

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

具体实现,通过建立一个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 :=&net.Dialer{Timeout: timeout}

  3. returnWrapperTCP(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. ifSocks5Proxy==""{

  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的默认直接扫描逻辑

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

跟进 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(&RunOpt.Sum,1)

  9. if result.Port=="137"|| result.Port=="nbt"{

  10. nbtScan(result)

  11. return

  12. }elseif result.Port=="135"|| result.Port=="wmi"{

  13. wmiScan(result)

  14. return

  15. }elseif result.Port=="oxid"{

  16. oxidScan(result)

  17. return

  18. }elseif result.Port=="icmp"|| result.Port=="ping"{

  19. icmpScan(result)

  20. return

  21. }elseif result.Port=="snmp"|| result.Port=="161"{

  22. snmpScan(result)

  23. return

  24. }elseif result.Port=="445"|| result.Port=="smb"{

  25. smbScan(result)

  26. ifRunOpt.Exploit=="ms17010"{

  27. ms17010Scan(result)

  28. }elseifRunOpt.Exploit=="smbghost"||RunOpt.Exploit=="cve-2020-0796"{

  29. smbGhostScan(result)

  30. }elseifRunOpt.Exploit=="auto"||RunOpt.Exploit=="smb"{

  31. ms17010Scan(result)

  32. smbGhostScan(result)

  33. }

  34. return

  35. }elseif result.Port=="mssqlntlm"{

  36. mssqlScan(result)

  37. return

  38. }elseif result.Port=="winrm"{

  39. winrmScan(result)

  40. return

  41. }else{

  42. initScan(result)

  43. }

  44. ....

  45. ...

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

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

跟进到默认的initScan

  1. func initScan(result *pkg.Result){

  2. var bs []byte

  3. target := result.GetTarget()

  4. if pkg.ProxyUrl!= nil && 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. ifRunOpt.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.1rnHost: %srnrn", 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")&& bytes.Contains(result.Content,[]byte("location: https"))){

  53. systemHttp(result,"https")

  54. }elseif 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 :=&Socket{

  3. Timeout: time.Duration(delay)* time.Second,

  4. }

  5. var conn net.Conn

  6. var err error

  7. ifProxyDialTimeout!= 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 Socketstruct{

  19. Conn net.Conn

  20. Countint

  21. Timeout time.Duration

  22. }

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

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

可以发现,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探测端口扫描,跟进之后发现其实实现代码大致是相同的

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

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

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

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

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

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

总结

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

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

参考

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

https://github.com/shadow1ng/fscan

https://github.com/SleepingBag945/dddd

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

原文始发于微信公众号(稻草人安全团队):扫描器解析日记之目标探测

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年10月9日14:14:01
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   扫描器解析日记之目标探测https://cn-sec.com/archives/3245651.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息