安全工具 fscan 源码分析
2023-6-18 00:1:44 Author: 白帽子(查看原文) 阅读量:71 收藏

fscan源码学

免责声明:本公众号所提供的文字和信息仅供学习和研究使用,不得用于任何非法用途。我们强烈谴责任何非法活动,并严格遵守法律法规。读者应该自觉遵守法律法规,不得利用本公众号所提供的信息从事任何违法活动。本公众号不对读者的任何违法行为承担任何责任。

1.0分析

工具地址: https://github.com/shadow1ng/fscan/tree/1.0

这篇文章主要内容是对fscan1.0版本的源代码的一个学习,除了特定漏洞的源代码没有进行注释以外,其他的主要的代码都添加了注释,能力有限,有些地方分析的可能会有错误,还望师傅们多多包涵。

个人感觉对fscan1.0源码通读一遍之后,对fscan这个工具也比以前更加熟悉,对于自己输入某个命令的时候,工具会以哪种状态运行会更清楚,感谢shadow1ng师傅

附上一个不严谨的过程图,仅供参考

目录结构
│  main.go
│ README.md
├─common
│ config.go
│ flag.go
│ log.go
│ Parse.go
│ ParseIP.go
│ ParsePort.go
└─Plugins
base.go
CVE-2020-0796.go
elasticsearch.go
findnet.go
ftp.go
icmp.go
memcached.go
mongodb.go
ms17017.go
mssql.go
mysql.go
portscan.go
postgres.go
redis.go
scanner.go
smb.go
ssh.go
webtitle.go
main.go
// 导入所需的包
package main

import (
"./Plugins" // 导入自定义的插件包
"./common" // 导入公共函数包
"fmt" // 导入 fmt 包
)

func main() {
var Info common.HostInfo // 定义一个结构体类型的变量 Info,用于保存主机信息
common.Flag(&Info) // 解析命令行参数,获取主机地址和端口信息
common.Parse(&Info) // 解析主机地址和端口信息,保存到 Info 变量中
Plugins.Scan(&Info) // 使用 Plugins 包中的 Scan 函数进行扫描
fmt.Println("scan end") // 输出扫描结束提示信息
}

  • main.go文件

    • 这个文件通过调用三个函数实现了命令行参数的获取,参数解析,扫描功能,通过此文件可以看出软件主要的运行过程:加载参数 ->  解析参数 -> 进行扫描

    • var Info common.HostInfo

    • 此结构体在 config.go 文件中进行了定义,主要作用是各种参数变量的定义

    • common.Flag(&Info)

    • 此函数在 common/flag.go 中进行了定义,主要作用就是通过flag库对命令行的参数进行一个获取,并赋值到 common.HostInfo 这个结果体中

    • common.Parse(&Info)

    • 此函数在 common/Parse.go中进行了定义,主要作用为 对用户名字典、密码字典、扫描主机,扫描类型进行一个处理,后面会详细分析

    • Plugins.Scan(&Info)

    • 此函数在 Plugins/scanner.go 中进行了定义,主要作用为通过传入 *common.HostInfo 参数,也就是最前面定义好并且进行了参数处理的结构体变量;首先判断是否进行 icmp 存活探测,然后根据扫描类型字段:Scantype来决定使用哪种扫描方式,也就是扫哪些漏洞或者端口,后面会详细分析。

common文件夹

config.go
// Userdict 是一个映射服务名到常用用户名的映射表
var Userdict = map[string][]string{
"ftp": {"www","admin","root","db","wwwroot","data","web","ftp"},
"mysql": {"root"},
"mssql": {"root","sa"},
"smb": {"administrator","guest"},
"postgresql": {"postgres","admin"},
"ssh": {"root","admin"},
"mongodb": {"root","admin"},
//"telnet": []string{"administrator","admin","root","cisco","huawei","zte"},
}

// Passwords 是一个常用密码的列表
var Passwords = []string{"admin123A","admin123","123456","admin","root","password","123123","654321","123","1","[email protected]","[email protected]","{user}","{user}123","","[email protected]!","qwa123","12345678","test","[email protected]#","123456789","123321","666666","fuckyou","000000","1234567890","8888888","qwerty","1qaz2wsx","abc123","abc123456","[email protected]","Aa123456","sysadmin","system","huawei"}

// PORTList 是一个映射服务名到默认端口号的映射表
var PORTList = map[string]int{
"ftp": 21,
"ssh": 22,
"mem": 11211,
"mgo": 27017,
"mssql": 1433,
"psql": 5432,
"redis": 6379,
"mysql": 3306,
"smb": 445,
"ms17010": 1000001,
"cve20200796":1000002,
"elastic": 9200,
"findnet": 135,
"all":0,
//"wenscan": 17010,
}

// Outputfile 是扫描结果输出文件的文件名
var Outputfile = "result.txt"

// IsSave 表示是否保存扫描结果
var IsSave = true

// DefaultPorts 是默认的端口号列表
var DefaultPorts = "21,22,23,80,135,443,445,1433,1521,3306,5432,6379,7001,8080,8089,9000,9200,11211,27017"

// HostInfo 是一个结构体,用于存储扫描主机所需要的信息
type HostInfo struct {
Host string // 主机地址
Ports string // 端口号列表
Url string // 目标URL
Timeout int64 // 扫描超时时间
Scantype string // 扫描类型
Isping bool // 是否启用 ping 检测
Threads int // 扫描线程数
Command string // 执行命令
Username string // 用户名
Password string // 密码
Userfile string // 用户名文件
Passfile string // 密码文件
Usernames []string // 用户名列表
Passwords []string // 密码列表
Outputfile string // 结果保存文件的文件名
IsSave bool // 是否保存结果
RedisFile string // 应该是redis相关文件
RedisShell string // redis漏洞利用写入的shell
}

  • 这个文件主要是对一些端口,服务常用用户名字典,密码字典,默认扫描端口以及 Hostinfo 结构体进行一个定义,如果想自己进行一个编译,可以按需加一些默认密码,默认扫描的端口之类的;对于 PORTList 这个map变量,严谨来说并不是服务和端口的映射,感觉用服务和编号映射感觉更准确一点,比如:ms17010 就没写端口后,而是一串数字(随便说说)。

flag.go
package common

import (
"flag" // 导入flag包,用于处理命令行参数
)

// Banner函数用于输出fscan的Banner
func Banner(){
banner := `
___ _
/ _ \ ___ ___ _ __ __ _ ___| | __
/ /_\/____/ __|/ __| '__/ _`+"`"+` |/ __| |/ /
/ /_\\_____\__ \ (__| | | (_| | (__| <
\____/ |___/\___|_| \__,_|\___|_|\_\
`
print(banner) // 输出fscan的Banner
}

