入口函数
代码语言:javascript复制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("[*] 扫描结束,耗时: %sn", t)
}
- common.Flag:从命令行获取输入的参数,并根据参数准备程序运行的方式
- common.Parse:解析输入的内容,如从文件中读取主机,将主机范围转化为主机切片
- Plugins.Scan:开始进行扫描
Parse
代码语言:javascript复制func Parse(Info *HostInfo) {
ParseUser()
ParsePass(Info)
ParseInput(Info)
ParseScantype(Info)
}
Parse函数会解析用户的输入,得到运行时所需要的目标,参数等。
ParseUser
代码语言:javascript复制func ParseUser()
从全局变量或取用户输入的username和userFile,将输入的用户名和文件中的用户名合并,再去重,最后把得到的用户名切片返回到全局变量Userdict[name]
中。
Userdict保存了常见用户名信息,例如Userdict['mysql']
的内容为{"root", "mysql"}
。
ParsePass
代码语言:javascript复制func ParsePass(Info *HostInfo)
从全局变量获取用户输入的password和passFile,将输入的密码和文件中的密码合并,再去重,最后把得到的密码切片返回到全局变量Passwords
中。
Passwords是一个字符串切片,用来保存密码信息。
ParsePass还对URL
,URLFile
,PortFile
这三个变量进行解析,将信息返回到Urls和Info.Ports中。
这里是否有些不严谨?
ParseInput
代码语言:javascript复制func ParseInput(Info *HostInfo)
初始化Info.Ports
,添加web常见端口和host常见端口,将用户额外输入的PortAdd
,UserAdd
,PassAdd
分别添加到对应的全局变量中。如果用户指定了代理,会将代理也添加到对应的全局变量中。
ParseScantype
代码语言:javascript复制func ParseScantype(Info *HostInfo)
处理用户输入的Scantype
,根据扫描类型指定要扫描的端口,并赋值给Info.Ports
。
使用switch对Info.Ports
进行赋值,是否可以改为对Info.Ports
添加端口,实现同时指定多种扫描类型?
Scan
获取主机切片
代码语言:javascript复制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
中的主机排除,最后返回一个包含目标主机的切片。
存活主机扫描
代码语言:javascript复制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函数。
开放端口扫描
代码语言:javascript复制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函数
代码语言:javascript复制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
函数部分代码:
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函数
代码语言:javascript复制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函数
代码语言:javascript复制func IsContain(items []string, item string) bool {
for _, eachItem := range items {
if eachItem == item {
return true
}
}
return false
}
判断元素是否在切片中。
icmp.go
CheckLive函数
代码语言:javascript复制chanHosts := make(chan string, len(hostslist))
chanHosts
用来接收存活的主机地址。
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 aliven", ip)
} else {
fmt.Printf("(ping) Target %-15s is aliven", ip)
}
}
AliveHosts = append(AliveHosts, ip)
}
livewg.Done()
}
}()
接下来开启一个goroutine,从chanHosts
中读取存活的主机并输出。
ExistHosts[ip]
用来判断主机是否被输出过,如果没有,则添加一个ExistHosts[ip]
,再将ip进行输出。IsContain
判断ip是否在目标范围(即hostslist)内(一般不会出现目标外的ip)。
if Ping == true {
RunPing(hostslist, chanHosts)
}
根据全局变量Ping
判断是否用RunPing
函数探测。
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)
}
}
}
函数解析:
代码语言:javascript复制// 监听本地网络地址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检测。
- 首先建立一个
conn
监听本地icmp网络地址,如果建立成功,则运行RunIcmp1
函数 - 如果建立失败,则进行无监听icmp探测,会建立一个连接
conn
连接本地回环地址,如果建立成功,则运行RunIcmp2
函数 - 如果还建立失败,就使用
Ping
方法进行检测
使用defer关键字,在程序结束时自动关闭conn。
代码语言:javascript复制livewg.Wait()
close(chanHosts)
在RunPing
,RunIcmp1
,RunIcmp2
函数中,如果有存活主机,会对livewg
执行Add操作;在前面接收结果的goroutine中,成功接收结果会执行Done操作,当所有结果接收完时,等待组livewg
才会释放,然后关闭结果管道。
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段进行分类。再返回包含存活主机的切片。
代码重复有点多...