Fscan源码解读
## 入口函数 ```go func main() { start := time.Now() var Info common.HostInfo common.Flag(&Info) common.Parse(&Info) Plugins.Scan(Info) t := time.Now().Sub(start) fmt.Printf("[*] 扫描结束,耗时: %s\n", t) } ``` - common.Flag:从命令行获取输入的参数,并根据参数准备程序运行的方式 - common.Parse:解析输入的内容,如从文件中读取主机,将主机范围转化为主机切片 - Plugins.Scan:开始进行扫描 ## Parse ```go func Parse(Info *HostInfo) { ParseUser() ParsePass(Info) ParseInput(Info) ParseScantype(Info) } ``` Parse函数会解析用户的输入,得到运行时所需要的目标,参数等。 ### ParseUser ```go func ParseUser() ``` 从全局变量或取用户输入的username和userFile,将输入的用户名和文件中的用户名合并,再去重,最后把得到的用户名切片返回到全局变量`Userdict[name]`中。 > Userdict保存了常见用户名信息,例如`Userdict['mysql']`的内容为`{"root", "mysql"}`。 ### ParsePass ```go func ParsePass(Info *HostInfo) ``` 从全局变量获取用户输入的password和passFile,将输入的密码和文件中的密码合并,再去重,最后把得到的密码切片返回到全局变量`Passwords`中。 > Passwords是一个字符串切片,用来保存密码信息。 ParsePass还对`URL`,`URLFile`,`PortFile`这三个变量进行解析,将信息返回到Urls和Info.Ports中。 ### ParseInput ```go func ParseInput(Info *HostInfo) ``` 初始化`Info.Ports`,添加web常见端口和host常见端口,将用户额外输入的`PortAdd`,`UserAdd`,`PassAdd`分别添加到对应的全局变量中。如果用户指定了代理,会将代理也添加到对应的全局变量中。 ### ParseScantype ```go func ParseScantype(Info *HostInfo) ``` 处理用户输入的`Scantype`,根据扫描类型指定要扫描的端口,并赋值给`Info.Ports`。使用switch对`Info.Ports`进行赋值,是否可以改为对`Info.Ports`添加端口,实现同时指定多种扫描类型? ## Scan ### 获取主机切片 ```go Hosts, err := common.ParseIP(info.Host, common.HostFile, common.NoHosts) if err != nil { fmt.Println("len(hosts)==0", err) return } ``` `ParseIP`函数会将`info.Host`,`common.HostFile`中的字符串解析成单个的主机字符串,并放到一个切片中,同时将`common.NoHosts`中的主机排除,最后返回一个包含目标主机的切片。 ### 存活主机扫描 ```go if common.NoPing == false && len(Hosts) > 0 { Hosts = CheckLive(Hosts, common.Ping) fmt.Println("[*] Icmp alive hosts len is:", len(Hosts)) } if common.Scantype == "icmp" { common.LogWG.Wait() return } common.GC() ``` `CheckLive`函数(源码解读在icmp.go中)会返回包含存活主机的切片,`common.Ping`是一个布尔值,表示是否进行ping扫描。`LogWG.Wait()`会等待`CheckLive`函数中的goroutine执行完毕后,再执行后面的代码,因为里面有Add和Done操作。执行完毕后进行一次垃圾回收。 > `common.GC()`是封装后的GC函数。 ### 开放端口扫描 ```go var AlivePorts []string if common.Scantype == "webonly" || common.Scantype == "webpoc" { AlivePorts = NoPortScan(Hosts, info.Ports) } else if common.Scantype == "hostname" { info.Ports = "139" AlivePorts = NoPortScan(Hosts, info.Ports) } else if len(Hosts) > 0 { AlivePorts = PortScan(Hosts, info.Ports, common.Timeout) fmt.Println("[*] alive ports len is:", len(AlivePorts)) if common.Scantype == "portscan" { common.LogWG.Wait() return } } if len(common.HostPort) > 0 { AlivePorts = append(AlivePorts, common.HostPort...) AlivePorts = common.RemoveDuplicate(AlivePorts) common.HostPort = nil fmt.Println("[*] AlivePorts len is:", len(AlivePorts)) } common.GC() ``` 首先判断扫描类型,如果是web扫描或者主机名扫描,则调用`NoPortScan`扫描`info.Ports`中的端口,此时`info.Ports`中的端口都是指定端口。其它情况则调用`PortScan`函数(源码解读在portscan.go中)扫描存活的端口。`LogWG.Wait()`会等待`PortScan`函数中的goroutine执行完毕后,再执行后面的代码。最后加上`HostPort`中的`"host:ip"`,去重后得到完整的`AlivePorts`。将`HostPort`的值设为空,再进行垃圾回收,释放内存。 暂时不清楚为什么要加上`HostPort`中的`"host:ip"`,唯一确定的是ParseIP.go中对`HostPort`进行了操作,然而只是从文件中读取了`"host:port"`,并没有进行扫描操作。(是否默认用户输入的`"host:port"`一定存在?) ### 漏洞扫描 存活主机扫描与开放端口扫描后,会对常见的漏洞进行扫描,如ms17010,ve20200796等,通过`AddScan`函数针对这些漏洞的端口进行扫描,暂时不做过多的分析。 ### AddScan函数 ```go func AddScan(scantype string, info common.HostInfo, ch *chan struct{}, wg *sync.WaitGroup) { *ch <- struct{}{} wg.Add(1) go func() { Mutex.Lock() common.Num += 1 Mutex.Unlock() ScanFunc(&scantype, &info) Mutex.Lock() common.End += 1 Mutex.Unlock() wg.Done() <-*ch }() } ``` 参数说明: - scantype:扫描的类型 - info:主机信息 - ch:用于计数的管道 - wg:等待组 info参数实际上是通过for循环反复对`Scan`函数中的`info`变量赋值而产生的。每次循环都对`info.Host`,`info.Ports`重新赋值,再传入到`AddScan`中。 `Scan`函数部分代码: ```go for _, targetIP := range AlivePorts { info.Host, info.Ports = strings.Split(targetIP, ":")[0], strings.Split(targetIP, ":")[1] ... // info.Ports实际上是scantype AddScan(info.Ports, info, &ch, &wg) ``` `AddScan`函数只在`Scan`函数中的漏洞扫描过程中调用,并且`Scan`函数通过多次调用`AddScan`函数会开启多个goroutine执行`ScanFunc`函数,由于是并发执行程序,因此需要共用同一个等待组,当所有`AddScan`函数中的`ScanFunc`函数执行完毕后,`Scan`函数才会继续执行。`AddScan`函数中的`Num`和`End`是用来计数的,分别表示已开启的任务数量和已完成的任务数量。`Mutex`是互斥锁,防止多个goroutine同时对`Num`和`End`操作时产生冲突导致结果不正确。 > 对单个参数进行操作,是否可以改用原子锁提高性能? ### ScanFunc函数 ```go func ScanFunc(name *string, info *common.HostInfo) { f := reflect.ValueOf(PluginList[*name]) in := []reflect.Value{reflect.ValueOf(info)} f.Call(in) } ``` `AddScan`函数会调用`ScanFunc`函数,并传入name参数(`AddScan`中的`scantype`),通过反射从`PluginList`中取出`name`对应的函数名。同理将`info`转化成对应的参数,供`f`调用。 ### IsContain函数 ```go func IsContain(items []string, item string) bool { for _, eachItem := range items { if eachItem == item { return true } } return false } ``` 判断元素是否在切片中。 ## icmp.go ### CheckLive函数 ```go chanHosts := make(chan string, len(hostslist)) ``` `chanHosts`用来接收存活的主机地址。 ```go 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() } }() ``` 接下来开启一个goroutine,从`chanHosts`中读取存活的主机并输出,再添加到`AliveHosts`中。每次从`chanHosts`中读取到一个ip后,都对`livewg`进行Done操作。`ExistHosts[ip]`用来判断主机是否被输出过,如果没有,则添加一个`ExistHosts[ip]`,再将ip进行输出。`IsContain`判断ip是否在目标范围(即hostslist)内(一般不会出现目标外的ip)。 ```go if Ping == true { RunPing(hostslist, chanHosts) } ``` 根据全局变量`Ping`判断是否用`RunPing`函数探测。 ```go else { //优先尝试监听本地icmp,批量探测 conn, err := icmp.ListenPacket("ip4:icmp", "0.0.0.0") if err == nil { RunIcmp1(hostslist, conn, chanHosts) } else { common.LogError(err) //尝试无监听icmp探测 fmt.Println("trying RunIcmp2") conn, err := net.DialTimeout("ip4:icmp", "127.0.0.1", 3*time.Second) defer func() { if conn != nil { conn.Close() } }() if err == nil { RunIcmp2(hostslist, chanHosts) } else { common.LogError(err) //使用ping探测 fmt.Println("The current user permissions unable to send icmp packets") fmt.Println("start ping") RunPing(hostslist, chanHosts) } } } ``` 函数解析: ```go // 监听本地网络地址laddr,网络类型net必须是面向数据包的网络类型 func ListenPacket(net, laddr string) (PacketConn, error) // 在网络network上连接地址address,并返回一个Conn接口。 func Dial(network, address string) (Conn, error) // 同Dial函数,增加了超时 func DialTimeout(network, address string, timeout time.Duration) (Conn, error) ``` 如果`Ping`为false,则使用icmp检测。 1. 首先建立一个`conn`监听本地icmp网络地址,如果建立成功,则运行`RunIcmp1`函数 2. 如果建立失败,则进行无监听icmp探测,会建立一个连接`conn`连接本地回环地址,如果建立成功,则运行`RunIcmp2`函数 3. 如果还建立失败,就使用`Ping`方法进行检测 使用defer关键字,在程序结束时自动关闭`conn`。 ```go livewg.Wait() close(chanHosts) ``` 在`RunPing`,`RunIcmp1`,`RunIcmp2`函数中,如果有存活主机,会对`livewg`执行Add操作;在前面接收结果的goroutine中,成功接收结果会执行Done操作,当所有结果接收完时,等待组`livewg`才会释放,然后关闭结果管道。 ```go if len(hostslist) > 1000 { arrTop, arrLen := ArrayCountValueTop(AliveHosts, common.LiveTop, true) for i := 0; i < len(arrTop); i++ { output := fmt.Sprintf("[*] LiveTop %-16s 段存活数量为: %d", arrTop[i]+".0.0/16", arrLen[i]) common.LogSuccess(output) } } if len(hostslist) > 256 { arrTop, arrLen := ArrayCountValueTop(AliveHosts, common.LiveTop, false) for i := 0; i < len(arrTop); i++ { output := fmt.Sprintf("[*] LiveTop %-16s 段存活数量为: %d", arrTop[i]+".0/24", arrLen[i]) common.LogSuccess(output) } } return AliveHosts ``` 最后根据主机数量确定输出类型,即根据B段或C段进行分类。再返回包含存活主机的切片。 ### RunIcmp1函数 ```go endflag := false ``` 结束标志,设置为true时,函数return。 ```go go func() { for { if endflag == true { return } msg := make([]byte, 100) _, sourceIP, _ := conn.ReadFrom(msg) if sourceIP != nil { livewg.Add(1) chanHosts <- sourceIP.String() } } }() ``` 1. 开启一个goroutine循环监听,首先判断`endflag`是否为true,若是true则函数return。 2. 接下来从`conn`中读取数据,`ReadFrom`会将数据读取到`msg`中,同时返回数据长度和目标地址。 3. 如果这个地址存在,则对`livewg`执行Add操作,将地址发送到`chanHosts`中。 当地址在`CheckLive`函数中被接收后会执行Done操作,防止`CheckLive`函数在接收完所有结果前对结果进行处理。 ```go for _, host := range hostslist { dst, _ := net.ResolveIPAddr("ip", host) IcmpByte := makemsg(host) conn.WriteTo(IcmpByte, dst) } ``` 函数解析: ```go // 将addr解析成ip,返回一个*IPAddr数据 func ResolveIPAddr(net, addr string) (*IPAddr, error) ``` 1. 从`hostslist`中读取主机,通过`ResolveIPAddr`函数将host转化成`*IPAddr`型数据。 2. 再通过自定义函数`makemsg`构造请求包。 3. 向`conn`中写入请求包,并发送到目标主机上。 如果目标主机有响应,则可以通过之前开启的goroutine程序接收到返回包,进而确定主机存活。 ```go start := time.Now() for { if len(AliveHosts) == len(hostslist) { break } since := time.Now().Sub(start) var wait time.Duration switch { case len(hostslist) <= 256: wait = time.Second * 3 default: wait = time.Second * 6 } if since > wait { break } } endflag = true conn.Close() ``` 这里会根据目标主机的数量改变监听时间,也就是等待上面的goroutine接收结果,当等待时间超过`wait`后,将`endflag`设为true,从而结束goroutine,最后再关闭`conn`连接。 第一个if判断,`AliveHosts`的长度总是小于等于`hostslist`的长度,这个条件几乎不会达成,不过在特殊情况下最多节省6秒的时间,即所有主机都存活,就不需要等了。 对`wait`的赋值可以放到for循环外,进而提高性能,因为`hostslist`的长度是不变的。 ### RunIcmp2函数 ```go num := 1000 if len(hostslist) < num { num = len(hostslist) } ``` 根据`hostslist`的长度确定下面管道的最大长度。 ```go var wg sync.WaitGroup limiter := make(chan struct{}, num) ``` 定义一个等待组和一个管道。 ```go for _, host := range hostslist { wg.Add(1) limiter <- struct{}{} go func(host string) { if icmpalive(host) { livewg.Add(1) chanHosts <- host } <-limiter wg.Done() }(host) } ``` 1. 从`hostslist`中读取主机,等待组`wg`执行Add操作,向`limiter`发送一个空结构体。 2. 开启一个goroutine,用`icmpalive`函数判断主机是否存活。 3. 如果存活,则对`livewg`执行Add操作,将地址发送到`chanHosts`中。 4. 从`limiter`接收一个空结构体,等待组`wg`执行Done操作。 ```go wg.Wait() close(limiter) ``` 当等待组`wg`计数器为0时,关闭`limiter`,函数结束。 ### icmpalive函数 ```go startTime := time.Now() conn, err := net.DialTimeout("ip4:icmp", host, 6*time.Second) ``` 定义开始时间,还有与host的icmp连接。 ```go defer func() { if conn != nil { conn.Close() } }() ``` 函数结束时自动关闭连接。 ```go if err != nil { return false } if err := conn.SetDeadline(startTime.Add(6 * time.Second)); err != nil { return false } ``` 连接失败,超时都会返回false。 ```go msg := makemsg(host) if _, err := conn.Write(msg); err != nil { return false } ``` 通过自定义函数`makemsg`构造请求包,并尝试向`conn`发送请求包,如果失败则返回false。 ```go receive := make([]byte, 60) if _, err := conn.Read(receive); err != nil { return false } ``` 从`conn`中读取响应,如果失败则返回false。 ```go return true ``` 最后返回true,说明程序执行成功,即成功连接目标主机,成功发送信息,成功接收信息。 ### RunPing函数 ```go var bsenv = "" if OS != "windows" { bsenv = "/bin/bash" } ``` 根据操作系统确定bash的路径。 ```go var wg sync.WaitGroup limiter := make(chan struct{}, 50) ``` 定义一个等待组和一个管道。 ```go for _, host := range hostslist { wg.Add(1) limiter <- struct{}{} go func(host string) { if ExecCommandPing(host, bsenv) { livewg.Add(1) chanHosts <- host } <-limiter wg.Done() }(host) } wg.Wait() ``` 1. 从`hostslist`中读取主机,等待组`wg`执行Add操作,向`limiter`发送一个空结构体。 2. 开启一个goroutine,用`ExecCommandPing`函数判断主机是否存活。 3. 如果存活,则对`livewg`执行Add操作,将地址发送到`chanHosts`中。 4. 从`limiter`接收一个空结构体,等待组`wg`执行Done操作。 5. 当等待组`wg`计数器为0时,函数结束。 这里有个小问题,没有关闭limiter管道。 ### ExecCommandPing函数 ```go var command *exec.Cmd if OS == "windows" { command = exec.Command("cmd", "/c", "ping -n 1 -w 1 "+ip+" && echo true || echo false") } else if OS == "linux" { command = exec.Command(bsenv, "-c", "ping -c 1 -w 1 "+ip+" >/dev/null && echo true || echo false") } else if OS == "darwin" { command = exec.Command(bsenv, "-c", "ping -c 1 -W 1 "+ip+" >/dev/null && echo true || echo false") } ``` 根据操作系统获取对应的命令行,并执行Ping命令。 ```go outinfo := bytes.Buffer{} command.Stdout = &outinfo ``` 规定将命令行的输出转到`outinfo`中。 ```go err := command.Start() if err != nil { return false } ``` 尝试开始执行命令,失败则返回false。 ```go if err = command.Wait(); err != nil { return false } else { if strings.Contains(outinfo.String(), "true") { return true } else { return false } } ``` 等待命令执行,失败则返回false。成功则判断`outinfo`中是否包含true,如果包含,则返回true,否则返回false。 ### makemsg函数 ```go func makemsg(host string) []byte ``` 根据主机构造请求包,不进行过多解读。 ### ArrayCountValueTop函数 对存活主机进行总结的函数,用于根据B段或C段输出结果,不进行过多解读。 ## portscan.go ### PortScan函数 ```go var AliveAddress []string probePorts := common.ParsePort(ports) noPorts := common.ParsePort(common.NoPorts) ``` 定义保存结果的切片,获取输入的端口和不需要扫描的端口。 ```go if len(noPorts) > 0 { temp := map[int]struct{}{} // 将要扫描的端口写入temp for _, port := range probePorts { temp[port] = struct{}{} } // 从temp中删除不要扫描的端口 for _, port := range noPorts { delete(temp, port) } // 将结果重新保存到probePorts var newDatas []int for port, _ := range temp { newDatas = append(newDatas, port) } probePorts = newDatas sort.Ints(probePorts) } ``` 从`probePorts`去掉`noPorts`中端口。 ```go workers := common.Threads Addrs := make(chan Addr, len(hostslist)*len(probePorts)) results := make(chan string, len(hostslist)*len(probePorts)) var wg sync.WaitGroup ``` 定义扫描的线程数,地址管道,结果管道,等待组。 ```go //接收结果 go func() { for found := range results { AliveAddress = append(AliveAddress, found) wg.Done() } }() ``` 开启一个goroutine从结果管道中接收结果,并保存到`AliveAddress`。 ```go //多线程扫描 for i := 0; i < workers; i++ { go func() { for addr := range Addrs { PortConnect(addr, results, timeout, &wg) wg.Done() } }() } ``` 构造一个goroutine池,从地址管道中接收地址,再通过`PortConnect`函数对地址进行连接,该函数会将存活的地址发送到结果管道。 ```go //添加扫描目标 for _, port := range probePorts { for _, host := range hostslist { wg.Add(1) Addrs <- Addr{host, port} } } ``` 将主机和端口拼接成地址,并发送到地址管道中。 ```go wg.Wait() close(Addrs) close(results) return AliveAddress ``` 等待wg为0,关闭管道,返回结果。 ### PortConnect函数 ```go func PortConnect(addr Addr, respondingHosts chan<- string, adjustedTimeout int64, wg *sync.WaitGroup) ``` 参数说明: - addr:目标地址 - respondingHosts:返回结果的管道 - adjustedTimeout:超时时间 - wg:等待组 wg实际上就是`PortScan`中的wg,当有端口开放时,会执行Add操作,在`PortScan`中接收结果时,会执行Done操作。 ```go host, port := addr.ip, addr.port ``` 获取主机和端口。 ```go conn, err := common.WrapperTcpWithTimeout("tcp4", fmt.Sprintf("%s:%v", host, port), time.Duration(adjustedTimeout)*time.Second) defer func() { if conn != nil { conn.Close() } }() ``` 尝试与主机端口建立连接。 ```go if err == nil { address := host + ":" + strconv.Itoa(port) result := fmt.Sprintf("%s open", address) common.LogSuccess(result) wg.Add(1) respondingHosts <- address } ``` 如果没有错误,说明端口开放,再向结果管道发送结果。 ### NoPortScan函数 该函数只是单纯的返回host:port形式的主机,全局只有两次在`Scan`函数中被调用。
创建时间:2023-09-16
|
最后修改:2023-12-27
|
©允许规范转载
酷酷番茄
首页
文章
友链