// Flag函数用于处理命令行参数,将参数值赋给HostInfo结构体的对应字段
func Flag(Info *HostInfo) {
Banner() // 输出fscan的Banner
// 使用flag包定义命令行参数
flag.StringVar(&Info.Host,"h","","IP address of the host you want to scan,for example: 192.168.11.11 | 192.168.11.11-255 | 192.168.11.11,192.168.11.12")
flag.StringVar(&Info.Ports,"p",DefaultPorts,"Select a port,for example: 22 | 1-65535 | 22,80,3306")
flag.StringVar(&Info.Command,"c","","exec command (ssh)")
flag.IntVar(&Info.Threads,"t",100,"Thread nums")
flag.BoolVar(&Info.Isping,"np",false,"not to ping")
flag.BoolVar(&Info.IsSave,"no",false,"not to save output log")
flag.StringVar(&Info.Username,"user","","username")
flag.StringVar(&Info.Userfile,"userf","","username file")
flag.StringVar(&Info.Password,"pwd","","password")
flag.StringVar(&Info.Passfile,"pwdf","","password file")
flag.StringVar(&Info.Outputfile,"o","result.txt","Outputfile")
flag.Int64Var(&Info.Timeout,"time",3,"Set timeout")
flag.StringVar(&Info.Scantype,"m","all","Select scan type ,as: -m ssh")
flag.StringVar(&Info.RedisFile,"rf","","redis file to write sshkey file (as: -rf id_rsa.pub) ")
flag.StringVar(&Info.RedisFile,"rs","","redis shell to write cron file (as: -rs 192.168.1.1:6666) ")
flag.Parse() // 解析命令行参数,并将参数值赋给对应的变量
}

  • 使用flag包处理命令行参数,将参数值赋给HostInfo结构体的对应字段。通过调用Flag函数,可以在命令行中指定扫描目标的IP地址、端口、用户名、密码等信息,从而实现fscan工具的配置;

  • 这个文件中可以关注一下某些参数的默认值,比如线程数,默认扫描所有模块等等,最新版本加了不少参数,比如线程调成了600;通过对这些参数的情况的了解,可以更加清楚的知道自己所用的命令参数将使fscan在什么样的情况下运行。

log.go
// LogSuccess 函数用于打印扫描成功的结果,并将其写入文件(如果指定了保存文件)
// 参数 result 为扫描成功的结果字符串
func LogSuccess(result string){
// 创建一个互斥锁,用于在多线程情况下控制并发访问
mutex := &sync.Mutex{}
mutex.Lock()
// 打印扫描结果
fmt.Println(result)
// 如果指定了保存文件,则将结果写入文件
if IsSave {
WriteFile(result,Outputfile)
}
mutex.Unlock()
}
// WriteFile 函数用于将扫描结果写入指定的文件中
// 参数 result 为要写入的扫描结果字符串
// 参数 filename 为指定的文件名
func WriteFile(result string,filename string) {
// 创建一个字节数组,保存要写入的字符串
var text = []byte(result+"\n")
// 打开指定的文件,如果文件不存在则创建一个新文件,文件权限设置为 0777
fl, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE, 0777)
if err != nil {
fmt.Println(err)
return
}
// 关闭文件句柄
defer fl.Close()
// 将字符串写入文件
_, err = fl.Write(text)
if err!= nil{
fmt.Println(err)
}
}
  • 这串代码主要作用是记录日志信息,通过互斥锁在并发的环境下控制并发访问文件

Parse.go
package common

import (
"bufio"
"flag"
"fmt"
"os"
"strconv"
"strings"
)

// Parse函数负责解析命令行参数并填充HostInfo结构
func Parse(Info *HostInfo) {
ParseUser(Info) // 解析用户名(们)
ParsePass(Info) // 解析密码(们)
ParseInput(Info) // 解析输入参数
ParseScantype(Info) // 解析扫描类型
}

// 解析用户名
func ParseUser(Info *HostInfo) {
// 如果用户名不为空
if Info.Username != "" {
// 分割多个用户名
users := strings.Split(Info.Username, ",")
for _, user := range users {
if user != "" {
Info.Usernames = append(Info.Usernames, user)
}
}
// 将用户名列表分配给全局变量Userdict
for name := range Userdict {
Userdict[name] = Info.Usernames
}
}
// 如果用户名文件不为空
if Info.Userfile != "" {
users, err := Readfile(Info.Userfile)
if err == nil {
for _, user := range users {
if user != "" {
Info.Usernames = append(Info.Usernames, user)
}
}
// 将用户名列表分配给全局变量Userdict
for name := range Userdict {
Userdict[name] = Info.Usernames
}
}
}
}

// 解析密码
func ParsePass(Info *HostInfo) {
// 如果密码不为空
if Info.Password != "" {
// 分割多个密码
passes := strings.Split(Info.Password, ",")
for _, pass := range passes {
if pass != "" {
Info.Passwords = append(Info.Passwords, pass)
}
}
// 将密码列表分配给全局变量Passwords
Passwords = Info.Passwords
}
// 如果密码文件不为空
if Info.Passfile != "" {
passes, err := Readfile(Info.Passfile)
if err == nil {
for _, pass := range passes {
if pass != "" {
Info.Passwords = append(Info.Passwords, pass)
}
}
// 将密码列表分配给全局变量Passwords
Passwords = Info.Passwords
}
}
}

// 读取文件内容并返回一个字符串切片
func Readfile(filename string) ([]string, error) {
file, err := os.Open(filename)
if err != nil {
fmt.Println("打开文件失败:", filename, err)
return nil, err
}
defer file.Close()
var content []string
scanner := bufio.NewScanner(file)
scanner.Split(bufio.ScanLines)
for scanner.Scan() {
text := strings.TrimSpace(scanner.Text())
if text != "" {
content = append(content, scanner.Text())
}
}
return content, nil
}

// ParseInput 函数解析 HostInfo 结构体中的输入参数。
// 如果 Host 字段为空,则打印错误信息并退出程序。
// 如果 Outputfile 字段非空,则将其值赋给全局变量 Outputfile。
// 如果 IsSave 字段为 true,则将其值赋给全局变量 IsSave。
func ParseInput(Info *HostInfo){
if Info.Host==""{
fmt.Println("Host is none")
flag.Usage()
os.Exit(0)
}
if Info.Outputfile != ""{
Outputfile = Info.Outputfile
}
if Info.IsSave == true{
IsSave = false
}
}

// ParseScantype 函数解析 HostInfo 结构体中的扫描类型参数。
// 如果指定的扫描类型不存在,则打印错误信息并退出程序。
// 如果指定了 -m 选项并且指定了 -p 选项,则只扫描指定端口的第一个端口,
// 并将其值赋给全局变量 PORTList 中的所有端口。
func ParseScantype(Info *HostInfo){
_,ok:=PORTList[Info.Scantype]
if !ok{
fmt.Println("The specified scan type does not exist")
fmt.Println("-m")
for name,_:=range PORTList{
fmt.Println(" ["+name+"]")
}
os.Exit(0)
}
if Info.Scantype != "all" && Info.Ports != DefaultPorts{
ScanPort := ParsePort(Info.Ports)[0]
Info.Ports = strconv.Itoa(ScanPort)
fmt.Println("if -m and -p only scan the first port:",Info.Ports)
for name,_:=range PORTList{
PORTList[name] = ScanPort
}
}
}

  • 这个文件中的代码主要是对用户名字典,密码字典,输入的目标主机,以及扫描的漏洞和端口信息进行一个解析处理,将命令行中输入的内容,赋值到HostInfo这个结构体的属性中。

  • 对于用户文件和密码文件,处理流程是:如果指定了用户名或者用户名文件,那么久会把这些用户名一个一个的添加到 "HostInfo.Username" 这个切片当中,然后再将:HostInfo.Username当中的所有用户名赋值到 config.go 这个文件当中定义的那个 "Userdict" 这个 map(字典) 的每一个key的vlaue中,简单来说就是将用户想指定的用户名赋值到工具原本的用户名字典中,并且是所有服务的用户名字典当中;最终的效果就是:所有涉及到账号爆破的服务,其爆破的用户名都是用户指定的字典。密码的处理和用户的处理差不多,只不过用户指定的密码是赋值给一个切片。

  • Readfile() 函数返回一个切片,同时读取文件的时候,是按行读取,并且通过scanner.Scan()和strings.TrimSpace() 这两个函数,扫描文件内容,对于不是空白行的就去出开头和最后的空格,避免了空白行和空格的干扰。

  • ParseScantype() 这个函数是处理 -m 参数和 -p 参数,主要是决定扫描什么漏洞;首先是判断用户的 -m 参数指定的字符是否和config中的 PORTList的kay对应,如果未找到,就报错并停止运行,说明用户想扫描的服务没有对应的功能。如果同时指定了 -m 和 -p 那么就可以实现扫描指定服务非默认端口。

