最近过了一遍go语言的语法,在尝试写工具时感觉还是欠缺很多东西,所以打算看看其他开源项目,首当其冲一定是Fscan(若有错误,恳请指正)
一 入口和http初始化
在main.go中启动入口
common.Flag为标准命令行解析
common.Parse进行对参数的解析
ParseUser():解析用户名。
- 如果Username变量不为空,将其按逗号分割成用户名列表。
- 如果Userfile变量不为空,读取文件中的用户名并添加到用户名列表中。
- 去除用户名列表中的重复项,并将结果赋值给Userdict字典的每个键。
ParsePass(Info *HostInfo):解析密码和哈希值。
- 如果Password变量不为空,将其按逗号分割成密码列表,并去重。
- 如果Passfile变量不为空,读取文件中的密码并添加到密码列表中。
- 如果Hashfile变量不为空,读取文件中的哈希值,并检查长度是否为32,如果不是则打印错误信息。
- 如果URL变量不为空,将其按逗号分割成URL列表,并去重。
- 如果UrlFile变量不为空,读取文件中的URL并添加到URL列表中。
- 如果PortFile变量不为空,读取文件中的端口并合并到Ports变量中。
ParseInput(Info *HostInfo):解析输入参数。
- 检查是否提供了主机信息,如果没有则打印错误信息并退出。
- 设置暴力破解线程数的默认值。
- 根据TmpSave变量的值设置是否保存结果。
- 设置默认端口和添加额外端口。
- 添加额外的用户名和密码,并去重。
- 处理Socks5代理和普通代理的设置,包括格式检查和错误处理。
- 检查哈希值的长度是否正确,并进行十六进制解码。
ParseScantype(Info *HostInfo):解析扫描类型。
- 根据Scantype变量的值,设置对应的端口。
- 如果Scantype不是"all",并且Ports是默认值,根据扫描类型选择相应的端口。
- 打印扫描类型和对应的端口信息。
然后就是 Plugins.Scan() 方法,首先对host进行解析Ip,然后走到lib.Inithttp
这段代码的主要作用是初始化两个HTTP客户端:Client和ClientNoRedirect。Client是一个普通的HTTP客户端,而ClientNoRedirect在发送请求时不会跟随重定向。这两个客户端都使用了自定义的http.Transport配置,包括拨号器、最大连接数、TLS配置等
func Inithttp() { //common.Proxy = "http://127.0.0.1:8080" if common.PocNum == 0 { common.PocNum = 20 } if common.WebTimeout == 0 { common.WebTimeout = 5 } err := InitHttpClient(common.PocNum, common.Proxy, time.Duration(common.WebTimeout)*time.Second) if err != nil { panic(err) } } func InitHttpClient(ThreadsNum int, DownProxy string, Timeout time.Duration) error { //定义了一个用于建立网络连接的函数 type DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) //net.Dialer结构体实例dialer,用于配置网络连接的参数 dialer := &net.Dialer{ Timeout: dialTimout, KeepAlive: keepAlive, } //http.Transport结构体实例tr,用于配置HTTP传输层 tr := &http.Transport{ DialContext: dialer.DialContext, MaxConnsPerHost: 5, //每个主机的最大连接数为5 MaxIdleConns: 0, //最大空闲连接数为0 MaxIdleConnsPerHost: ThreadsNum * 2, //每个主机的最大空闲连接数为ThreadsNum的两倍 IdleConnTimeout: keepAlive, //空闲连接的超时时间 TLSClientConfig: &tls.Config{MinVersion: tls.VersionTLS10, InsecureSkipVerify: true}, //设置TLS客户端配置,最低TLS版本为1.0,并且跳过证书验证 TLSHandshakeTimeout: 5 * time.Second, //TLS握手超时时间为5秒 DisableKeepAlives: false, //不禁用连接保持 } //检查是否存在Socks5代理 if common.Socks5Proxy != "" { dialSocksProxy, err := common.Socks5Dailer(dialer) //创建一个Socks5代理的拨号器 if err != nil { return err } if contextDialer, ok := dialSocksProxy.(proxy.ContextDialer); ok { tr.DialContext = contextDialer.DialContext } else { return errors.New("Failed type assertion to DialContext") } } else if DownProxy != "" { if DownProxy == "1" { //代理为 1 ,使用HTTP 代理 DownProxy = "http://127.0.0.1:8080" } else if DownProxy == "2" { //代理为 2 ,使用socks5 代理 DownProxy = "socks5://127.0.0.1:1080" } else if !strings.Contains(DownProxy, "://") { DownProxy = "http://127.0.0.1:" + DownProxy } if !strings.HasPrefix(DownProxy, "socks") && !strings.HasPrefix(DownProxy, "http") { return errors.New("no support this proxy") } u, err := url.Parse(DownProxy) if err != nil { return err } tr.Proxy = http.ProxyURL(u) //设置tr的代理为解析后的URL } //创建一个http.Client实例Client Client = &http.Client{ Transport: tr, Timeout: Timeout, } //创建一个http.Client实例ClientNoRedirect,用于处理不跟随重定向的请求 ClientNoRedirect = &http.Client{ Transport: tr, Timeout: Timeout, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, //设置ClientNoRedirect的重定向检查函数,使其不跟随重定向 } return nil }
二 主机和端口探测
继续向下看
如果Noping是false (默认为false)或者 Scantype 为 icmp时进入CheckLive中使用icmp或ping进行存活探测
CheckLive函数,首先创建一个channel,用来接受存活IP
func CheckLive(hostslist []string, Ping bool) []string { //chanHosts是一个缓冲通道,用于传递IP地址给后台goroutine chanHosts := make(chan string, len(hostslist)) go func() { for ip := range chanHosts { //从chanHosts通道中读取IP地址,并检查每个IP地址是否已经存在于ExistHosts映射中。如果不存在,并且该IP地址也在hostslist中,那么将其标记为存活 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) } } //存活的IP地址添加到AliveHosts列表中,并调用livewg.Done() AliveHosts = append(AliveHosts, ip) } livewg.Done() } }()
然后判断ICMP还是Ping的方式来检测存活
if Ping == true { //使用ping探测 RunPing(hostslist, chanHosts) } 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) } } }
Ping探测
直接通过系统命令进行检测
监听本地Icmp方式探测
通过makemsg方法构造了一个Icmp包,如果返回不为空,则存活
func RunIcmp1(hostslist []string, conn *icmp.PacketConn, chanHosts chan string) { endflag := false go func() { for { if endflag == true { return } msg := make([]byte, 100) _, sourceIP, _ := conn.ReadFrom(msg) if sourceIP != nil { livewg.Add(1) chanHosts <- sourceIP.String() } } }() for _, host := range hostslist { dst, _ := net.ResolveIPAddr("ip", host) IcmpByte := makemsg(host) conn.WriteTo(IcmpByte, dst) } //根据hosts数量修改icmp监听时间 start := time.Now() for { if len(AliveHosts) == len(hostslist) { break } since := time.Since(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() }
无监听icmp探测
func RunIcmp2(hostslist []string, chanHosts chan string) { num := 1000 if len(hostslist) < num { num = len(hostslist) } var wg sync.WaitGroup limiter := make(chan struct{}, num) for _, host := range hostslist { wg.Add(1) limiter <- struct{}{} go func(host string) { //icmpalive 函数检查主机是否存活 if icmpalive(host) { livewg.Add(1) chanHosts <- host } <-limiter wg.Done() }(host) } wg.Wait() close(limiter) } func icmpalive(host string) bool { startTime := time.Now() //尝试使用 ICMP 协议连接到目标主机,超时时间设置为 6 秒。 conn, err := net.DialTimeout("ip4:icmp", host, 6*time.Second) if err != nil { return false } defer conn.Close() //设置连接的死线(deadline),即连接必须在指定时间内完成,否则超时 if err := conn.SetDeadline(startTime.Add(6 * time.Second)); err != nil { return false } msg := makemsg(host) //连接发送 ICMP 请求数据包 if _, err := conn.Write(msg); err != nil { return false } receive := make([]byte, 60) if _, err := conn.Read(receive); err != nil { return false } return true }
端口探测
分为NoPortScan和PortScan
PortScan代码
创建两个通道:Addrs
用于存储要扫描的地址,results
用于接收扫描结果,通过在PortConnect调用common.WrapperTcpWithTimeout 进行连接测试是否开放
func PortScan(hostslist []string, ports string, timeout int64) []string { var AliveAddress []string probePorts := common.ParsePort(ports) if len(probePorts) == 0 { fmt.Printf("[-] parse port %s error, please check your port format\n", ports) return AliveAddress } 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) } workers := common.Threads Addrs := make(chan Addr, 100) results := make(chan string, 100) 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 } func PortConnect(addr Addr, respondingHosts chan<- string, adjustedTimeout int64, wg *sync.WaitGroup) { host, port := addr.ip, addr.port conn, err := common.WrapperTcpWithTimeout("tcp4", fmt.Sprintf("%s:%v", host, port), time.Duration(adjustedTimeout)*time.Second) if err == nil { defer conn.Close() address := host + ":" + strconv.Itoa(port) result := fmt.Sprintf("%s open", address) common.LogSuccess(result) wg.Add(1) respondingHosts <- address } }
common.WrapperTcpWithTimeout
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) { //get conn var conn net.Conn if Socks5Proxy == "" { var err error //指定使用tcp4和address连接 conn, err = forward.Dial(network, address) if err != nil { return nil, err } } else { //走代理的方式 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 }
三 漏洞扫描
然后开始漏扫逻辑
通过AddScan调度,然后ScanFunc进行反射执行PluginList中获取的方法
func ScanFunc(name *string, info *common.HostInfo) {
defer func() {
if err := recover(); err != nil {
fmt.Printf("[-] %v:%v scan error: %v\n", info.Host, info.Ports, err)
}
}()
//reflect.ValueOf 获取 PluginList 字典中对应 *name 键的值的反射值
f := reflect.ValueOf(PluginList[*name])
//创建一个反射值的切片 in
in := []reflect.Value{reflect.ValueOf(info)}
f.Call(in)
}
这里可以看下mysql的插件(只是一个简单的爆破,如果二开的话可以添加更多功能)
Web扫描
所有的扫描中,主要部分其实是webtitle的扫描,看一下这webtitle方法,调用了GOWebTitle和InfoCheck
方法
func WebTitle(info *common.HostInfo) error { //webpoc类型 if common.Scantype == "webpoc" { WebScan.WebScan(info) return nil } err, CheckData := GOWebTitle(info) info.Infostr = WebScan.InfoCheck(info.Url, &CheckData) if !common.NoPoc && err == nil { WebScan.WebScan(info) } else { errlog := fmt.Sprintf("[-] webtitle %v %v", info.Url, err) common.LogError(errlog) } return err }
GOWebTitle函数中做了一个url的拼接,如果端口不是80或者443,还会调用GetProtocol来获取协议类型,之后通过geturl 对url进行访问和解析,获取到resp的body和header信息存储到CheckData结构中,并且对重定向和访问400错误做了响应处理
func GOWebTitle(info *common.HostInfo) (err error, CheckData []WebScan.CheckDatas) { if info.Url == "" { switch info.Ports { case "80": info.Url = fmt.Sprintf("http://%s", info.Host) case "443": info.Url = fmt.Sprintf("https://%s", info.Host) default: host := fmt.Sprintf("%s:%s", info.Host, info.Ports) protocol := GetProtocol(host, common.Timeout) info.Url = fmt.Sprintf("%s://%s:%s", protocol, info.Host, info.Ports) } } else { if !strings.Contains(info.Url, "://") { host := strings.Split(info.Url, "/")[0] protocol := GetProtocol(host, common.Timeout) info.Url = fmt.Sprintf("%s://%s", protocol, info.Url) } }
err, result, CheckData := geturl(info, 1, CheckData)
if err != nil && !strings.Contains(err.Error(), "EOF") {
return
}
GetProtocol函数(发起tls连接,如果连接成功或者含有特定报错信息则判断为https,感觉自己写工具会用到)
func GetProtocol(host string, Timeout int64) (protocol string) {
protocol = "http"
//如果端口是80或443,跳过Protocol判断
if strings.HasSuffix(host, ":80") || !strings.Contains(host, ":") {
return
} else if strings.HasSuffix(host, ":443") {
protocol = "https"
return
}
//func WrapperTcpWithTimeout(network, address string, timeout time.Duration) (net.Conn, error) {
// //创建一个定制化的网络连接,设置超时时间
// d := &net.Dialer{Timeout: timeout}
// return WrapperTCP(network, address, d)
//}
socksconn, err := common.WrapperTcpWithTimeout("tcp", host, time.Duration(Timeout)*time.Second)
if err != nil {
return
}
conn := tls.Client(socksconn, &tls.Config{MinVersion: tls.VersionTLS10, InsecureSkipVerify: true})
defer func() {
if conn != nil {
defer func() {
if err := recover(); err != nil {
common.LogError(err)
}
}()
conn.Close()
}
}()
conn.SetDeadline(time.Now().Add(time.Duration(Timeout) * time.Second))
err = conn.Handshake()
if err == nil || strings.Contains(err.Error(), "handshake failure") {
protocol = "https"
}
return protocol
}
回到webtitle 看看WebScan.InfoCheck函数
通过正则去匹配Rule中的各种规则,放到infoname数组中
func InfoCheck(Url string, CheckData *[]CheckDatas) []string { var matched bool var infoname []string for _, data := range *CheckData { for _, rule := range info.RuleDatas { if rule.Type == "code" { //通过正则匹配 matched, _ = regexp.MatchString(rule.Rule, string(data.Body)) } else { matched, _ = regexp.MatchString(rule.Rule, data.Headers) } if matched == true { infoname = append(infoname, rule.Name) } } //flag, name := CalcMd5(data.Body) //if flag == true { // infoname = append(infoname, name) //} } infoname = removeDuplicateElement(infoname) if len(infoname) > 0 { result := fmt.Sprintf("[+] InfoScan %-25v %s ", Url, infoname) common.LogSuccess(result) return infoname } return []string{""} }
继续走到WebScan.WebScan
POC扫描
首先初始化poc,在CheckInfoPoc寻找相关poc,放到pocinfo结构体(Target和PocName ),然后进入Execute()
Poc的结构
// 确保一个操作只被执行一次,常用于初始化操作 var once sync.Once var AllPocs []*lib.Poc func WebScan(info *common.HostInfo) { //使用 once 变量调用 Do 方法,并传入 initpoc 函数 once.Do(initpoc) var pocinfo = common.Pocinfo buf := strings.Split(info.Url, "/") //协议、主机和端口重新组合成一个字符串 pocinfo.Target = strings.Join(buf[:3], "/") //pocinfo.PocName 不为空,表示已经指定了要执行的漏洞检查名称 if pocinfo.PocName != "" { Execute(pocinfo) } else { for _, infostr := range info.Infostr { pocinfo.PocName = lib.CheckInfoPoc(infostr) Execute(pocinfo) } } }
initpoc()中对pocs目录下的yaml进行load
func initpoc() { if common.PocPath == "" { entries, err := Pocs.ReadDir("pocs") if err != nil { fmt.Printf("[-] init poc error: %v", err) return } for _, one := range entries { path := one.Name() if strings.HasSuffix(path, ".yaml") || strings.HasSuffix(path, ".yml") { if poc, _ := lib.LoadPoc(path, Pocs); poc != nil { AllPocs = append(AllPocs, poc) } } }
LoadPoc通过yaml.Unmarshal从yaml中解析出Poc
func LoadPoc(fileName string, Pocs embed.FS) (*Poc, error) {
p := &Poc{}
yamlFile, err := Pocs.ReadFile("pocs/" + fileName)
if err != nil {
fmt.Printf("[-] load poc %s error1: %v\n", fileName, err)
return nil, err
}
err = yaml.Unmarshal(yamlFile, p)
if err != nil {
fmt.Printf("[-] load poc %s error2: %v\n", fileName, err)
return nil, err
}
return p, err
}
回到WebScan往下然后开始Execute()检测,这个代码就有点复杂了
func executePoc(oReq *http.Request, p *Poc) (bool, error, string) { c := NewEnvOption() c.UpdateCompileOptions(p.Set)
在executePoc中首先执行了NewEnvOption(),创建并配置了一个自定义的 CEL 环境选项
UpdateCompileOptions()中继续声明变量
创建env环境,然后解析http请求
env, err := NewEnv(&c)
if err != nil {
fmt.Printf("[-] %s environment creation error: %s\n", p.Name, err)
return false, err, ""
}
req, err := ParseRequest(oReq)
if err != nil {
fmt.Printf("[-] %s ParseRequest error: %s\n", p.Name, err)
return false, err, ""
}
当item.Value值不是newReverse() 默认走到evalset,evalset函数通过替换模板变量来动态构建攻击载荷,大概是评估测试结果(求明白的大佬可以教一下我,这个地方的evalset调用我不是特别清楚作用)
variableMap := make(map[string]interface{}) defer func() { variableMap = nil }() variableMap["request"] = req for _, item := range p.Set { k, expression := item.Key, item.Value if expression == "newReverse()" { if !common.DnsLog { return false, nil, "" } variableMap[k] = newReverse() continue } err, _ = evalset(env, variableMap, k, expression) if err != nil { fmt.Printf("[-] %s evalset error: %v\n", p.Name, err) } }
然后就会在evalset中调用Evaluate执行env中定义的函数
接下来是正式的poc尝试,clusterpoc函数取ymal中的规则,调用clustersend执行
下面是clusterpoc的代码解释
func clustersend(oReq *http.Request, variableMap map[string]interface{}, req *Request, env *cel.Env, rule Rules) (bool, error) { // 遍历variableMap中的每个键值对,将变量替换到rule的Headers、Path和Body中 for k1, v1 := range variableMap { _, isMap := v1.(map[string]string) if isMap { continue // 如果值是一个map,则跳过此次循环 } value := fmt.Sprintf("%v", v1) // 将值格式化为字符串 // 遍历rule的Headers,替换其中的变量 for k2, v2 := range rule.Headers { if strings.Contains(v2, "{{"+k1+"}}") { rule.Headers[k2] = strings.ReplaceAll(v2, "{{"+k1+"}}", value) } } // 替换rule.Path和rule.Body中的变量 rule.Path = strings.ReplaceAll(strings.TrimSpace(rule.Path), "{{"+k1+"}}", value) rule.Body = strings.ReplaceAll(strings.TrimSpace(rule.Body), "{{"+k1+"}}", value) } // 根据oReq的URL Path设置req.Url.Path if oReq.URL.Path != "" && oReq.URL.Path != "/" { req.Url.Path = fmt.Sprint(oReq.URL.Path, rule.Path) } else { req.Url.Path = rule.Path } // 替换Path中的空格为%20 req.Url.Path = strings.ReplaceAll(req.Url.Path, " ", "%20") // 创建新的http请求 newRequest, err := http.NewRequest(rule.Method, fmt.Sprintf("%s://%s%s", req.Url.Scheme, req.Url.Host, req.Url.Path), strings.NewReader(rule.Body)) if err != nil { return false, err // 如果创建请求失败,返回错误 } // 复制原始请求的Header到新请求 newRequest.Header = oReq.Header.Clone() // 设置rule中定义的Headers for k, v := range rule.Headers { newRequest.Header.Set(k, v) } // 发送请求并获取响应 resp, err := DoRequest(newRequest, rule.FollowRedirects) newRequest = nil if err != nil { return false, err // 如果请求失败,返回错误 } // 将响应存入variableMap variableMap["response"] = resp // 如果rule定义了Search规则,执行搜索 if rule.Search != "" { result := doSearch(rule.Search, GetHeader(resp.Headers)+string(resp.Body)) if result != nil && len(result) > 0 { // 如果正则匹配成功 for k, v := range result { variableMap[k] = v } } else { return false, nil // 如果正则匹配失败,返回false } } // 使用CEL环境评估rule.Expression out, err := Evaluate(env, rule.Expression, variableMap) if err != nil { if strings.Contains(err.Error(), "Syntax error") { fmt.Println(rule.Expression, err) // 打印语法错误信息 } return false, err // 如果评估表达式失败,返回错误 } // 如果表达式结果为false,不继续执行后续rule if fmt.Sprintf("%v", out) == "false" { return false, err // 如果最后一步执行失败,返回false } return true, err // 返回评估结果和错误(如果有) }
这里更多的代码就暂且不看了,对于cel表达式这块还不是特别清楚,大致结构已经清晰了
四 Fscan的混淆编译
顺便将go语言fscan的混淆方式也记录一下
主要是garble混淆
go install mvdan.cc/garble@latest garble -literals build main.go
命令
garble -tiny -literals -seed=random build -ldflags="-w -s" main.go
正常编译
混淆编译
其余免杀方法:
exe签名 工具: