入口函数

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

func Parse(Info *HostInfo) {
	ParseUser()
	ParsePass(Info)
	ParseInput(Info)
	ParseScantype(Info)
}

Parse函数会解析用户的输入,得到运行时所需要的目标,参数等。

ParseUser

func ParseUser()

从全局变量或取用户输入的username和userFile,将输入的用户名和文件中的用户名合并,再去重,最后把得到的用户名切片返回到全局变量Userdict[name]中。

Userdict保存了常见用户名信息,例如Userdict['mysql']的内容为{"root", "mysql"}

ParsePass

func ParsePass(Info *HostInfo)

从全局变量获取用户输入的password和passFile,将输入的密码和文件中的密码合并,再去重,最后把得到的密码切片返回到全局变量Passwords中。

Passwords是一个字符串切片,用来保存密码信息。

ParsePass还对URLURLFilePortFile这三个变量进行解析,将信息返回到Urls和Info.Ports中。

ParseInput

func ParseInput(Info *HostInfo)

初始化Info.Ports,添加web常见端口和host常见端口,将用户额外输入的PortAddUserAddPassAdd分别添加到对应的全局变量中。如果用户指定了代理,会将代理也添加到对应的全局变量中。

ParseScantype

func ParseScantype(Info *HostInfo)

处理用户输入的Scantype,根据扫描类型指定要扫描的端口,并赋值给Info.Ports。使用switch对Info.Ports进行赋值,是否可以改为对Info.Ports添加端口,实现同时指定多种扫描类型?

Scan

获取主机切片

Hosts, err := common.ParseIP(info.Host, common.HostFile, common.NoHosts)
if err != nil {
	fmt.Println("len(hosts)==0", err)
	return
}

ParseIP函数会将info.Hostcommon.HostFile中的字符串解析成单个的主机字符串,并放到一个切片中,同时将common.NoHosts中的主机排除,最后返回一个包含目标主机的切片。

存活主机扫描

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函数。

开放端口扫描

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函数

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.Hostinfo.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函数中的NumEnd是用来计数的,分别表示已开启的任务数量和已完成的任务数量。Mutex是互斥锁,防止多个goroutine同时对NumEnd操作时产生冲突导致结果不正确。

对单个参数进行操作,是否可以改用原子锁提高性能?

ScanFunc函数

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函数

func IsContain(items []string, item string) bool {
	for _, eachItem := range items {
		if eachItem == item {
			return true
		}
	}
	return false
}

判断元素是否在切片中。

icmp.go

CheckLive函数

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 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)。

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)
		}
	}
}

函数解析:

//  监听本地网络地址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

livewg.Wait()
close(chanHosts)

RunPingRunIcmp1RunIcmp2函数中,如果有存活主机,会对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段进行分类。再返回包含存活主机的切片。

RunIcmp1函数

endflag := false

结束标志,设置为true时,函数return。

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函数在接收完所有结果前对结果进行处理。

for _, host := range hostslist {
	dst, _ := net.ResolveIPAddr("ip", host)
	IcmpByte := makemsg(host)
	conn.WriteTo(IcmpByte, dst)
}

函数解析:

//  将addr解析成ip,返回一个*IPAddr数据
func ResolveIPAddr(net, addr string) (*IPAddr, error)
  1. hostslist中读取主机,通过ResolveIPAddr函数将host转化成*IPAddr型数据。
  2. 再通过自定义函数makemsg构造请求包。
  3. conn中写入请求包,并发送到目标主机上。

如果目标主机有响应,则可以通过之前开启的goroutine程序接收到返回包,进而确定主机存活。

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函数

num := 1000
if len(hostslist) < num {
	num = len(hostslist)
}

根据hostslist的长度确定下面管道的最大长度。

var wg sync.WaitGroup
limiter := make(chan struct{}, num)

定义一个等待组和一个管道。

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操作。
wg.Wait()
close(limiter)

当等待组wg计数器为0时,关闭limiter,函数结束。

icmpalive函数

startTime := time.Now()
conn, err := net.DialTimeout("ip4:icmp", host, 6*time.Second)

定义开始时间,还有与host的icmp连接。

defer func() {
	if conn != nil {
		conn.Close()
	}
}()

函数结束时自动关闭连接。

if err != nil {
	return false
}
if err := conn.SetDeadline(startTime.Add(6 * time.Second)); err != nil {
	return false
}

连接失败,超时都会返回false。

msg := makemsg(host)
if _, err := conn.Write(msg); err != nil {
	return false
}

通过自定义函数makemsg构造请求包,并尝试向conn发送请求包,如果失败则返回false。

receive := make([]byte, 60)
if _, err := conn.Read(receive); err != nil {
	return false
}

conn中读取响应,如果失败则返回false。

return true

最后返回true,说明程序执行成功,即成功连接目标主机,成功发送信息,成功接收信息。

RunPing函数

var bsenv = ""
if OS != "windows" {
	bsenv = "/bin/bash"
}

根据操作系统确定bash的路径。

var wg sync.WaitGroup
limiter := make(chan struct{}, 50)

定义一个等待组和一个管道。

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函数

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命令。

outinfo := bytes.Buffer{}
command.Stdout = &outinfo

规定将命令行的输出转到outinfo中。

err := command.Start()
if err != nil {
	return false
}

尝试开始执行命令,失败则返回false。

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函数

func makemsg(host string) []byte

根据主机构造请求包,不进行过多解读。

ArrayCountValueTop函数

对存活主机进行总结的函数,用于根据B段或C段输出结果,不进行过多解读。

portscan.go

PortScan函数

var AliveAddress []string
probePorts := common.ParsePort(ports)
noPorts := common.ParsePort(common.NoPorts)

定义保存结果的切片,获取输入的端口和不需要扫描的端口。

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中端口。

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()
	}
}()

开启一个goroutine从结果管道中接收结果,并保存到AliveAddress

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

构造一个goroutine池,从地址管道中接收地址,再通过PortConnect函数对地址进行连接,该函数会将存活的地址发送到结果管道。

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

将主机和端口拼接成地址,并发送到地址管道中。

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

等待wg为0,关闭管道,返回结果。

PortConnect函数

func PortConnect(addr Addr, respondingHosts chan<- string, adjustedTimeout int64, wg *sync.WaitGroup)

参数说明:

  • addr:目标地址
  • respondingHosts:返回结果的管道
  • adjustedTimeout:超时时间
  • wg:等待组

wg实际上就是PortScan中的wg,当有端口开放时,会执行Add操作,在PortScan中接收结果时,会执行Done操作。

host, port := addr.ip, addr.port

获取主机和端口。

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
}

如果没有错误,说明端口开放,再向结果管道发送结果。

NoPortScan函数

该函数只是单纯的返回host:port形式的主机,全局只有两次在Scan函数中被调用。