ParsePort.go
// ParsePort函数将传入的端口参数解析成端口数组
// 参数ports为以逗号分隔的端口字符串,可以包含单个端口和端口范围(如1-100)
// 函数返回一个int类型的数组,包含所有解析出的端口
func ParsePort(ports string) []int {
var scanPorts []int
slices := strings.Split(ports, ",") // 将传入的端口字符串按逗号分隔成切片
for _, port := range slices {
port = strings.Trim(port, " ") // 去掉端口字符串中的空格
upper := port
if strings.Contains(port, "-") { // 判断端口字符串是否包含范围符号“-”
ranges := strings.Split(port, "-") // 如果包含范围符号“-”,则将其拆分成两个端口字符串
if len(ranges) < 2 { // 如果拆分后的切片长度小于2,表示无法解析成端口范围,跳过本次循环
continue
}
sort.Strings(ranges) // 将两个端口字符串按字符串排序
port = ranges[0] // 取得排序后的第一个端口字符串作为起始端口
upper = ranges[1] // 取得排序后的第二个端口字符串作为结束端口
}
start, _ := strconv.Atoi(port) // 将起始端口字符串转换成整型
end, _ := strconv.Atoi(upper) // 将结束端口字符串转换成整型
for i := start; i <= end; i++ { // 将起始端口到结束端口的所有端口号加入端口数组
scanPorts = append(scanPorts, i)
}
}
return scanPorts
}
  • 输入的端口可能会是:80,443 或者 1-100,这个函数就是处理这些格式的,最终返回一个存有端口的切片。

ParseIP.go
package common

import (
"errors"
"net"
"regexp"
"strconv"
"strings"
)

// 定义一个全局变量,表示 IP 解析错误
var ParseIPErr error = errors.New("host parsing error\n" +
"format: \n" +
"192.168.1.1/24\n" +
"192.168.1.1\n" +
"192.168.1.1,192.168.1.2\n" +
"192.168.1.1-255")

// 解析 IP,返回一个 IP 切片和错误信息
func ParseIP(ip string) ([]string, error) {
// 正则表达式,用于匹配字符串中是否有字母
reg := regexp.MustCompile(`[a-zA-Z]+`)
switch {
case strings.Contains(ip[len(ip)-3:len(ip)], "/24"):
return ParseIPA(ip)
case strings.Contains(ip[len(ip)-3:len(ip)], "/16"):
return ParseIPD(ip)
case strings.Contains(ip[len(ip)-2:len(ip)], "/8"):
return ParseIPE(ip)
case strings.Contains(ip, ","):
return ParseIPB(ip)
case strings.Count(ip, "-") == 1:
return ParseIPC(ip)
case reg.MatchString(ip):
// 如果字符串中有字母,则尝试解析为 IP
_, err := net.LookupHost(ip)
if err != nil {
return nil, err
}
return []string{ip}, nil
default:
// 尝试将字符串解析为 IP
testIP := net.ParseIP(ip)
if testIP == nil {
return nil, ParseIPErr
}
return []string{ip}, nil
}
}

// 解析 CIDR 格式的 IP
func ParseIPA(ip string) ([]string, error) {
realIP := ip[:len(ip)-3]
testIP := net.ParseIP(realIP)

if testIP == nil {
return nil, ParseIPErr
}

IPrange := strings.Join(strings.Split(realIP, ".")[0:3], ".")
var AllIP []string
for i := 0; i <= 255; i++ {
AllIP = append(AllIP, IPrange+"."+strconv.Itoa(i))
}
return AllIP, nil
}

// 解析 IP 段,例如:192.168.111.1,192.168.111.2
func ParseIPB(ip string) ([]string, error) {
IPList := strings.Split(ip, ",")
for _, i := range IPList {
testIP := net.ParseIP(i)
if testIP == nil {
return nil, ParseIPErr
}
}
return IPList, nil

}

// 解析一个 IP 地址范围,例如:192.168.111.1-255
func ParseIPC(ip string) ([]string, error) {
// 分割 IP 地址和范围
IPRange := strings.Split(ip, "-")
// 解析第一个 IP 地址
testIP := net.ParseIP(IPRange[0])
// 解析 IP 范围,若超过 255 或者解析出错,则返回错误
Range, err := strconv.Atoi(IPRange[1])
if testIP == nil || Range > 255 || err != nil {
return nil, ParseIPErr
}
// 分割 IP 地址的每个段
SplitIP := strings.Split(IPRange[0], ".")
ip1, err1 := strconv.Atoi(SplitIP[3])
ip2, err2 := strconv.Atoi(IPRange[1])
// 检查 IP 范围是否合法,若不合法,则返回错误
PrefixIP := strings.Join(SplitIP[0:3], ".")
if ip1 > ip2 || err1 != nil || err2 != nil {
return nil, ParseIPErr
}
// 构造 IP 地址列表
var AllIP []string
for i := ip1; i <= ip2; i++ {
AllIP = append(AllIP, PrefixIP+"."+strconv.Itoa(i))
}
return AllIP, nil
}

// 解析一个 IP 地址段,例如:192.168.0.0/16
func ParseIPD(ip string) ([]string, error) {
// 去除 IP 地址段的掩码信息
realIP := ip[:len(ip)-3]
// 解析 IP 地址
testIP := net.ParseIP(realIP)
// 若 IP 地址解析出错,则返回错误
if testIP == nil {
return nil, ParseIPErr
}
// 取出 IP 地址段的前两段
IPrange := strings.Join(strings.Split(realIP, ".")[0:2], ".")
// 构造 IP 地址列表
var AllIP []string
for a := 0; a <= 255; a++ {
for b := 0; b <= 255; b++ {
AllIP = append(AllIP, IPrange+"."+strconv.Itoa(a)+"."+strconv.Itoa(b))
}
}
return AllIP, nil
}

func ParseIPE(ip string) ([]string, error) {
// 取出输入 IP 字符串的前缀
realIP := ip[:len(ip)-2]
// 将输入 IP 地址字符串转换为 IP 地址类型
testIP := net.ParseIP(realIP)
// 如果输入的 IP 地址无效,则返回错误
if testIP == nil {
return nil, ParseIPErr
}
// 获取 IP 地址的第一段数字作为网段前缀
IPrange := strings.Join(strings.Split(realIP, ".")[0:1], ".")
var AllIP []string
// 循环生成所有可能的 IP 地址
for a := 0; a <= 255; a++ {
for b := 0; b <= 255; b++ {
// 生成除了子网地址之外的所有 IP 地址
AllIP = append(AllIP, IPrange+"."+strconv.Itoa(a)+"."+strconv.Itoa(b)+"."+strconv.Itoa(1))
AllIP = append(AllIP, IPrange+"."+strconv.Itoa(a)+"."+strconv.Itoa(b)+"."+strconv.Itoa(254))
}
}
return AllIP, nil
}

  • 这个处理输入的不同格式的IP地址

Plugins文件夹

按照main.go文件中的代码,在扫描阶段,调用的是Scan()函数,所以对于Plugins文件夹内的文件,首先分析scanner.go,通过对这个文件夹中的scan()函数的解读,按照函数执行流程逐个分析其他调用到的函数。

func main() {
var Info common.HostInfo
common.Flag(&Info) //fmt.Println(Info.Host,Info.Ports)
common.Parse(&Info)
Plugins.Scan(&Info)
fmt.Println("scan end")
}
Scanner.go
func Scan(info *common.HostInfo)  {
// 将传入的HostInfo中的Host转化为IP地址列表
Hosts,_ := common.ParseIP(info.Host)
// 如果info.Isping为false,则执行ICMPRun函数进行ping测试
if info.Isping == false{
Hosts = ICMPRun(Hosts)
}
// 使用TCPportScan函数进行端口扫描,AlivePorts为存活的IP地址和端口的组合
_,AlivePorts := TCPportScan(Hosts,info.Ports,"icmp",3) //return AliveHosts,AlivePorts

// 初始化一个存储常用服务端口的列表
var severports []string
for _,port:=range common.PORTList{
severports = append(severports,strconv.Itoa(port))
}
// 初始化一个只包含Oracle服务端口的列表
severports1 := []string{"1521"}

// 初始化一个int类型的channel,用于协程之间的通信
var ch = make(chan int,info.Threads)
// 初始化一个同步等待组
var wg = sync.WaitGroup{}

// 初始化一个字符串类型的变量,用于存储扫描类型
var scantype string

// 遍历存活IP地址和端口的组合
for _,targetIP :=range AlivePorts{
// 将IP地址和端口分离出来
scan_ip,scan_port := strings.Split(targetIP,":")[0],strings.Split(targetIP,":")[1]
// 将当前扫描的IP地址赋值给HostInfo中的Host
info.Host = scan_ip

// 如果扫描类型为all,则扫描所有常用服务端口和非常用服务端口
if info.Scantype == "all"{
// 如果当前端口在常用服务端口列表中,则执行AddScan函数进行扫描
if IsContain(severports,scan_port){
AddScan(scan_port,info,ch,&wg)
}else {
// 如果当前端口不在常用服务端口列表中,且不是Oracle服务端口,则执行WebTitle函数进行http服务扫描
if !IsContain(severports1,scan_port){
info.Url = fmt.Sprintf("http://%s",targetIP)
wg.Add(1)
go WebTitle(info,ch,&wg)
ch <- 1
}
}
// 如果当前端口为445,则同时进行NetBIOS扫描和SMB版本扫描
if scan_port == "445"{
AddScan("1000001",info,ch,&wg)
AddScan("1000002",info,ch,&wg)
}
}else {
// 如果扫描类型为指定端口,则执行AddScan函数进行扫描
port,_:=common.PORTList[info.Scantype]
scantype = strconv.Itoa(port)
AddScan(scantype,info,ch,&wg)
}
}
// 等待所有协程执行完毕
wg.Wait()
}

  • 执行流程:

    • 首先,如果没有使用 -np 参数,则先使用 ping 进行一个存活探测:这里的小问题就是,存活检测的方式少了,对于禁ping的主机,就无法使用,这也导致有时候用fscan扫描会发现扫不到东西,或者是扫的东西太少了,而如果使用了 -np 可是全面扫描,但是又会浪费很多时间,特别是A段,B段这种。

    • 之后就是:TCPportScan(), TCP - Prot 端口扫描,返回的是存活的ip地址加端口的一个组合 "AlivePorts" ;这个函数传入的 model = "icmp" 好像最终没用使用这个参数,在1.0版本里面。

    • 接下来是定义了一个 "severports" 字符型切片,用来存储 common.PORTList这个map里面的每个key对应的value,一个切片里面存储了一堆端口号(不全是,ms17是一串数字),这个的作用主要是用来和之前扫描出来的端口进行一个匹配,如果开放端口里面有 severports  里面的端口,那么就开启一个对应漏洞扫描的协程。

    • 接下来是一个for循环,这里就是漏洞扫描部分的核心,首先是从之前的获取到的存活 ip + prot 组合变量 AlivePorts 里面分别取出 ip 和 prot , 然后将 ip 赋值给 info.Host ;接下来判断扫描类型,对于使用者来说就是 -m 参数,默认是 all , 如果制定了 -m 参数,那么就会进入下方的 else 的代码块;

    • 首先看 Scantyp == all 的情况:内部代码块中,主要是通过判断存活端口的情况来选择要进行的漏洞扫描插件,首先是将  severports 和 scan_port 进行一个匹配,第一个变量就是在common/config.go 中定义好的 PORTList 这个map的 value 组成的切片,存储了目前工具可以进行漏洞探测的所有服务的端口,scan_port 就是每个目标开放的端口号,通过 IsContain() 函数将开放的端口依次和 severports  中的端口进行比较,如果相等,就返回true,如果最终都没有相等的,就返回false。

func IsContain(items []string, item string) bool {
for _, eachItem := range items {
if eachItem == item {
return true
}
}
return false
}
    • 如果 IsContain() 返回为true,就会调用 AddScan() 这个函数,这个函数的主要作用就是加锁,然后调用scan_func(),这个是核心的一个函数,主要是通过反射调用Plugins文件夹里面的那些漏洞扫描函数,通过反射的方式,实现非常方便的扩展,以后如果想手动扩展扫描的漏洞,就可以自己在Plugins中写好代码,然后在base.go和config.go中添加相应的内容。

func scan_func(m map[string]interface{}, name string, infos ...interface{}) (result []reflect.Value, err error) {
// 根据传入的函数名获取函数对象
f := reflect.ValueOf(m[name])
// 判断传入的参数个数是否和函数定义的参数个数相同
if len(infos) != f.Type().NumIn() {
// 如果不相同则返回错误
err = errors.New("The number of infos is not adapted.")
if err != nil {
fmt.Println(err.Error())
}
}
// 定义一个切片,存放传入的参数
in := make([]reflect.Value, len(infos))
for k, info := range infos {
// 将参数转化为反射值,并存入in切片中
in[k] = reflect.ValueOf(info)
}
// 调用函数,传入参数,并返回函数的返回值
result = f.Call(in)
return result, nil
}
    • 接下来是对于这个函数的解析

    • 首先通过反射获取对应的服务漏洞的扫描模块的函数,这个详细方式是:先说 m map[string]interface{}、name string、 infos 这三个参数,第一个参数是 base.go 文件里面定义的:PluginList 这个map,存储是 "端口":函数名,有点像注册的感觉,将端口和漏扫函数联系起来;name参数就是端口号,这个端口号的来源是 "scan_port" 这个变量,也就是主机的开放的端口,同时能够和config.go里面的PORTList这个map里面可以匹配上的端口。infos 这个是反射调用的时候需要传入的参数,就是给漏扫函数传的参数。如果我扫描的是21端口ftp服务,那么m map[string]interface{}传入的就是 "PluginList" 、name 这个参数就是"21",是端口号,字符型,infos 是 *common.HostInfo、ch、wg。

package Plugins
var PluginList = map[string]interface{}{
"21": FtpScan,
"22": SshScan,
"135": Findnet,
"445": SmbScan,
"1433":MssqlScan,
"3306": MysqlScan,
"5432": PostgresScan,
"6379": RedisScan,
"9200":elasticsearchScan,
"11211":MemcachedScan,
"27017":MongodbScan,
"1000001": MS17010,
"1000002": SmbGhost,
//"WebTitle":WebTitle,
}
    • f := reflect.ValueOf(m[name]) ;再来看这段代码,假设 name == 21 那么 m[21] 的value就是 "FtpScan" 而这个就和 Plugins/ftp.go 文件当中的  FtpScan(info common.HostInfo,ch chan int,wg sync.WaitGroup) 函数名字一样,这里插一句:PluginList 当中的 value就正好和 Plugins 当中对应的漏洞扫描的函数名一样,通过这样的方式就可以使用反射调用指定的漏洞探测代码。例如此处的假设就是通过21端口号获得value == FtpScan ,然后通过reflect.ValueOf(FtpScan)获得 reflect.Value 类型的值 并复制给 f。这个 f 可以被用于反射调用对应的函数。

    • 通过反射调用函数,除了需要获取到函数的reflect.Value,还需要再调用函数的时候传入参数,所以接下来的一串代码都是处理参数的,首先是通过一个if 判断infos 的个数和  f.Type().NumIn() 返回 f 这个函数的参数个数做一个对比,如果数量不一样,直接就报错,如果一样,才进行函数的调用。

    • 接下来是通过反射的方式准备好调用函数时传入的参数,最终通过 f.call(in) 调用漏洞探测函数,最后返回结果;首先是参数准备这里,由于反射调用函数的 Call() 方法传入的参数是[]reflect.Value 类型,所以对于infos、ch、wg 这三个参数还需要通过反射reflect.ValueOf(info)使其变成  reflect.Value 类型,这样才能传递到 Call() 函数中,通过创建一个切片 in 将三个参数都的 reflect.Value 类型都放在其中,然后 调用 f.Call(in) 来调用漏洞扫描函数,由于 name 的不同,f 会是不同的函数,可能是 FtpScan,也可能是SshScan ,一样的代码有不一样的结果。

    • 再说说如何扩展:假如我要加一个 8080 的端口的漏洞探测扫描插件,函数名就叫做 TomCat() 那么我首先去 common/config.go 里面的 PORTList 这个map里面加一行:"tomcat":8080 ,然后在 Plugins/base.go里面的 PluginList 这个map 里面加一行 : "8080":TomCat ,这里要注意,这个 TomCat 要和 TomCat() 函数名一样,这样才能通过反射调用起来,然后再在Plugins这个文件夹里面创建一个.go 文件,然后写漏洞探测代码;具体代码首先需要接收四个参数,首先写漏洞探测代码,最终结果如果存在漏洞,就需要true,如果不存在就返回false,并且还需要 加上 wg.Done() ; <-ch 通知任务完成,以及将管道中的一个参数释放掉;具体细节还需要再学习一下。

    • 对于没有在 PORTList 里面的端口,默认是获取WebTitle ,也就是网站的title,这里有一点不明白的是为什么有:if !IsContain(severports1,scan_port) 这样一个判断,单独和1521这个端口进行一个比较;

    • ms17-010和cve20200796在 PORTList  里面对应的不是端口,而是 1000001 和 1000002,所以单独用一个 if scan_port == "445" 来处理这个内容,同样是调用:AddScan() 函数反射调用漏洞探测代码。

    • 当我们指定 -m 不是all的时候,就会来到 Scan() 函数的最后一个else 代码块,也就是获取指定的端口,然后调用AddScan()

    • Plugins中的其他大部分文件都是某个具体漏洞的探测代码,这个涉及到漏洞原理方面的知识,这里就不分析了。

端口扫描源码阅读 Plugins/portscan.go

func ParsePort(ports string) []int {
var scanPorts []int // 用于存储解析后的端口的切片

slices := strings.Split(ports, ",") // 使用逗号将端口字符串分割成单独的端口段

for _, port := range slices {
port = strings.Trim(port, " ") // 去除端口段中的前导和尾随空格

upper := port // 使用当前端口初始化upper变量

if strings.Contains(port, "-") {
ranges := strings.Split(port, "-") // 使用破折号将端口段分割成多个端口范围

if len(ranges) < 2 { // 跳过无效的范围,即包含少于两个值的范围
continue
}

sort.Strings(ranges) // 对范围进行排序
port = ranges[0]
upper = ranges[1]
}

start, _ := strconv.Atoi(port) // 将起始端口和结束端口转换为整数
end, _ := strconv.Atoi(upper)

for i := start; i <= end; i++ {
scanPorts = append(scanPorts, i) // 将解析后的端口添加到scanPorts切片中
}
}

return scanPorts
}

    • 此函数作用为解析端口号字符串,转换成整数切片,每一个元素就是一个端口号

    • 先用 ,进行分割,在此基础上去除每个元素的空格,然后再看是否有 - 符号,如果有,在对这个 - 符号进行处理,找到最小值和最大值,然后用for循环将 - 指定返回的端口号都存到切片中

func ProbeHosts(host string, ports <-chan int, respondingHosts chan<- string, done chan<- bool, model string, adjustedTimeout int) {
Timeout := time.Duration(adjustedTimeout) * time.Second // 根据给定的调整后的超时时间创建超时时长
for port := range ports {
start := time.Now() // 记录开始时间
con, err := net.DialTimeout("tcp4", fmt.Sprintf("%s:%d", host, port), time.Duration(adjustedTimeout)*time.Second) // 尝试与指定主机和端口建立TCP连接,设置连接超时时间
duration := time.Now().Sub(start) // 计算连接建立的持续时间
if err == nil {
defer con.Close() // 延迟关闭连接
address := host + ":" + strconv.Itoa(port) // 构建主机地址字符串
result := fmt.Sprintf("%s open", address) // 构建结果字符串
common.LogSuccess(result) // 记录成功的连接结果
respondingHosts <- address // 将响应的主机地址发送到通道中
}
if duration < Timeout {
difference := Timeout - duration // 计算剩余超时时间
Timeout = Timeout - (difference / 2) // 调整超时时间为剩余超时时间的一半
}
}
done <- true // 向完成通道发送完成信号
}
    • 这个函数是对端口开发探测的一个封装,对超时时间进行了一个设置,连接成功的 ip:prot 会放入到管道中进行下一步使用,最后一个if,计算超时时间,这一步可以优化 timeout 时间,扫描效率更高

    • 1.0版本没有加代理,之后会直接看最新版的代码

func ScanAllports(address string, probePorts []int, threads int, timeout time.Duration, model string, adjustedTimeout int) ([]string, error) {
ports := make(chan int, 20) // 创建一个缓冲通道,用于存储要扫描的端口号
results := make(chan string, 10) // 创建一个缓冲通道,用于存储扫描结果
done := make(chan bool, threads) // 创建一个缓冲通道,用于标记完成的线程数量

for worker := 0; worker < threads; worker++ {
go ProbeHosts(address, ports, results, done, model, adjustedTimeout) // 启动指定数量的工作线程来扫描主机
}

for _, port := range probePorts {
ports <- port // 将要扫描的端口号发送到通道中
}
close(ports) // 关闭端口通道

var responses = []string{} // 存储扫描结果的切片
for {
select {
case found := <-results: // 从结果通道中接收扫描结果
responses = append(responses, found) // 将扫描结果添加到响应切片中
case <-done: // 从完成通道中接收信号
threads-- // 完成的线程数量减1
if threads == 0 {
return responses, nil // 如果所有线程都完成了,则返回扫描结果
}
case <-time.After(timeout): // 在指定的超时时间后,返回当前的扫描结果
return responses, nil
}
}
}

    • 创建三个通道:ports 用于存储要扫描的端口号,results 用于存储扫描结果,done 用于标记完成的工作线程数量。

    • 使用 for 循环创建指定数量的工作线程(根据 threads 变量),并在每个工作线程中调用 ProbeHosts 函数,传入地址、端口通道、结果通道、完成通道、模型和调整后的超时时间。

    • 使用 for range 循环遍历要扫描的端口数组 probePorts,并将每个端口号发送到 ports 通道中。

    • 关闭 ports 通道,表示不再发送更多的端口号。

    • 创建一个空的字符串切片 responses,用于存储扫描结果。

    • 使用 select 语句来监听三个通道的事件:

    • 如果从 results 通道接收到结果,将结果追加到 responses 切片中。

    • 如果从 done 通道接收到信号,表示有一个工作线程完成了扫描,将 threads 变量减1。如果所有工作线程都完成了扫描(threads 变为0),则返回 responses 切片作为结果。

    • 如果在超时时间(由 timeout 参数指定)之后仍未接收到结果,则返回当前的 responses 切片作为部分结果。

    • 函数返回 responses 切片和 nil,表示扫描完成且无错误。

func TCPportScan(hostslist []string, ports string, model string, timeout int) ([]string, []string) {
var AliveAddress []string
var aliveHosts []string

probePorts := ParsePort(ports) // 解析要扫描的端口号

lm := 20
// 根据主机列表的大小选择并发限制值
if len(hostslist) > 5 && len(hostslist) <= 50 {
lm = 40
} else if len(hostslist) > 50 && len(hostslist) <= 100 {
lm = 50
} else if len(hostslist) > 100 && len(hostslist) <= 150 {
lm = 60
} else if len(hostslist) > 150 && len(hostslist) <= 200 {
lm = 70
} else if len(hostslist) > 200 {
lm = 75
}

thread := 5
// 根据要扫描的端口号数量选择线程数
if len(probePorts) > 500 && len(probePorts) <= 4000 {
thread = len(probePorts) / 100
} else if len(probePorts) > 4000 && len(probePorts) <= 6000 {
thread = len(probePorts) / 200
} else if len(probePorts) > 6000 && len(probePorts) <= 10000 {
thread = len(probePorts) / 350
} else if len(probePorts) > 10000 && len(probePorts) < 50000 {
thread = len(probePorts) / 400
} else if len(probePorts) >= 50000 && len(probePorts) <= 65535 {
thread = len(probePorts) / 500
}

var wg sync.WaitGroup
mutex := &sync.Mutex{}
limiter := make(chan struct{}, lm)
aliveHost := make(chan string, lm/2)

go func() {
for s := range aliveHost {
fmt.Println(s) // 打印活动的主机
}
}()

for _, host := range hostslist {
wg.Add(1)
limiter <- struct{}{}
go func(host string) {
defer wg.Done()
// 扫描主机的所有端口
if aliveAdd, err := ScanAllports(host, probePorts, thread, 5*time.Second, model, timeout); err == nil && len(aliveAdd) > 0 {
mutex.Lock()
aliveHosts = append(aliveHosts, host) // 将活动的主机添加到列表中
for _, addr := range aliveAdd {
AliveAddress = append(AliveAddress, addr) // 将活动的地址添加到列表中
}
mutex.Unlock()
}
<-limiter
}(host)
}

wg.Wait()
close(aliveHost)

return aliveHosts, AliveAddress
}

    • 解析要扫描的端口号:通过调用ParsePort函数,将传入的端口号字符串解析为整数数组。

    • 根据主机列表的大小和要扫描的端口数量,确定并发限制值和线程数,以优化扫描速度和资源利用。

    • 创建并发限制器、结果存储通道和活动主机通道。

    • 启动并发的goroutine来扫描每个主机的端口。

    • 在每个goroutine中,调用ScanAllports函数来执行对单个主机的端口扫描。

    • 如果扫描结果中存在活动的地址(表示端口是开放的),则将该主机和地址添加到结果列表中。

    • 所有扫描操作完成后,关闭活动主机通道并返回结果列表。

fscan1.8.2端口扫描源码学习 Plugins/portscan.go

type Addr struct {
ip string
port int
}
    • 这个结构体用来存储目标ip+port

package main

import (
"fmt"
"sort"
"sync"
"time"
)

// Addr 表示主机和端口的组合
type Addr struct {
Host string // 主机地址
Port int // 端口号
}

// PortScan 对给定的主机列表和端口执行端口扫描
func PortScan(hostslist []string, ports string, timeout int64) []string {
var AliveAddress []string // 存储活动的主机地址

// 解析要探测的端口
probePorts := common.ParsePort(ports)

// 解析要排除的端口
noPorts := common.ParsePort(common.NoPorts)

// 从 probePorts 列表中删除排除的端口
if len(noPorts) > 0 {
temp := map[int]struct{}{}
for _, port := range probePorts {
temp[port] = struct{}{}
}

for _, port := range noPorts {
delete(temp, port)
}

var newDatas []int
for port := range temp {
newDatas = append(newDatas, port)
}
probePorts = newDatas
sort.Ints(probePorts)
}

// 设置并发扫描的线程数
workers := common.Threads

// 创建用于传递地址的通道
Addrs := make(chan Addr, len(hostslist)*len(probePorts))

// 创建用于接收结果的通道
results := make(chan string, len(hostslist)*len(probePorts))

var wg sync.WaitGroup

// 接收结果的协程
go func() {
for found := range results {
AliveAddress = append(AliveAddress, found)
wg.Done()
}
}()

// 多线程扫描
for i := 0; i < workers; i++ {
go func() {
for addr := range Addrs {
PortConnect(addr, results, timeout, &wg)
wg.Done()
}
}()
}

// 添加扫描目标
for _, port := range probePorts {
for _, host := range hostslist {
wg.Add(1)
Addrs <- Addr{host, port}
}
}

wg.Wait()
close(Addrs)
close(results)

return AliveAddress
}

传入参数:主机列表,端口字符串,超时时间,返回要给string切片

最新版本的并发控制和1.0版本有点不一样,感觉跟简洁了。首先是准备数据,将需要扫描的端口,存放 Addrs 结构体的管道,以及存放结果的管道,这两个管道的大小都是 len(hostslist)*len(probePorts) (:我有个疑问,这个数字是不是有点大了,假如我扫个C段全端口:254*65535=16645890,不知道我有没有算对,如果算对了,这个数据是不是有点大了,会不会比较消耗资源?)

然后启动一个接受结果的协程等待,接受结果这里,通过每一次循环就会 wg.Done() 通过这个来解决同步的问题

主线程中创建工人扫描端口,工人的数量由 common.Threads 并发数决定,默认因该是600,这次没有通读代码,不知道中间有没有什么设定之列的,没搜索到。最新版本这个并发数是否可以这样优化:假如我扫描一个端口,那600的并发数是不是有599都没用,那么是否可以这样,len(probePorts) < 600 ; workers := len(probePorts)  , 这里并不是抬杠哈,只是有疑问,毕竟没有亲手写过这个,肯定有些细节我没有考虑到。

创建工人函数的时候,同时也会有 wg.Done() 这里初看有点不懂,后面分析

然后是用 嵌套 for 添加目标,这里有个有意思的点,外层时 port,内层是 ip,比如扫:22 端口的时候,会把所有 ip 的 22 都扫了,再扫下一个端口,实际测试的情况如下图,这样的好处就是:对于多目标,不会因为并发量太高而造成 dos ,而且效率更高。

添加目标的时候,每添加一个,就会 wg.Add(1) , 添加方式是以 Addr(host,port) 写入管道的方式,然后这个管道会通过上方的多线程扫描创建的工人匿名函数取出来然后传入到 PortConnect 函数中,工人函数的作用就是从 Addr 管道里面取出 单个 addr,然后传入端口连接函数,同时传入的参数还有 结果管道,超时时间,&wg 控制并发

最后就是wg.Wait() 等待,以及关闭管道,还有返回string结果切片,其中存放的是打开的 ip:port

func PortConnect(addr Addr, respondingHosts chan<- string, adjustedTimeout int64, wg *sync.WaitGroup) {
host, port := addr.Host, addr.Port

// 使用调整后的超时时间创建 TCP 连接
conn, err := common.WrapperTcpWithTimeout("tcp4", fmt.Sprintf("%s:%v", host, port), time.Duration(adjustedTimeout)*time.Second)
defer func() {
if conn != nil {
conn.Close()
}
}()

// 如果没有错误,表示端口开放
if err == nil {
address := host + ":" + strconv.Itoa(port)
result := fmt.Sprintf("%s open", address)
common.LogSuccess(result)
wg.Add(1)
respondingHosts <- address
}
}

形参:addr结构体用来存储 ip,port ; 结果管道用来接受结果,并且只能写入,超时时间,用于设置tcp连接的超时时间,wg控制同步

该函数的作用为:接受一个Addr 结构体,然后尝试发起tcp连接,如果连接成功,没有报错,就将这个 host:port 以 string 类型的形式写到结果管道中,供另外一个结果读取协程获取结果并且添加到结果切片中,最后返回。

详细步骤:

最开始将 ip ,port 分别赋值给两个变量

然后发起连接,这里使用的函数是对net.Dial() 的一个封装,主要是为了添加代理的功能,后面细看

defer 函数结束,一定要关闭连接

如果tcp连接建立成功,err没有保存,就将结果按照格式写入到结果管道中,这里还使用了 common.LongSuccess对结果进行了处理。之后是 wg.add(1) 同步控制。

详细看看代理部分:

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) {
var conn net.Conn

// 如果没有设置 Socks5Proxy,直接使用 forward.Dial 进行连接
if Socks5Proxy == "" {
var err error
conn, err = forward.Dial(network, address)
if err != nil {
return nil, err
}
} else {
// 使用 Socks5 代理进行连接
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
}

func Socks5Dailer(forward *net.Dialer) (proxy.Dialer, error) {
u, err := url.Parse(Socks5Proxy)
if err != nil {
return nil, err
}
if strings.ToLower(u.Scheme) != "socks5" {
return nil, errors.New("Only support socks5")
}
address := u.Host
var auth proxy.Auth
var dailer proxy.Dialer

// 如果 Socks5 代理设置了用户名和密码
if u.User.String() != "" {
auth = proxy.Auth{}
auth.User = u.User.Username()
password, _ := u.User.Password()
auth.Password = password
dailer, err = proxy.SOCKS5("tcp", address, &auth, forward)
} else {
// 没有设置用户名和密码
dailer, err = proxy.SOCKS5("tcp", address, nil, forward)
}

if err != nil {
return nil, err
}
return dailer, nil
}

执行流程:common.WrapperTcpWithTimeout  -> WrapperTCP -> Socks5Dailer

首先是设置超时时间,然后调用 WrapperTCP  发起请求,如果制定了代理参数就调用 Socks5Dailer  设置代理

代理相关库 "golang.org/x/net/proxy" 需要下载。

"golang.org/x/net/proxy" 是 Go 语言的一个第三方包,提供了对代理服务器的支持。它包含了用于创建和使用各种代理协议(如 SOCKS5、HTTP)的接口和实现。

具体来说,"golang.org/x/net/proxy" 包提供了以下功能:

1. 创建代理连接器(Dialer):该包定义了 `Dialer` 接口,用于创建代理连接。您可以使用 `proxy.SOCKS5` 函数创建一个 SOCKS5 代理连接器,或使用 `proxy.HTTP` 函数创建一个 HTTP 代理连接器。

2. 通过代理连接进行网络通信:使用代理连接器创建的连接器可以用于通过代理服务器与目标服务器进行网络通信。您可以使用创建的代理连接器进行 TCP 连接,并通过该连接发送和接收数据。

使用 "golang.org/x/net/proxy" 包可以帮助您在 Go 语言中与代理服务器进行交互,实现通过代理进行网络通信的功能。这对于需要通过代理访问远程资源或保护网络安全的应用程序非常有用。

直接看 WrapperTCP 函数的 else 部分,通过调用 Socks5Dailler 函数,通过 proxy设置代理,并且返回一个  proxy.Dialer 对象,然后通过这个对象调用 Dial对ip:port 发起连接请求,最后返回一个 conn

socks5Dailer 函数

u,err := url.Parse(Socks5Proxy) 通过url.Parse 对Socks5Proxy 进行解析,方便后面使用,

`url.Parse` 是 Go 语言标准库中的一个函数,用于解析 URL 字符串并返回一个 `*url.URL` 类型的结构体。`url.URL` 结构体包含了解析后的 URL 的各个部分,例如协议方案、主机名、端口、路径等等。

使用 `url.Parse` 可以方便地解析和操作 URL 字符串。一旦解析完成,您就可以通过访问 `url.URL` 结构体的字段来获取或修改 URL 的各个组成部分。

以下是 `url.Parse` 的一些常见用途:

1. 解析 URL 字符串:将一个字符串解析为一个 `*url.URL` 类型的结构体,方便后续对 URL 进行操作和访问。

2. 提取 URL 的各个部分:通过访问 `url.URL` 结构体的字段,可以获取 URL 的协议方案、主机名、端口、路径、查询参数等等信息。

3. 构建 URL:可以使用 `url.URL` 结构体提供的方法和字段,根据需要修改 URL 的各个组成部分,然后通过 `url.String()` 方法将修改后的 URL 转换回字符串形式。

总之,`url.Parse` 是一个非常实用的函数,可用于处理和操作 URL 字符串。

后面的内容就是通过proxy 库设置代理,这里需要proxy库使用的相关知识,最后返回一个可以调用 Dial 方法的对象。

func NoPortScan(hostslist []string, ports string) (AliveAddress []string) {
probePorts := common.ParsePort(ports) // 解析要探测的端口
noPorts := common.ParsePort(common.NoPorts) // 解析要排除的端口

if len(noPorts) > 0 {
temp := map[int]struct{}{}

// 将探测的端口添加到临时集合中
for _, port := range probePorts {
temp[port] = struct{}{}
}

// 从临时集合中删除排除的端口
for _, port := range noPorts {
delete(temp, port)
}

// 将剩余的端口重新组成列表
var newDatas []int
for port := range temp {
newDatas = append(newDatas, port)
}
probePorts = newDatas
sort.Ints(probePorts)
}

// 遍历探测的端口和主机列表,生成要扫描的地址
for _, port := range probePorts {
for _, host := range hostslist {
address := host + ":" + strconv.Itoa(port)
AliveAddress = append(AliveAddress, address)
}
}

return
}

这个函数根据传入的主机列表和端口,生成需要扫描的地址列表。它首先解析要探测的端口和要排除的端口,然后根据排除的端口从探测的端口中筛选出要扫描的端口。接下来,通过遍历扫描的端口和主机列表,生成需要扫描的地址,并将其添加到 AliveAddress 切片中。最后,函数返回生成的地址列表。

并发同步

看代码之前有一个疑惑就是:如果wg.Done() 先执行,会造成程序提前退出,这里代码上下顺序是先 wg.Done() 不过没出现问题,就很好奇作者师傅是如何实现的;接下是详细分析

可以看到,总共有两个 wg.Done() 这两个调用的前面都有从管道中读取数据的操作,而在写入管道的时候,会执行 wg.Add(1) 具体代码在:第 64行和第86行,分别是向Addrs管道添加需要扫描目标的时候 和 向结果管道添加开放端口的时候,这样就实现了 :添加了一个需要扫描的ip:port 此时执行wg.Add信号量为 1 , 工人函数从管道中取出,并且调用 PortConnect(addr, results, timeout, &wg) 将IP:PORT 和 &wg 传入 , 在函数内部,如果这个端口关闭,就直接返回,然后工人函数调用 wg.Done() 此时信号量减一,为0,网络请求是i/o密集型操作,速度肯定没有主线程的那个 添加扫描目标的for循环快,所以说,当发起tcp连接时,wg的实际信号量估计已经很大了,这个值可能等于 len(host*port) ,然后假如所有的端口都关闭,然后 PortConnect 函数返回,并且执行后面的 wg.Done() 然后信号量最终为0,wg.Wait()阻塞结束,往下执行。(后半截有点多余哈,我难得删了,算是我的一些思考。这里重复一下,添加扫描目标的那个嵌套for循环是在主线程中执行,所以信号量不可能提前为 0 的情况,刚刚思考的时候把这个for放到协程中去了思考了,有点问题,师傅们看看就行。)

接下来说端口开放的情况:端口开放的时候,PortConnect 函数会调用 wg.Add(1) 这个wg是调用他的工人函数传进来的,所以地址应该是和上面的wg是同一个,端口开放,wg.Add(1) 信号量加一,而在哪里减掉呢?在 47 行代码中,这是一个从 results 管道中接受开放端口结果的匿名函数协程,PortConnect  函数将开放的端口写入 results 管道中,然后供这个匿名函数读取结果并添加到结果切片中,然后才 wg.Done() 这个wg.Done() 执行的条件是要从管道里面取出要给结果,而取出一个结果的前提是得有一个结果装入到管道中,而装入到管道中之前会执行 wg.Addr(1) 这里保证了先加后减。

总结一下:在61行代码那个嵌套for循环中添加扫描目标,是在主线程中执行,当所有目标添加完毕才会到达 wg.Wait(),此时信号量等于len(host*port) (这里先不考虑工人函数的执行);然后工人函数从 Addrs 管道中取值,(工人函数和结果读取函数的协程都是在添加扫描目标之前就运行起来了,只不过管道里面没有东西,造成了阻塞),取值之后调用:PortConnect  对一个目标发起请求,如果端口关闭,则返回并执行 wg.Done() 信号量减一,如果端口开放,则在 PortConnect   函数内部还会执行 wg.Addr(1) 并将结果写入到 resulets 中,然后结果取得的那个匿名函数协程就会解除阻塞状态,从 results 管道中读取结果,然后将结果添加到结果集切片中,然后执行 wg.Done() 将在 PortConnect   这个函数中wg.Add(1) 的一个信号量给减一,然后回到工人函数,工人函数调用完 PortConnect   后,也是执行 wg.Done() 将之前在嵌套for循环中添加的信号量减一,整个流程大概就是这样;假如我三个ip,扫3个端口,那么添加扫描目标的时候就会 wg.Add(1) 3x3=9次,此时就是9个信号量,然后假如三个端口都关闭,那么工人从管道中获取到ip:port后,扫描结果为关闭,不会在 PortConnect  执行 wg.Add(1) 直接返回,然后wg.Done() ,这个过程执行9次, 最终信号量为0,wg.Wait() 阻塞结束;如果所有端口开放,9个信号量固定,工人函数调用 PortConnect   后,PortConnect  函数还会wg.Add(1) 9次,此时总共的信号量就是9+9=18个(先不看wg.Done()) ;然后结果读取的那个协程发现有结果来了,立马停止阻塞,从管道中读取结果,然后 wg.Done() 由于 PortConnect  wg.Add(1) 进行了 9 次,然后也是向管道写入了 9 个结果,所以结果读取协程也是读取到了 9 个结果,然后 wg.Done() 进行了 9 次,然后加上工人函数的 wg.Done() 的9次,加起来正好 wg.Done() 了 18 次,这个值和 添加扫描目标的 wg.Add(18) 正好相同

关注公众号

公众号长期更新安全类文章,关注公众号,以便下次轻松查阅

觉得文章对你有帮助 请转发 点赞 收藏


文章来源: http://mp.weixin.qq.com/s?__biz=MzAwMDQwNTE5MA==&mid=2650246765&idx=1&sn=ea9625c0a9da8bc574e6056dbf18d0e6&chksm=82ea55c4b59ddcd266035db5605647f3f543f8de21d4318654cd3f97ada3243bab477e0afc3c#rd
如有侵权请联系:admin#unsafe.sh